Alexey Suvorov dev blog

Мой разработческий блог

Archive for the ‘.net’ Category

Type с null в FullName

leave a comment »

При попытке получить тип generic аргумента метода в не generic классе или generic классе непараметризованного каким либо типом-параметром, FullName результата будет null. При этом GetHashCode и Equals не будут корректно работать по сравнению с типом который можно получить через typeof для open generic. Пример:

// то же для Generic<T>
public class NotGeneric
{
    public void GenericMethod<T>(IEnumerable<T> seq){}
}

var m = typeof(NotGeneric).GetMethod("GenericMethod")
var t = m.GetParameters()[0].ParameterType;
Assert.Null(t.FullName);

Также можно видеть следующие лично для меня неожиданные эффекты:

// то же для Generic<T>
public class NotGeneric
{
    public void GenericMethod<T>(IEnumerable<T> seq){}
}

var m = typeof(NotGeneric).GetMethod("GenericMethod")
var t = m.GetParameters()[0].ParameterType;

Assert.False(t == typeof(IEnumerable<>));
Assert.False(t.GetHashCode() == typeof(IEnumerable<>).GetHashCode());

На самом деле generic уже параметризован типом с пустым GUID. Знания данной детали в принципе достаточно чтобы отличить все типы перегрузок generic методов и вызвать корректный.

// то же для Generic<T>
public class NotGeneric
{
    public void GenericMethod<T>(IEnumerable<T> seq){}
}

var m = typeof(NotGeneric).GetMethod("GenericMethod")
var t = m.GetParameters()[0].ParameterType;

Assert.True(t.GetGenericArguments()[0].GUID == Guid.Empty);

Written by alexeysuvorov

15.12.2014 at 6:28 пп

Опубликовано в .net, C#

Детали реализации стека — часть первая (перевод)

2 комментария

В последнее время мне снова выпала возможность провести несколько собеседований и часто на вопрос «что такое значимые и ссылочные типы» я слышал «значимые типы — это типы, экземпляры которых располагаются на стеке, а ссылочные — это типы, экземпляры которых располагаются в куче». Так что сегодня я решил сделать перевод очень старой, но не потерявшей свою актуальность статье Eric-а Lippert-а. Оригинал статьи можно найти тут. Я постарался сделать перевод как можно более читаемым и лёгким для восприятия на Русском языке, так что он существенно отличается от оригинала по форме, но не по смыслу.

Какое-то время назад я писал о том, что «ссылки» — это не «адреса», когда речь идёт о C# и размещении его объектов в памяти.Хотя это действительно так, но это всего лишь деталь реализации, но не смысл «ссылки». Другая деталь реализации, которую часто путают с сутью — это то, что «память под значимые типы (value types) выделяется на стеке». Я часто это вижу, потому что именно так написано в нашей документации.

Практически каждая статья, которую я вижу, подробно описывает (часто неверное) что такое стек и что основное различие между значимыми и ссылочными типами — это то, что значимые типы располагаются на стеке. Я уверен Вы можете найти множество примеров таких статей в сети.

Я считаю, что определение значимых типов, которое основанное на деталях реализации, а не на их наблюдаемом поведении одновременно и запутывающее, и не совсем правильное. Наиболее значимой характеристикой значимого типа является не то как он располагается в памяти, а то как они ведут себя с точки зрения семантики: «значимые типы» всегда передаются «по значению», т.е. копируются. Если бы основные различия между ссылочными и значимыми типами были бы в деталях расположения в памяти, то мы бы назвали их «типы в куче» и «типы на стеке». Но в общем случае это не имеет никакого отношения к сути. В общем случае важно то как экземпляры значимых типов копируются и сравниваются.

К сожалению документация не сфокусирована на наиболее значимых характеристиках, но сфокусирована на деталях реализации и упускает суть значимых типов. Я бы очень хотел, чтобы все те статьи, которые объясняют, «что такое стек» вместо этого объясняли бы что такое «копирование по значению» и как непонимание этого механизма может стать причиной ошибок.

Утверждение о том, что значимые типы располагаются на стеке в общем случае не верно. В документации на MSDN правильно замечено, что значимые типы располагаются на стеке иногда. Например, поле типа int в ссылочном типе — это часть объекта этого типа и, как и весь объект его поле расположено в куче. Та же история с локальными переменными, которые попадают в замыкание анонимных методов (*), потому что они по сути становятся полями скрытого класса и тоже располагаются в куче, так что локальные переменные могут располагаться в куче даже если они значимого типа.

