понедельник, 19 апреля 2010 г.

The operation has timed out in .NET and connectionManagement

Произошла одна интересная ситуация на одной высоконагруженных систем, над которой мне как-то пришлось работать.

Ситуация следующая:
Один из компонентов системы, который разработан на .NET 2.0 и является Windows сервисом, так же он работал в большое количество потоков, вдруг начинал выдать ошибки о таймауте выполнения операции.
Начали расследование... поставили снифер, оказалось, что соединения даже не открывались. Точнее всё было ещё более интересней: соединения сначала нормально открывались по всем хостам, далее на один хост, на который шло больше всего сообщений, количество соединений доходило примерно до 20, а затем начинали сыпаться ошибки и именно только по этому хосту по остальным всё было нормально.

Долго думали и курили Гуголь, в итоге на одном из забугорных форумов была найдена заметка о том, что майкрософт ограничивает по-умолчанию количество исходящих соединений на один хост... вот такая вот интересная фича.

В общем-то вопрос решался простой вставкой вот такого текста в конфигурационный файл компонента приложения:

<connectionManagement>
<add address="*" maxconnection="5000">
</connectionManagement>

Т.е. тут происходит указание, что всего может быть не более пяти тысяч исходящих подключений. Вообще почитав доки на MSDN можно понять, что тут достаточно шаблонированное управление количеством исходящих подключений и можно конкретно указать как домен, так и поддомен целиком.

понедельник, 1 марта 2010 г.

ConfigurationManager vs WebConfigurationManager

Проблема была в следующем:
Писалась библиотека, которая должна одинаково хорошо работать как в WebApp, так и в ClientApp. Настройка библиотеки для работы, можно было бы осуществлять, как непосредственно (через установку свойств статических классов), так и через *.config приложения. Всё бы ничего, только вот незадача для получения данных из конфига в WebApp используется класс WebConfigurationManager, а для ClientApp используется ConfigurationManager. В общем-то они делают реализацию для одного и того же абстрактного класса. Начал думать, как отличить в каком контексте работает приложение. Было придумано куча вариантов, включая получение пути к конфигурационному файлу (из System.AppDomain.CurrentDomain.SetupInformation.ConfigurationFile и не париться вообще в отличении контекстов) и разбор его вручную чтобы найти свою секцию и забрать из неё данные. Но всё это как-то не прозрачно, да и запутано. А потом я вспомнил про слово-оператор as.... и получилось достаточно компактно и понятно:

DataMapConfigSectionHandler dmcsh;
dmcsh = ConfigurationManager.GetSection("ntlDataMapConfig") as DataMapConfigSectionHandler;
if (dmcsh == null)
{
dmcsh = WebConfigurationManager.GetSection("ntlDataMapConfig") as DataMapConfigSectionHandler;
}

После этого нужно dmcsh проверить на null, так как dmcsh может равняться null, если секция, которую вы указали не найдена.
Правда, такой подход всё равно накладывает определённые нехорошести: классы ConfigurationManager и WebConfigurationManager находятся в разных сборках соответственно System.Configuration.dll и System.Web.dll. CLR придётся загрузить их обе, и соответственно отъесть немного памяти, если для вас это критично, то, наверное всё-таки придётся загружать вручную конфигурационный файл и парсить его ручками, как XML документ.

четверг, 25 февраля 2010 г.

Конвертирование между типами System.String и Enum - кастомным.

В общем-то давно не писал, некогда как-то писать и тут начал возиться с системой используемой в .NET для создания собственных секций файлах .config. Всё бы ничего, только вот некоторые данные, которые должны храниться в конфиге приложения в системе имеют тип Enum.
Самое простое просто написать два хелпера: первый будет состоять из кучи if-else, где последовательно будут сравнивать строки и на выход выкинуть необходимое enum значение. Второй хелпер должен был бы конвертить Enum в строку, опять же просто switch перебирающий значения перечисления и возвращающий строку.

Почесав репу и скрепя клавиатурой сердцем решил так и сделать... но тут понял, что switch-то понятно работает в нормально сборке достаточно шустро - на хешах, а вот перебор вариантов if-else всё-таки перебор и он как известно достаточно медленная вещь.

Погуглив и попробовав несколько вариантов получилась такая вот тема:

1) Для конвертирования System.String в Enum:
(EnumType)Enum.Parse(typeof (EnumType), somethingString, true);

2) Для конвертирования Enum в System.String:
Enum.GetName(typeof(EnumType), somethingString);

Пояснения:
EnumType - это какой-то определённый нами энумератор;
somethingString - строчка с названием одного из членов перечисления;

пятница, 3 апреля 2009 г.

ClearQuest и .NET Часть 3

Ну что же продолжаем знакомиться с API ClearQuest.

Сегодня будем добавлять дефекты в его БД. Получилось два метода, хотя можно было уложить и в один. Собственно вот:


Public Sub AddDefect(ByVal defect As DefectObject)
Dim sessionObj = GetSession()

Dim entityObj = sessionObj.BuildEntity("defect")

'Обязательные поля
entityObj.SetFieldValue("SubProject", defect.SubProject)
AddSectionsInDefect(entityObj, defect.Section)
entityObj.SetFieldValue("Headline", defect.Headline)
entityObj.SetFieldValue("Owner", defect.Owner)
entityObj.SetFieldValue("Type", defect.Type)
entityObj.SetFieldValue("Severity", defect.Severity)
'================================================
entityObj.SetFieldValue("Description", defect.Description)
entityObj.SetFieldValue("FoundInBuild", defect.FoundInBuild)
entityObj.SetFieldValue("OriginalSource", defect.OriginalSource)

Dim status = entityObj.Validate()

If (status <> "") Then Throw New ArgumentException(status, "defect")

entityObj.Commit()
End Sub

Private Sub AddSectionsInDefect(ByVal entityObj As Object, ByVal secrions As IList(Of String))
Dim tempStr As String

For Each tempStr In secrions

entityObj.AddFieldValue("Section", tempStr)

Next tempStr
End Sub

Так... начнём, как обычно, с теории.
API предоставляет достаточно удобный интерфейс для создания и заведения дефектов.
В общем-то процесс добавления разбит на несколько стадий: Получение сессии, создание сущности, заполнение полей, валидация и сохранение в БД.


Получить сессию - думаю понятно, далее создаём сущность, в данном случае - это сущность дефекта, вот так Dim entityObj = sessionObj.BuildEntity("defect") . Заполняем поля - для этого существует специальный метод: SetFieldValue("SubProject", defect.SubProject) , на вход которому необходимо подать, соответственно, название поля и значение, которое необходимо присвоить. Так же существует способ заполнения списочных (коллекций) полей AddFieldValue("Section", tempStr) . Сигнатура метода такая же, но работает он как метод Add(объект) у коллекций во многих языках.

Далее самое интересное - валидация. Она нужна для того, чтобы проверить наш заполненный объект сущности на соответствие схеме базы данных. Так как обычно при развороте схемы появляются поля, которые должны быть заполнены в обязательном порядке, да и константы не кто не отменял. Так вот для валидации служит специальный метод у объекта сущности под названием Validate() . Он возвращает значение строкового типа, а так же меняет статус объект на "валидированный" и фишка в том, что только из этого статуса объект можно сохранить командой Commit() .

Так вот, метод Validate() возвращает строку. Если возвращенная строка пустая, то значит всё ОК, если нет значит нам возвращается описание проблемы, к тому же достаточно подробное. Т.е. это возвращаемое значение нужно как-то обрабатывать.

Всё... теперь, если валидация успешна, можно сохранять данные :) PS. метод
Commit() .

В следующий раз расскажу как аттачить файлы.

среда, 1 апреля 2009 г.

ClearQuest и .NET Часть 2

Продолжаем разговор об API ClearQuest

Как обещал пишем запрос к базе данных CQ.
Как я говорил - любые действия с api начинаются с получения сессии.
В API предусмотрен специальный генератор запросов к базе дефектов. Сейчас я приведу несколько методов, которые я использовал для запроса:
Public Function GetDefectById(ByVal id As String) As DefectObject
Dim cqSession = GetSession()
Dim queryDef = PrepareQueryDef(cqSession)

Dim filterNode = queryDef.BuildFilterOperator(EBoolOp.BOOL_OP_OR)
filterNode.BuildFilter("id", ECompOp.COMP_OP_EQ, id)

Dim resultSet = cqSession.BuildResultSet(queryDef)

Return ExecuteAndParseResultSet(resultSet)(0)
End Function

Private Function PrepareQueryDef(ByVal session As SessionClass) As Object
Dim querydef = session.BuildQuery("defect")
querydef.BuildField("id")
querydef.BuildField("State")
querydef.BuildField("SubProject")
querydef.BuildField("Section")
querydef.BuildField("OriginalSource")
querydef.BuildField("Headline")
querydef.BuildField("Description")
querydef.BuildField("Severity")
querydef.BuildField("Owner")
querydef.BuildField("FoundInBuild")
querydef.BuildField("Type")
querydef.BuildField("Submit_Date")
Return querydef
End Function

Private Function ExecuteAndParseResultSet(ByVal resultSet As Object) As IList(Of
DefectObject)

resultSet.EnableRecordCount()
resultSet.Execute()
Dim count = resultSet.RecordCount

If (count = 0) Then Return Nothing

Dim ListOfDefects As New List(Of DefectObject)

Dim i As Integer

For i = 1 To count

resultSet.MoveNext()

Dim defectObject As New DefectObject()