Короче говоря, мы имеем объяснение, которое ничего не объясняет. Отбросив соображения производительности что ещё, с точки зрения ожиданий разработчика может заставить CLRjitter разместить переменную типа int на стеке, а не в куче? Ничего, пока не нарушается спецификация система может выбирать наиболее эффективную стратегию генерирования кода.

Ага, никто не обещал, что операционная система поверх которой реализован CLI предоставляет массив размером в 1 мегабайт под названием «стек». Windows обычно делает это и этот одно мегабайтный массив отличное место для того, чтобы эффективно хранить небольшие объекты с коротким временем жизни, но нет никаких требований или гарантий, что операционная система предоставляет такого рода структуру или что jitter будет её использовать. В принципе Jitter может решить создавать все локальные переменные в куче не смотря на потерю производительности, это будет работать пока семантические требования к значимым типам выполняются.

Ещё хуже думать, что значимые типы «быстрые и маленькие» а ссылочные «большие и медленные». Действительно, значимые типы могут быть сгенерированные jitter-ом в код на стеке, который очень быстр как при выделении, так и при очистке памяти. Но при этом большие структуры, создаваемые на куче, такие как массив элементов значимого типа, тоже создаются очень быстро при условии, что они инициализируются значениями по умолчанию. И ссылочные типы занимают дополнительное место в памяти. И конечно существуют такие условия, когда значимые типа дают большой выигрыш в производительности. Но в подавляющем большинстве программ то, как локальные переменные создаются и уничтожаются не может быть узким местом с точки зрения производительности.

Нано оптимизации по превращению ссылочных типов в значимые дающие несколько наносекунд выигрыша не стоят того. Это нужно делать только если данные профайлера показали, что существуют настоящие проблемы которые могут быть решены использованием значимых типов вместо ссылочных. У меня нет таких данных. При выборе использовать ссылочный или значимый тип я всегда руководствуюсь тем как тип, который я создаю должен вести себя семантически.

(*) справедливо и для блока итератора.

Written by alexeysuvorov

27.04.2014 at 1:33 пп

Опубликовано в .net, C#

Fody.MethodDecorator

with one comment

AOP — акроним который некоторые читают как «больше не нужно писать много кода». На данный момент из бесплатных тулов, на мой взгляд лидирует Fody с неимоверным количеством полезных и бесполезных плагинов казалось бы на все случаи жизни. При ближайшем рассмотрении однако часто оказывается, что инструмент не такой зрелый как хочется. Но исходный код открыт и лицензия не запрещает лепить по образу и подобию. Примерно так и появился MethodDecoratorEx — мой форк Fody.MethodDecorator.

Первоначально задача стояла реализовать бизнес журналирование с набором метрик который мог бы легко расширяться. Для этой задачи отлично подходит MethodDecorator, но проблема была в том, что MethodDecorator:
1 — требовал чтобы атрибут которым помечается метод для логирования был в коренном namespace (читай вообще без namespace)
2 — требовал чтобы атрибут был в той же сборке что и декорируемый код, хотя обычно такие вещи выносят в общие сборки
3 — не умел получать параметры декорируемого метода

Для того чтобы устранить эти и некоторые другие недостатки я начал свою ветку разработки: https://github.com/alexeysuvorov/MethodDecorator

Как всем этим пользоваться

Устанавливаем nuget package

>Install-Package MethodDecoratorEx.Fody

Он автоматически установит Fody в выбранный проект (проекты). Устанавливать MethodDecorator имеет смысл только в проекты, которые содержат методы которые необходимо декорировать. Fody и MethodDecorator не навязывают использование ссылок на какие либо сборки.

После того как пакет добавился настало время выбирать по какому пути пойти в реализации атрибута. Пути всего два:
1 — Создать аттрибут содержащий методы OnEnter, OnExit, OnException (OnInit если нужно получать параметры метода и this) определённых сигнатур и методы, которые необходимо декорировать и сборку или модуль в которых этот код находится этим же самым атрибутом. Т.е. Флаги на аттрибуте будут: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Assembly | AttributeTargets.Module)]. Модуль или сборку мы помечаем для того, чтобы MethodDecorator мог найти этот атрибут не перебирая все атрибуты всех методов убийственно замедляя время компиляции. Атрибут может находиться в какой угодно сборке в которой он может успешно реализовать логику внутри методов.
2 — В папке пакета есть сборка MethodDecoratorInterfaces с интерфейсом IMethodDecorator. Атрибут реализующий этот метод будет найден MethodDecorator-ом и все помеченные им методы будут декорированы кодом вызывающим методы этого атрибута.