defectObject.Id = resultSet.GetColumnValue(1)
defectObject.State = resultSet.GetColumnValue(2)
defectObject.SubProject = resultSet.GetColumnValue(3)
defectObject.Section = resultSet.GetColumnValue(4)
defectObject.OriginalSource = resultSet.GetColumnValue(5)
defectObject.Headline = resultSet.GetColumnValue(6)
defectObject.Description = resultSet.GetColumnValue(7)
defectObject.Severity = resultSet.GetColumnValue(8)
defectObject.Owner = resultSet.GetColumnValue(9)
defectObject.FoundInBuild = resultSet.GetColumnValue(10)
defectObject.Type = resultSet.GetColumnValue(11)
defectObject.SubmitDate = resultSet.GetColumnValue(12)

ListOfDefects.Add(defectObject)

Next i

Return ListOfDefects
End Function


Теперь разберём, что же мы делаем в данных функциях.
Функция GetDefectById(ByVal id As String) входная функция, которая видна при использовании класса.
Две другие функции являются вспомогальными и служат просто для логического разделения кода. Функция PrepareQueryDef(ByVal session As SessionClass) As Object настраивает запрос, т.е. в ней указывается какие поля необходимо получить, а функция ExecuteAndParseResultSet(ByVal resultSet As Object) As IList(Of DefectObject) выполняет запрос и разбирает то что нам вернуло API.
Теперь по порядку... что же делается в функциях.

В API для создания запроса существует специальный объект BuildQuery("defect"), который мы получаем из сессии. При его создании мы указываем для какой сущности будем генерировать запрос. Это может быть defect, Section или что-то ещё... в общем зависит от схемы, используемой в вашей базе.

Далее созданный объект запроса необходимо настроить. Настройка заключается как и в SQL в указании того, какие поля необходимы (то что пишется в SQL после ключевого слова select) и какие фильтры мы накладываем на запрос (выражение where).

Указание необходимых полей происходит через метод BuildField("id") , как видно ему передаётся строковый параметр с названием поля.

Для формирования фильтра необходимо получить специальный объект Dim filterNode = queryDef.BuildFilterOperator(EBoolOp.BOOL_OP_OR), у объекта запроса, что я сделал здесь. Передаваемое значение конструктору является указанием на то, что будет за оператор между выражениями. Фактически может быть два значения 1 или 2, соотвественно OR или AND. Далее нужно создать собтвенно выжение указывающее на то по какому полю ведётся фильтрация, делается это в этом выражении filterNode.BuildFilter("id", ECompOp.COMP_OP_EQ, id) , первый параметр указывает по какому полю фильтруем, второй указывает по какому провилу (в данном случаем это правило равенства (=)... и в последнем параметре указывается значение для фильтра. Всем правилам фильрации соответствует большая группа целочисленных констант. В моём примере это COMP_OP_EQ которая в реале равна 1.
На сайте IBM есть более-менее пригодная документации по API,вот тут указано как использовать фильтр. А вот тут рассказано, какие константы правил понимает API.

Так... продолжаем.

Вас наверное уже интересует, а как же получить то, для чего мы формировали запрос. Тут всё классически. В этом смысле API CQ очень похожа на DataReader из стандартной библиотеки .NET. да-да.. всё как обычно экзекьют и получаем выборку :) Правда есть одна загвоздка: по умолчанию отключен каунтер записей, его надо включить :)
Но давайте обо всём по порядку.

Мы подготовили объект запроса. Теперь этот запрос нужно выполнить. Для этого в API существует специальный объект под названием ResultSet, его необходимо получить из объекта сессии вот так Dim resultSet = cqSession.BuildResultSet(queryDef) . Объект получили... теперь включаем подсчёт записей и выполняем запрос: esultSet.EnableRecordCount(), resultSet.Execute() . Всё, теперь результат запроса находится в нашем объекте.

Далее всё аналогично DataReader. Получаем каунт Dim count = resultSet.RecordCount . Так же как и в DataReader есть специальный метод для последовательного перебора записей полученной выборки. Для того чтобы начать читать необходимо сместиться к первой записи resultSet.MoveNext() . Для перевода "корретки" к следующей записи используем этот же метод. Для того чтобы получить доступ к значению поля в записи используем метод GetColumnValue(1) . Параметр передаваемый в данный метод целочисленного типа и определяет номер столбца. Столбцы нумеруются в том же порядке, в котором мы их создавали при создании объекта запроса. Т.е. полная аналогия с DataReader.
Ещё хотелось бы сделать замечание. Если мы просим вернуть дефект со сложным полем (например, Section - т.е. дефект может быть сразу из нескольких областей), то мы получим несколько записей с одним и тем же дефектом, но с разным значеним сложного поля... Поэтому при перегоне выборки в более человеческий вид нужно это учитывать и соответственно обработать соответсвенным образом.

На этом пока всё. :) Потом расскажу как добавлять записи в CQ через API.