Одна из возможных реализаций

// Atribute should be "registered" by adding as module or assembly custom attribute
[module: Interceptor]

// Any attribute which provides OnEntry/OnExit/OnException with proper args
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Assembly | AttributeTargets.Module)]
public class InterceptorAttribute : Attribute, IMethodDecorator {
    // instance, method and args can be captured here and stored in attribute instance fields
    // for future usage in OnEntry/OnExit/OnException
    public void Init(object instance, MethodBase method, object[] args) {
        TestMessages.Record(string.Format("Init: {0} [{1}]", method.DeclaringType.FullName + "." + method.Name, args.Length));
    }
    public void OnEntry() {
        TestMessages.Record("OnEntry");
    }

    public void OnExit() {
        TestMessages.Record("OnExit");
    }

    public void OnException(Exception exception) {
        TestMessages.Record(string.Format("OnException: {0}: {1}", exception.GetType(), exception.Message));
    }
}

public class Sample {
    [Interceptor]
    public void Method()
    {
        Debug.WriteLine("Your Code");
    }
}

Соответственно метод Sample.Method будет декорирован и примет вид

public class Sample {
    public void Method(int value) {
        InterceptorAttribute attribute = 
            (InterceptorAttribute) Activator.CreateInstance(typeof(InterceptorAttribute));

        // in c# __methodref and __typeref don't exist, but you can create such IL 
        MethodBase method = MethodBase.GetMethodFromHandle(__methodref (Sample.Method), 
                                                           __typeref (Sample));

        object[] args = new object[1] { (object) value };

        attribute.Init((object)this, method, args);

        attribute.OnEntry();
        try {
            Debug.WriteLine("Your Code");
            attribute.OnExit();
        }
        catch (Exception exception) {
            attribute.OnException(exception);
            throw;
        }
    }
}

Подробней остановлюсь на методе Init. Если этого метода нет в атрибуте, то код создания массивов параметров тоже не будет генерироваться. В статических классах параметр this будет null, так что его всегда следует проверять. Как видно из примера инстанс атрибута создаётся на один вызов метода, так что не стоит беспокоится об изменении состояния внутри класса.

Совсем недавно был добавлен атрибут IntersectMethodsMarkedByAttribute (имя атрибута фиксировано, MethodDecorator ищет его по имени). Он позволяет декорировать методы помеченные атрибутами не имеющими реализаций методов OnEnter/OnExit/OnException. В этом случае будут вызваны методы самого IntersectMethodsMarkedByAttribute. Самый простой сценарий который можно представить — это

[module:IntersectMethodsMarkedBy(typeof(TestMethod))]

В данном случае будут декорированы все тестовые методы помеченные майкрософтовским тестовым атрибутом.

Это вроде всё. Напишу потом поподробней как MethodDecorator ищет классы и атрибуты если будет интерес.

Written by alexeysuvorov

24.04.2014 at 2:53 дп

Опубликовано в .net, C#, Fody

MetadataExchangeClient и Windows authentication

with one comment

В какой то момент встала задача подружить MetadataExchangeClient и WCF сервис работающий на сайте с windows аутентификацией. «Дружба» приложения с сервисом происходила в 2 этапа: 1 – прочитать контракт 2 – читать/передавать данные. С этапом 2 всё решилось добавление секции security в байндинг, но вот с этапом 1 возникли неожиданные трудности.

Читать метаданные можно несколькими способами, но самый простой с точки зрения и сервера и клиента выглядит так:

var metaTransfer = new MetadataExchangeClient();
var metadataSet = metaTransfer.GetMetadata(url, MetadataExchangeClientMode.HttpGet);

Читает метаданные без настройки mex эндпоинта на стороне сервера, но есть один нюанс. Если сосредоточится и передать в конструктор MetadataExchangeClient экземпляр httpBinding с правильно настроенными параметрами безопасности, то ничего не случится. Точнее случится попытка неавторизованного доступа и исключение. Самое правильное место — это свойство HttpCredentials куда нужно передать что нибудь вроде CredentialCache.DefaultCredentials

var metaTransfer = new MetadataExchangeClient();
metaTransfer.HttpCredentials = CredentialCache.DefaultCredentials;
var metadataSet = metaTransfer.GetMetadata(url, MetadataExchangeClientMode.HttpGet);

А можно было бы и виртуальный метод переопределить, который возвращает HttpWebRequest и это даже было бы правильнее с архитектурной точки зрения – не вывешивать свойство наружу, но это не первая жертва подхода «будь проще» во фреймворке.

Written by alexeysuvorov

11.09.2013 at 9:45 пп

Опубликовано в .net, WCF

NHibernate.StaleStateException и порядок следования в мапингах nHibernate

leave a comment »

Все примеры orm показывают как использовать их правильно, но в реальной жизни их зачастую используют неправильно по соображениям удобства или производительности. В основном проекте мы используем nHibernate и делаем это неправильно :).

Если в 2-х словах то есть контейнер, который содержит назовем их виджеты, еще у контейнера есть сущности которые позволяют управлять некоторыми параметрами целой группы виджетов.

nHibernate

Так как ни группа ни свойства заранее неизвестны, то в базе они хранятся как ассоциация состоящая из WidgetId и PropertyId (на схеме PropertyId пропущено т.к. несущественно). Вся проблема заключается в том, что при удалении контейнера нужно удалять все виджеты и все мапинги на них. Но т.к по соображениям удобства мапинг содержит только WidgetId а не сам виджет, то nHibernate ничего не знает про эту связь и логичным решением поддержания базы в консистентном состоянии видится внешний ключ из таблицы мапингов на таблицу виджетов. Но тут же возникает проблема при удалении самого контейнера. nHibernate как ответственный бухгалтер считает что он сам должен удалить все учтённые мапинги и выстреливает исключением NHibernate.StaleStateException: Unexpected row count: 0; expected: 1 если вдруг база данных самовольно сделала это за него.

Решение оказалось очень простым — порядок следования деклараций в hbm файле является значимым и помещение тега коллекции мапингов до тега коллекции виджетов спасает ситуацию.

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping assembly="..." 
                   namespace="..." 
                   xmlns="urn:nhibernate-mapping-2.2">
  <class name="Container">
    <id name="Id">
      <generator class="..." />
    </id>
    
   <set name="CommonParameters" table="CommonPropertyMapping" cascade="all-delete-orphan">
      <key column="ContainerId" not-null="true" />
      <one-to-many class="CommonProperty" />
   </set>
   <set name="Widgets" table="Widget" cascade="all-delete-orphan">
      <key column="ContainerId" not-null="false" />
      <one-to-many class="Widget"/>
   </set>
  </class>
</hibernate-mapping>

Немного криво? Полностью согласен! Но мы живем в реальном мире.

Written by alexeysuvorov

06.06.2013 at 7:21 дп

Опубликовано в .net, nHibernate

Tagged with

Не найден Microsoft.Office.Interop.Word

leave a comment »

У меня потерялся весь Interop. Потерялся очень странно, потому что я видел его в GAC в списке сборок, но физически на диске сборки отсутствовали. Переустановка офиса не помогла, не помогло и добавление Office tools for visual studio к сетапу студии потому что как оказалось отношения к interop это не имеет вообще.
Далее 3 шага, каждый из которых может привести к решению проблемы, упорядоченные по возрастанию кривизны решения:

  • Установить Microsoft Office 2010: Primary Interop Assemblies Redistributable скачав его отсюда
  • Если не помог инсталлер и сборки не появились в add reference в студии, то скачиваем lessmsi или аналогичную тулу которая умеет распотрошить msi. Открываем msi скаченный на предыдущем шаге и достав сборки инсталлируем их в gac вручную
  • Мне не помог и вариант #2 так что я просто сложил нужные сборки (office.dll и Microsoft.office.interop.word.dll) в директорию к своему скрипту

В общем и целом опыт очень неприятный.

Written by alexeysuvorov

06.06.2013 at 7:20 дп

Опубликовано в .net, Uncategorized

Tagged with

F# — парсинг XML экспортированного из Excel

leave a comment »

WCF сервисы, SQL и noSQL базы данных — это реалии крупных западных компаний. Местный малый бизнес до сих пор обменивается данными в Word документах, в лучшем случае в Excel. Правильный парсинг Excel должен начинаться как-то так:

Microsoft.Office.Interop.Excel.Application excelApp = new Microsoft.Office.Interop.Excel.Application();

Но на shared хостинге это решительно невозможно и тогда компромиссным решением может служить XML экспорт из Excel. Проблема этого подхода в том, что придётся делать всё с нуля. В силу привычки для парсинга я использую HtmlAgilityPack, но в данном случае возникла проблема с namespaces. Дело в том, что экспорт Excel в XML выглядит как то так:

<?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:o="urn:schemas-microsoft-com:office:office"
 xmlns:x="urn:schemas-microsoft-com:office:excel"
 xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:html="http://www.w3.org/TR/REC-html40">
...
 <Worksheet ss:Name="Battery">
  <Table ss:ExpandedColumnCount="6" ss:ExpandedRowCount="537" x:FullColumns="1"
   x:FullRows="1" ss:DefaultRowHeight="15">
   <Column ss:Width="45"/>
   <Column ss:AutoFitWidth="0" ss:Width="374.25"/>
   <Column ss:Width="38.25"/>
   <Column ss:Width="57.75" ss:Span="2"/>
   <Row ss:Height="30">
    <Cell ss:StyleID="s64"><Data ss:Type="String">X-XX-951</Data></Cell>
    <Cell ss:StyleID="s66"><Data ss:Type="String">***</Data></Cell>
    <Cell ss:StyleID="s66"><Data ss:Type="String">        2600,00</Data></Cell>
    <Cell ss:StyleID="s66"><Data ss:Type="String">        2250,00</Data></Cell>
    <Cell ss:StyleID="s66"><Data ss:Type="String">        1850,00</Data></Cell>
   </Row>
  </Table>
 </Worksheet>
...
</Workbook>

Все ячейки имеют namespace и xpath выражение должно быть построено с учётом этого факта. Альтернативой был saxon, но стиль в котором написан API выжимает слезу — вспоминаются институтские годы и .net 1.0. Так что меньшим злом оказалась обёртка над стандартным XmlDocument. XmlNamespaceManager должен передаваться в каждый запрос, так что имеет его сохранить. Код:

module XmlHelpers =
    let EnumerableToSeq<'a> (x:System.Collections.IEnumerable) =
        let enumerator = x.GetEnumerator()
        seq {
            while enumerator.MoveNext() do 
                yield (enumerator.Current :?> 'a)
        }

    type NsEnabledDocument (fileName:string) =
        let doc = new XmlDocument()
        do doc.Load(fileName)
        let nsManager = new XmlNamespaceManager(doc.NameTable)
        //все запросы будут выполнены с namespace manager-ом
        let doSelect sel p = sel(p,nsManager)
        
        member public this.AddNamespace ns nsVal =
            nsManager.AddNamespace(ns,nsVal)
            nsManager.PushScope()

        member public this.Select path = 
            doSelect doc.DocumentElement.SelectNodes path 
            |> EnumerableToSeq<XmlNode>

        member public this.SelectSingle path =
            doSelect doc.DocumentElement.SelectSingleNode path

Использовать как-то так:

let doc = NsEnabledDocument(file)
doc.AddNamespace "ss" "urn:schemas-microsoft-com:office:spreadsheet"
let nodes = doc.Select "//ss:Worksheet[@ss:Name='Battery']/ss:Table/ss:Row"

После получения документа и выборки ячеек надо понять что содержит строка, является ли она заголовком или содержит нужные данные. Получение типа строки по его содержимому:

let (|CountVal|DoubleVal|StringVal|) (s:string) =
    match s with
    | "****" -> CountVal ">30"
    | "***"  -> CountVal "11-30"
    | "**"   -> CountVal "4-10"
    | "*"    -> CountVal "1-3"
    | s ->
        let (b,d) = Double.TryParse(s,NumberStyles.Any,CultureInfo.CurrentCulture.NumberFormat)
        if b then DoubleVal(d)
        else StringVal(s)

Этот код полностью зависит от предметной области, в данном случае ячейка содержащая «*» — «****» означает примерное количество товара на складе.
Соответственно ячейки делятся на 4 типа: с данными не попадающие в другие категории (DataCell), ячейка с ценой (DoubleCell), ячейка с количеством (CountCell) и Xml не являющийся ячейкой (NotCell):

let (|DataCell|DoubleCell|CountCell|NotCell|) (n:XmlNode) 
    : Choice<string,double, string, unit> =
    if null = n then NotCell
    else
        match (n.Descendants() |> Seq.toList) with
        | (data : XmlNode) :: [] -> 
            match data.InnerText with 
            | CountVal x -> CountCell(x)
            | DoubleVal x -> DoubleCell(x)
            | StringVal x -> DataCell(x)
        | _ -> NotCell

Здесь .Descendants() — метод расширение

type System.Xml.XmlNode with
        member this.Descendants () =
            this.ChildNodes |> EnumerableToSeq<XmlNode>

Теперь код парсинга примитивно прост — мы задаём тип и последовательность ячеек которые должна содержать строка и конструируем готовый объект из уже типизированных объектов:

let PriceRowToObject (row:XmlNode) = 
    let cells = row.Descendants() |> FilterTextNodes |> Seq.toList
    match cells with
    | DataCell(a) :: DataCell(desc) :: CountCell(count) :: 
      DoubleCell(p1) :: DoubleCell(p2) :: DoubleCell(p3) :: _ -> 
        Some({Article = a; Count = count;  Desc = desc; PriceMax = p2; PriceMin = p3}: PriceItem)
    | _ -> None

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

Written by alexeysuvorov

17.06.2012 at 5:09 пп

Опубликовано в .net, F#

Сериализация классов из динамически загружаемых сборок (плагинов)

with one comment

Сериализация объектов для передачи в сообщениях задача с первого взгляда тривиальная, ровно до момента пока все сериализуемые классы достижимы на момент компиляции. Проект freespace по своей сути является набором плагинов объединенных системой коммуникации на основе xmpp. Xmpp протокол предусматривает общение с помощью сообщений в XML формате, т.е. Сборка отвечающая за коммуникацию будет сериализовывать и десериализовывать теоретически неограниченное разнообразие классов из динамически подгружаемых через MEF плагинов. Нужно отметить что я буду говорить о data contract сериализации, а не о стандартной, основная причина – использование сериализайии на основе контрактов не накладывает требования иметь публичный конструктор без параметров. Сперва код, который не работает:

public static Data InitializeFrom<T>(this Data obj, T payload)
    where T : SomeBaseClassForAllMessages
{
    Contract.Requires(obj != null, "Data object must be pre-initialized");
    var s = new DataContractSerializer(payload.GetType());
    var sb = new StringBuilder();
    using (var xw = XmlWriter.Create(sb))
    {
    s.WriteObject(xw, payload);
    xw.Flush();
    obj.Add(XElement.Parse(sb.ToString()));
    }

    return obj;
}

Весь вопрос в том, как рантайм узнает какой именно тип десериализовывать, на входе у него только строка XML в которой теоретически есть имя класса, но рантайм не будет метаться по загружённым сборкам в поисках нужного класса. Выход достаточно прост:

//stream и object - внешние параметры
var knownTypes =
    (from assembly in AppDomain.CurrentDomain.GetAssemblies()
        //can be any perfix, or without it at all
        where assembly.FullName.StartsWith("ActiveMesa.FreeSpace")
        select assembly.GetTypes()
        .Where(t => t.IsSubclassOf(typeof(FreeSpaceMessage))))
        .Aggregate((prev, next) => prev.Concat(next));
var des = new DataContractSerializer(obj.GetType(), KnownTypes);
des.WriteObject(stream,obj);

И на последок чуть чуть опитмизации, создание сериализатора / десериализатора очень затратная операция и когда сообщений много (во FreeSpace их действительно много) то это может больно ударить по производительности

internal static class DataContractSerializersStorage
{
    private static readonly Dictionary _serializers = new Dictionary();

    private static readonly object _syncobj = new object();
    private static volatile IEnumerable _knownTypes;
    private static IEnumerable KnownTypes {
        get
        {
            if (null == _knownTypes)
            {
                lock (_syncobj)
                {
                    if(null == _knownTypes){
                        var tmp = (from assembly in AppDomain.CurrentDomain.GetAssemblies()
                            where assembly.FullName.StartsWith("ActiveMesa.FreeSpace")
                            select assembly.GetTypes()
                                .Where(t => t.IsSubclassOf(typeof(FreeSpaceMessage))))
                                .Aggregate((prev, next) => prev.Concat(next));
                        _knownTypes = tmp;
                    }
                }
            }
            return _knownTypes;
        }
    }
    public static DataContractSerializer GetSerializer(Type t)
    {
        var name = t.FullName;
        if(!_serializers.ContainsKey(name))
        {
            lock (_serializers)
            {
                if (!_serializers.ContainsKey(name))
                {
                _serializers.Add(name, new DataContractSerializer(t, KnownTypes));
                }
            }
        }
        return _serializers[name];
    }
}

В конце хочется отметить что лично мне больше по вкусу json сериализация, слишком уж XML широк многословен.

Written by alexeysuvorov

21.08.2011 at 8:44 дп

Опубликовано в .net, C#

Tagged with , ,

mongodb: «old lock file, terminating»

with one comment

Короткий пост, про конкретную проблему.

Очень неприятный момент, особенно у нас, где сервера даже у «хорошего» хостера иногда падают от перебоев с питанием, причём падают как будто из них действительно шнур выдёргивали. Работая на Mongo 1.6.5 такой проблемы нет, но перейдя на 1.8.1 однажды утром выяснилось, что база сайта недоступна и сервак жутко тормозит.

В логах было что то вроде
exception in initAndListen std::exception: old lock file, terminating
На деле выяснилось, что Mongo с какой то версии стал создавать mongod.lock файл в директории с данными и при жёстком (без возможности корректно завершить работу сервиса или mongod процесса) выключении он этот файл не удаляет. После загрузки монго проверяет этот файл и видя в нём старые данные повисает в бесконечном цыкле пытаясь запуститься при этом отъедая весь процессор. Проблема решалась переводом сервиса монго в состояние Disabled, удалением lock файла и запуском службы, но это всё руками и вконечно в production такого допускать нельзя.

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

 if ( oldFile ) {
            // we check this here because we want to see if we can get the lock
            // if we can't, then its probably just another mongod running
            string errmsg;
            if (cmdLine.dur) {
                if (!dur::haveJournalFiles()) {
                    vector<string> dbnames;
                    getDatabaseNames( dbnames );
                    if ( dbnames.size() == 0 ) {
                        // this means that mongod crashed
                        // between initial startup and when journaling was initialized
                        // it is safe to continue
                    }
                    else {
                        errmsg = str::stream()
                            << "************** \n"
                            << "old lock file: " << name << ".  probably means unclean shutdown,\n"
                            << "but there are no journal files to recover.\n"
                            << "this is likely human error or filesystem corruption.\n"
                            << "found " << dbnames.size() << " dbs.\n"
                            << "see: http://dochub.mongodb.org/core/repair for more information\n"
                            << "*************";
                    }
                }
            }
            else {
                errmsg = str::stream()
                         << "************** \n"
                         << "old lock file: " << name << ".  probably means unclean shutdown\n"
                         << "recommend removing file and running --repair\n"
                         << "see: http://dochub.mongodb.org/core/repair for more information\n"
                         << "*************";
            }

            if (!errmsg.empty()) {
                cout << errmsg << endl;
#ifdef WIN32
                CloseHandle( lockFileHandle );
#else
                close ( lockFile );
#endif
                lockFile = 0;
                uassert( 12596 , "old lock file" , 0 );
            }
        }

Отмечу лишь, что лучше сразу идти на http://www.mongodb.org/display/DOCS/Journaling. При установке mongo нужно указать
--journal
что решает проблему со старым lock фалом, но добавляет небольшие (по заверениям разработчиков) накладные расходы при записи + будет тратится время на обработку этих журналов после hard reset, что в моей ситуации более чем приемлимо.

PS: Интересно было посмотреть в сорцы монго,встречаются комментарии

//instance.cpp line 773
/* This ought to be an unlink(), but Eliot says the last
time that was attempted, there was a race condition
with acquirePathLock().  */

Скажет — как отрежет.

Update:
При включении опции —journal и попытке подключить существующие файлы баз данных возможна ошибка:

Tue Apr 19 20:53:58 [dur] lsn set 477451
Tue Apr 19 20:54:23 [initandlisten] connection accepted from 127.0.0.1:1196 #2
Tue Apr 19 20:54:23 [conn2] createPrivateMap failed *:/****/****/*******.* errno:8 Недостаточно памяти для обработки команды.
Tue Apr 19 20:54:23 [conn2] Assertion: 13636:createPrivateMap failed (look in log for error)
Tue Apr 19 20:54:23 [conn2] end connection 127.0.0.1:1196

Written by alexeysuvorov

18.04.2011 at 8:11 пп

Опубликовано в .net, MongoDb

MongoDB C# driver (10gen) — продолжение

2 комментария

После первого поста про mongodb я на несколько недель переключался на f# и вернувшись понял, что всё забыто. Пришлось перечитывать собственный пост, и видимо не одному мне раз даже Dmitry Nesteruk влепил мне like. Лирическое отступление закончилось, дальше будут несколько вещей, которые так или иначе отняли у меня время на расследование как это работает, а иногда и на копание в исходных кодах драйвера. Сразу оговорюсь, в данной статье как и в предыдущей речь пойдёт о драйвере, ссылка на который размещена на сайте монго, это драйвер от 10gen.

Частичная загрузка объектов

Начав переводить один из реальных проектов на Монго я сразу столкнулся с необходимостью загружать объекты без вложенных коллекций, которые он содержит. Надо заметить, что рассматриваемый драйвер работает очень просто. Через классы хелперов и визардов он просто складывает строки чтобы получить работающий запрос на родном для mongo javascript. Чтобы не загружать какое то поле, достаточно в запросе указать field_name:0

  db.users.find({}, {thumbnail:0});

В драйвере всё не так очевидно, но после некоторого копания удалось получить вот такой код:

MongoCollection coll = GetCollection();
FieldsBuilder fbExclude = Fields.Exclude(new string[]{“thumbnail”});
//can be FindAllAs<TEntity>()
MongoCursor result = coll.FindAll().SetFields(fbExclude);

Все результаты в курсоре будут без поля thumbnail, проверено, работает замечательно. Все не загруженные поля будут инициализированы значениями по умолчанию, для коллекций null. Тут есть одна тонкость, если вы не загрузите какое либо поле, а потом запишете этот объект, то он совершенно законно перетрёт старый в базе и вот такой тест упадёт на последнем Assert-е:

public class Data
{
  [BsonId]
  public ObjectId Id {get;set;}
  public int Area {get;set;}
}
Data x = new Data();
x.Area = 20;
var db = GetDb();
var coll = db.GetCollection<Data>(typeof(Data).FullName);
db.ClearColl<Data>();
coll.Save(x);
Data y = coll.FindAllAs<Data>().SetFields(Fields.Exclude(new string[]{"Area"})).FirstOrDefault();
Assert.AreEqual(0, y.Area);
coll.Save(y);
Data z = coll.FindAllAs<Data>().SetFields(Fields.Exclude(new string[] { "Area" })).FirstOrDefault();
//fail of course
Assert.AreEqual(20, z.Area);

Я думаю это как то решается через Find and modify, но пока такой потребности не стоит и всё что нужно менять грузится полностью.

Выбор по элементу вложенного массива

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

public class HighLevelCodeInterval 
{
  public HighLevelCodeInterval() { }
    
  public HighLevelCodeInterval(int mn, int mx) 
  {
    Min = mn; Max = mx;
  }
  public int Min { get; set; }
  public int Max { get; set; }
}
public class RegionObject 
{
  public RegionObject() 
  {
    PostalCodes = new List<int>();
  }
 
  [BsonId]
  public int Id { get; set; }
  public string Name { get; set; }
  public List<HighLevelCodeInterval> HighLevelCodes { get; set; }
  public List<int> PostalCodes { get; set; }
}

Тогда запрос выборки по конкретному коду будет:

QueryComplete q = Query.EQ("PostalCodes", postCode);
MongoCollection coll = GetCollection();
RegionObject result = coll.FindOneAs<RegionObject>();

Я бы сказал неожиданно просто. Для поиска по диапазонам приведу только сам запрос:

int nCodeValue = …;
Query.And(     
     	Query.LTE("HighLevelCodes.Min", nCodeValue),
                	Query.GTE("HighLevelCodes.Max", nCodeValue)
)

Надо признать, что поиска по вложенным массивам в монго предстален очень достойно.

Идентификаторы

Драйвер поддерживает 2 встроенных генератора идентификаторов: для Guid и ObjectId типов, т.е. достаточно декорировать свойства одного из этих типов аттрибутом BsonId и дальше всё сделается само. Но идентификатор может быть любым, у меня отлично работают целочисленные идентификаторы, но за их уникальностью следит внешняя по отношению к монго программа. Так же утверждают, есть возможность самом написать генератор:

public class EmployeeIdGenerator : IIdGenerator 
{
  object GenerateId(){ ... }
  bool IsEmpty(object id) { ... }
}
public class Employee 
{ 
  [BsonId(IdGenerator = typeof(EmployeeIdGenerator)]
  public int Id { get; set; }
  // other fields or properties
}

Сам я правда не пробовал.

Заключение

Документо ориентированные базы данных подходят далеко не для всех проектов. Если не получается красиво, то может просто DDD неприменим. Я нашёл что один из моих проектов идеально укладывается в DDD и несказанно рад исчезновению огромного числа таблиц, если всё будет хорошо, то скоро будут тесты производительности в духе MondoDB vs MSSQL на реально работающем приложении.

PS: Хочу упомянуть утилиту , которая очень помогает посмотреть что записалось в базу в результате выполненных операций – это Mongo Vue

Written by alexeysuvorov

11.03.2011 at 11:08 пп

Опубликовано в .net, C#, MongoDb