Страницы

вторник, 1 апреля 2014 г.

Вакансия .NET разработчика в моей командe.

 

Наши продукты развиваются, что потихоньку приводит к необходимости расширения команды.

В ассортименте сложные и интересные задачи, и амбициозные планы по развитию продуктов.

Откликайтесь на вакансию. Ищем двух человек, добровольная акция “приведи друга” приветствуется. Smile

пятница, 12 апреля 2013 г.

NLog. Memory Leak.

Вчера вечером один из наших сервисов перешёл в странное состояние. Занял всю свободную память (около 5 Гб), но еще продолжал работать. При этом обычное потребление  составляло 200-300 Мб. В общем, проблему решили традиционным для утечек памяти способом – сделали дамп памяти через Process Explorer, и перезапустили сервис.

Анализ дампа через WinDebug показал огромное количество объектов типа LogEventInfo, и сопутствующих ему объектов. Если коротко – то во всём виноват NLog. Почему такое произошло было непонятно, но решили посмотреть как будет развиваться ситуация. Весь вечер ничего подозрительного не происходило. Однако утром сервис скушал уже около 1Гб памяти, и потихоньку продолжал набивать себя байтами. Надо было что-то срочно менять.

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

На таких конфигурациях работало 7 target'ов. Но сломался только один, нагрузка на который была особенно интенсивной (8-10 ГБ в сутки). Изучение документации на AsyncWrapper показало наличие интересного параметра batchSize:

batchSize - Number of log events that should be processed in a batch by the lazy writer thread. Integer Default: 100

Т.е. в нашем случае AsyncWrapper сбрасывал не более 100 событий за один раз. При этом параметр timeToSleepBetweenBatches равен 500 мс (значение по-умолчанию 50 мс приводило к очень серьёзной загрузке ЦП, поэтому мы брали больший интервал). Итого NLog записывал на диск не более 200 записей в секунду. Очевидно, надо было попробовать увеличить количество записей. Кроме того, проконсультировавшись с документаций на FileTarget, увеличил размер буфера записи. В итоге получилась такая конфигурация:

Через полминуты сервис вернулся к своему обычному состоянию. Мораль - внимательно изучайте возможности своих инструментов!

среда, 10 апреля 2013 г.

Codestellation DarkFlow. Неправильный путь.

Когда я только приступил к реализации, идеологически скопировал архитектуру TPL. Каждая реализация IExecutor была владельцем потоков и своей очереди задач. Чтобы добиться желаемого уровня многопоточности предполагалось передавать каждому клиенту требуемый экземпляр IExecutor.

image

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

  • Интеграция с DI-контейнером требовала интенсивной чёрной магии, чтобы клиенту желаемый экземпляр IExecutor. Игры с точками расширения контейнера позволяли это сделать, но было выглядело туманно и ненадёжно.
  • Само по себе неявная зависимость способа исполнения задачи от того, кто поместил ее в очередь, а не от самой задачи выглядит противоестественно. Управлять этим процессом правильнее, отталкиваясь от задачи.
  • Нет контроля над общим уровнем многопоточности, т.к. каждый IExecutor не общается с другим, и количество одновременно запущенных потоков может достигать суммы ограничений потоков для всех исполнителей.
  • Конфигурировать приходилось каждый исполнитель в отдельности, и Dispose’ить тоже. Что добавляло излишней возни с кодом.

Под тяжестью проблем пришлось практически полностью переписать реализацию. Что из этого получилось расскажу в следующий раз.

воскресенье, 7 апреля 2013 г.

Codestellation DarkFlow. API.

Как я и говорил в прошлый раз, ключ к читаемому и поддерживаемому коду – отделение кода многопоточности от кода приложения. Об этом писал еще Фаулер (Martin Fowler)  в своей библии PoEAA. Вынести многопоточность на уровень инфраструктуры очень заманчиво, и… вполне возможно! Но с помощью голого .NET этот узел не разрубить.

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

Мне очень нравятся эти интерфейсы. Они просты, и разделяются две очень важные вещи: действие от момента его исполнения. Просим выполнить задачу, и всё. Когда она выполнится – решит реализация IExecutor. Стоит отметить, что для успешного применения код приложения должен быть написан в стиле, который исключает предположения о моменте и порядке выполнения задач. В противном случае получается транзитивная связность между клиентским и инфраструктурным кодом. Такой код тяжело понимать, отлаживать и тестировать.

Задача простоты тестирования для таких абстракций упрощается – необходимости тестировать многопоточный код нет. Он протестирован на уровне реализации IExecutor. А любую реализацию ITask легко тестировать, вызывая Execute в синхронном режиме.

Собственно, про API всё. В следующий раз расскажу, как я ошибся с первой реализаций, и почему это была ошибка.

суббота, 30 марта 2013 г.

Codestellation DarkFlow. Укрощение многопоточности.

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

Проблемы

Доставшийся мне код страдал от двух главных проблем:

Поддержка кода. Любой плохо написанный код тяжело поддерживать. Зависимости между объектами, цикломатическая сложность, и т.д. бывает непростой для понимания задачей само по себе. Многопоточность добавляет  новое измерение, усложняя код многократно. Если попытаться систематизировать проблемы с поддержкой, получится примерно такой набор:

  • Неструктурированная синхронизация – возникающие в неожиданных местах примитивы синхронизации, пытающиеся связать множество претендующих на ресурс потоков. Часто это выражается в попытки сделать блокировки более глобальными. Что с чем синхронизируется при этом понять довольно сложно.
  • Асинхронные вызовы через APM/TPL. Модель APM перегружена инфраструктурными примитивами, что сильно усложняет его понимание (кто не верит – пусть попробует написать собственный IAsyncResult, и для окончательного счастья сделать вложенные вызовы APM). Смешение полезного кода с кодом APM делало код практически не читаемым. Использование TPL немного смягчало боль, но в целом не решало проблемы.
  • Сложности с тестированием. Написание тестов на многопоточный бизнес код совсем нетривиальная задача. Кроме сложности в понимании кода добавляются “радости” внезапных ошибок, пришедших из потока, запущенного совсем другим тестом (особый камень в огород таймеров).

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

  • Каждый поток требует 1 Мб памяти для стека. Пока потоков мало – можно не беспокоится. Но когда в пиковых нагрузках количество потоков подскакивает до 1000 – это проблема.
  • Множество заблокированных потоков создают лишнюю нагрузку на планировщик потоков, увеличивают количество требуемых переключений контекста. Всё это создаёт бесполезную нагрузку на процессор.
  • Примитивы синхронизации Windows требуют перехода в режим ядра, даже если при этом не происходит ожидания и блокировки потока. Такие вызовы обходятся очень дорого. Кроме того, создание объектов ядра весьма дорого так же дорого. CLR например использует пулы объектов ядра для работы Monitor’а именно по этой причине.
  • Сборщик мусора останавливает (Suspend) все потоки, и сканирует их стеки в поисках более не используемых объектов. Надо ли говорить, что производительности это не добавляет? 

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

Решение

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

  • Избегать (в крайнем случае  минимизировать) вторжение асинхронного кода в код приложения. Для случая асинхронного выполнения синхронной операции этот пункт мог бы решить даже ThreadPool.QueueUserWorkItem, но при этом возникает проблемы обработки исключений и контроля уровня многопоточности. Для вызовов в стиле APM задача еще более усложняется, и окончательного решения у меня пока нет.
  • Избегать конкурентного доступа к ресурсам. Идею как этого достичь почерпнул из node.js. Чтобы избегать синхронизации – не надо использовать конкурентный доступ к ресурсам. Это не означает использовать только один поток вообще, но один поток одновременно.
  • Не тратить понапрасну машинные ресурсы -  не создавать лишние потоки (ThreadPool будет к месту, но использовать надо с хитростями), минимизировать использование примитивов синхронизации ядра, не блокировать потоки. Такая цель требует изменения подхода к проектированию, но вполне достигаема.

Эти идеи (частично) воплощены в проекте Codestellation DarkFlow. Пока он страдает отсутствием документации, но над этим недостатком я собираюсь в ближайшее время поработать. О его внутренностях и механизмах расскажу в следующей раз.

PS. Прощу прощения, но видимо предыдущая серия блогов про CardFlow останется незаконченной. Как всегда, времени на всё не хватает. Да и с тех пор успел изрядно подзабыть начатое.

четверг, 22 ноября 2012 г.

CardFlow. Приёмочный тест.

Любую разработку стоит начинать с приёмочного теста. Чем приёмочный тест отличается от модульных тестов? В первую очередь приёмочный тест – это тестирование всей функциональности системы с точки зрения конечного пользователя. Скажем, если вы нажали кнопку добавить в корзину – то необходимо проверить содержимое корзины на соответствие добавленному товару. Пользователь – это не обязательно человек. Если речь идёт про RESTful-сервис – то пользователь это другой сервис. Важная особенность – тестируют по принципу чёрного ящика, все действия с сервисом только через внешние интерфейсы или внешнее API. Хотя при этом надо использовать заглушки на внешние системы – лишняя недетерминированность ни к чему. Кроме того, заглушки могут эмулировать особые случаи поведения внешних систем (ошибки, разрывы связи и т.д).

Обычно приёмочные тесты пишутся на языке Gerkin. С этим же языком и работает SpecFlow, с помощью которого я и собираюсь писать тест. Для тех кто не знаком со SpecFlow, рекомендую посмотреть примеры. Кода немного, и можно быстро понять что к чему, даже не читая документацию.

Первый тест лучше писать для  минимальной реализации, постепенно докручивая в нём фишки. В моём случае это простейшее действие – создание новой Kanban-доски с указанными параметрами. У меня получилось так:

Как начать реализовывать систему – расскажу в следующий раз.

PS. Намучившись с форматированием кода решил окончательно переехать на gist’ы от github. Получается гораздо быстрее, и чище исходник блога.

воскресенье, 18 ноября 2012 г.

CardFlow. EventStore.

По жизненным обстоятельствам пришлось временно оставить профессию блоггера, но к счастью у меня оно снова есть. За это время утекло много воды, пришло много интересных идей (о которых вечно не хватает времени написать), я стал счастливым обладателем Windows 8 (честно счастливым, но об этом в другом раз). А еще пришло понимание, что свободного времени много не будет. Всё это приводит меня к неизбежности мысли уменьшить масштаб учебного проекта – оставить только создание и редактирование Kanban-досок.

В прошлый раз я остановился на поиске шины для приложения. Пришло время сделать еще один выбора – Event Store.  Но выбор был простым:

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

Что это значит? Для тестов жизненно необходимы три вещи Arrange, Act, Assert. Все шага должны выполняться быстро. Очень быстро. В вопросе хранения данных это означает возможность запустить хранилище в том же процессе. Совсем идеально хранить данные в памяти. Такой режим есть например в RavenDB. Да даже Microsoft озаботилась тестированием, и сотворила IIS Express для тестирования. В случае Грега Янга мы имеем только отличный сервер, что для быстрых тестов совершенно не годиться.

Второй вариант сразу с главной страницы подкупил наличием такого режима. В остальном тоже неплохо, поддержка разных способов хранения данных, разных логгеров (включая встроенную возможность логгировать в консоль – плюсик для тестов). Конфигурация простая, понятно даже без документации.

Был еще вариант написать некую свою реализацию. Но он был отброшен сразу. Делать хорошую реализацию долго, а плохую не хочется. Да и для промышленной эксплуатации лучше сразу освоить приличное хранилище.

В следующий раз расскажу про самое главное, с чего следует начинать разработку – приёмочный тест.

пятница, 28 сентября 2012 г.

Эффективное тестирование. Design for testability.

 

Перед тем, как вернутся к CardFlow хотелось бы сделать большое отступление на тему тестов.   Тесты – важнейший элемент развития кода. Быть может, даже более важный, чем важный рабочий код системы. Подушка безопасности, которая предохраняет программы от ошибок программистов. Как сохранить тесты полезными и рабочими? Я полагаю, здесь есть три важные ортогональные концепции:

  • Поддерживаемость. (Maintainability).
  • Детерминизм. (Determinism).
  • Скорость. (Speed)

  Изначально, я хотел сделать этот список нумерованным, и отсортировать по убыванию важности. Но не смог расставить приоритеты (поэтому расставил в порядке убывания количества букв). Каждая из этих концепций чрезвычайно важна, и пренебрежение любой из них делает тесты обузой. Будь я Мартином Фаулером, ввёл бы специальный акроним – MDS, а тесты написанные с соблюдением этих концепций – MDS Complaint .

  Как должен выглядеть тест, чтобы называться MDS-Complaint:

  1. 100% детерминирован (любите отлаживать обрыв ethernet-кабеля через в дебаггере? я тоже не люблю).
  2. Не существует другого теста на тестируемую функциональность. (DRY, DRY, DRY).
  3. Выполняется менее чем за 20 миллисекунд (1000 тестов менее чем за 20 секунд – мой идеал.).
  4. Зеленеет после рефакторинга кода, если функционал не был повреждён (скажи прощай mock’ам). 
  5. Содержит не более 10 строк кода в самом тесте, и не более 10 в setup-методе. (не люблю долго вникать в суть теста).

  Достичь MDS-совместимости тестов не так уже легко. К счастью, всё (хорошо, почти всё) уже украдено до нас. Вот мой краткий анализ болевых точек, на которые надо обращать внимание при написании тестов. 

Поддерживаемость.

  Как и код другой системы, тесты не пишутся раз навсегда. Их приходится пересматривать (а может и переписывать) на ежедневной основе. Одни устарели, другие требует дополнительной инициализации после изменений в коде системы, третьи вдруг начали ловить ошибку, которой раньше никто не замечал. И есть несколько факторов, которые могут испортить поддерживаемость тестов:

  1. Организация тестов – в первую очередь, расположение. Если вы за 10 секунд не смогли найти нужный вам тест в проекте – что-то не так. Местоположение тестов должно быть интуитивно понятно, и подчинено неким соглашениям.
  2. Логика в тестах – очень страшное зло. Логика в тестах зачастую дублирует логику тестируемого кода, поэтому ошибки будут повторятся, и тест будет зелёным. Это страшно на самом деле. 
  3. Mocks – многие мне не поверят, но я абсолютно убеждён: в 95% случаев тесты с mock-объектами не тестируют ничего, кроме правильности установки ожиданий. Малейший рефакторинг приводит к необходимости вносить изменения в ожидания, причём зачастую в нескольких местах. Хотя система по-прежнему остаётся рабочей. Это чрезвычайно досадная печалька.
  4. Количество тестов. Как и любой другой код, тесты требуют поддержки, рефакторинга, создание дополнительных классов и т.д. Мысль проста – меньше тестов – меньше затрат на поддержку (и выше скорость выполнения).
  5. Имена тестов – Как и для методов, классов, интерфейсов и т.д. лучший комментарий для теста – его имя. Если вы не не можете написать хорошее имя для теста – скорее всего он тестирует слишком много всего сразу. Хорошенько подумайте, прежде чем его написать.

Детерминизм.

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

  1. Изоляция - Состояние системы, доставшееся после предыдущих тестов должно быть очищено, и новый тест работает с чистого листа. Всегда. Что это означает на практике? Возьмите перед тестом новый экземпляр объекта, полностью очистите базу данных, удалите файл и т.д. Исключение можно сделать лишь для объектов, состояние которых не влияет на тесты ( скажем, прочтённая из конфигурации строка подключения) ..
  2. Многопоточность. Асинхронные/многопоточные тесты – это всегда пляски с бубном в целях детерминизма. В таких тестах всегда приходится заниматься либо периодическим опросом, либо играться с примитивами синхронизации. И тот и другой способ сильно бьют по удобству поддержки и скорости выполнения тестов. Правильный вариант – тесты должны быть однопоточные, хотя иногда их сложно написать в таком ключе.  
  3. Удалённые сервисы. - Как минимум раз в неделю такой тест будет падать из-за проблем с сетью или самим удалённым сервисом. Исходя из своей практики, тесты с удалённым сервисом никогда не бывают более чем 90% детерминированы. Поэтому никогда не используйте удалённый сервис. Только заглушку, реализующую   интерфейса адаптера к удалённому сервису.
  4. Текущее время. – Настоящее реальное время в тестах ни к чему, поэтому под рукой всегда надо иметь хорошую реализацию фиктивных часов. Один из таких примеров я демонстрировал ранее.

Скорость.

  1. Многопоточность. Из-за склонности многопоточных тестов к периодическому опросу (polling), тесты часто содержат нечто вроде Thread.Sleep(1000). Целая секунда потерянного на сон потока! Неуж-то в это время нельзя заняться чем-то более полезным? Можно! Не пишите таких тестов. Чуть менее вредительский вариант – использование примитивов синхронизации, вроде ManualResetEvent, но в целом проблемы одинаковы – мы тупим в ожидании.
  2. Удалённые сервисы. Как и говорил ранее – межпроцессные вызовы выполнятся на 4-6 порядков медленнее внутрипроцессных. А хорошее тестирование содержит десятки-сотни вызовов удалённых сервисов. Если один вызов длиться 100мс, и тесты содержат 90 вызовов – то нам предстоит полторы минуты тупить в монитор.  Непозволительно долго и бессмысленно. Хорошая заглушка спасёт нас.
  3. Файловая система. Операции с файловой системой очень просты и понятны, писать для них заглушку не хочется. Но жесткие диски (даже SSD) способны изрядно притормозить процесс тестирования, особенно если в фоне на жёсткий диск записывается еще и фильм из интернета Улыбка Что делать если фильм мешает тестам? Правильно – нафиг такие тесты жесткие диски. Виртуальный диск спасёт и скорость, и фильм!
  4. Базы данных. База данных обычно тоже является удалённым сервисом, но отличается  весьма высокой детерминированностью. Чего нельзя сказать о скорости. Поэтому при выборе БД и технологии доступа к ней стоит присмотреться в возможности перевода БД во внутрипроцессный режим (SQLite, SQL Server Compact, RavenDB). А еще лучше и хранить данные в памяти, как это умеет SQLite. Кроме чистой скорости мы выиграем еще и возможность гонять тесты параллельно, без риска испортить тестам контекст.

Справделивости ради, я не единственный, кто проводил такой анализ. Фаулер тоже писал о проблемах тестирования. К сожалению, написал поздно – я набил все шишки самостоятельно. Зато еще раз убедился в правильности собственных выводов.

Design For Testablity.

На пути тестирования поджидает множество опасностей. При проектировании системы все их надо брать в расчёт, и изначально продумывать архитектуру приложения со встроенной поддержкой тестирования. Каждый класс, каждый метод, каждая строчка кода должна быть рассмотрена под микроскопом тестирования.

четверг, 27 сентября 2012 г.

CardFlow. Как выбрать шину сервисов.

Сделать второй шаг к коду оказалось гораздо сложнее. Как и написано во всех agile-книжках, я собирался использовать подход test first. А первый тест подразумевает отправку сообщения в шину, и получение какого-то результата. Здесь поджидала самая большая трудность – выбрать шину. Для начала надо было определиться с  требованиями к идеальной шине:

  • Xcopy-развёртывание.
  • Возможность работы во внутрипроцессном режиме.
  • Поддержка контейнеров (забегая вперёд - их поддерживают все шины).
  • Простая конфигурация. Идеально – без конфигурации.
  • Не требующий администрирования транспорт.

А теперь посмотрим, как им соответствуют кандидаты. Выбор (к счастью) не так уж велик, и рассмотреть стоит всего трёх кандидатов:

  • NServiceBus – идеальный вариант для рабочих .net систем. Поддерживает все модные концепции сообщения, команды, события, саги, контейнеры и всё-всё-всё.   Документация в идеальном состоянии – хорошо структурирована, описывает все аспекты, всё по делу. Но есть пара ложек дёгтя. Во-первых, транспорт MSMQ – для рабочих система пойдёт, но настраивать MSDTC в учебных целях совсем не хочется.  Во-вторых – требует лицензии. Конечно можно добыть express-лицензию, но… это ведь дополнительные усилия. А усилия я экономлю. Улыбка
  • MassTransit – тоже близка к идеалу, но… в качестве транспорта может использовать только MSMQ или RabbitMQ. Про MSQM я уже писал, а RabbitMQ требует установки отдельной среды выполнения. Есть и специальный тестовый транспорт – Loopback, но на реале не попробуешь. Документация неплоха, хотя до NServiceBus не дотягивает. Если бы не транспорт – это мой выбор. Может быть свободное время, попробую написать транспорт на Rhino.Queues.
  • Rhino Service Bus – документация откровенно куцая, информации вообще немного, и ту надо собирать по крупицам. Для быстро понимания советую прочитать пару статей Oren Eine, а потом почитать wiki на github’e про изменения в версии 2.0. В качестве транспорта MSMQ и Rhino.Queues. Последняя не требует никаких административных действий, и сразу готова к работе. И это решающий плюс. Ах да, есть один минус в карму – использует противный log4net, но это я могу пережить.

MassTransit отвергнут. В учебных целях не хочется возиться с администрированием. Если бы у меня была настоящая рабочая система – я бы разорился на NServiceBus. А пока снова возьмусь за Rhino.ServiceBus.

четверг, 6 сентября 2012 г.

CardFlow. Первый шаг к коду.

  Каждый .NET разработчик знает правило формирования имен – “Компания.Проект.Сборка”. Чаще всего это название идёт напрямую в имя сборки. Получается нечто подобное:

image

Два проекта – это еще ничего. Но когда их число подходит к 10 искать в этом нагромождении букв нужный проект уже сложно и долго. Инструменты вроде Resharper’а конечно спасают ситуацию быстрыми клавишами навигации. Но всё равно, периодически надо ковырять свойство конкретного проекта приходиться искать его глазами. Это раздражает. К счастью, бесполезное сочетание  “Компания.Проект” можно смело выкинуть на помойку:

image

Однако имена сборок всё равно надо сделать соответствующими шаблону “Компания.Проект.Сборка”. Проще пареной репы, просто пропишите их в свойствах проекта:

image

Есть и еще одна полезная мелочь. По-умолчанию, студия задаёт выходные папки (bin) проектов в подпапке каждого проекта. В результате, в процессе сборки проекта мегабайты сборок могут копироваться туда-сюда, сильно замедляя процесс. Кроме того, напрасно тратиться дисковое пространство. Это звучит глупо по современным меркам HDD, но я использую виртуальный диск, поэтому проблема свободного места для меня по прежнему актуальна. Замените Output path по-умолчанию на “..\bin” в Release и Debug конфигурации проектов, и жизнь станет немного легче:

image

Мелочи, а способны экономить время и нервы (а они в современной жизни на вес золота). Подмигивающая рожица

пятница, 31 августа 2012 г.

CardFlow. Гранит науки.

Недавно я писал, какие веб-фреймворки мне хотелось бы посмотреть. В том числи упомянул необходимость предварительно реализовать серверную часть. Поскольку проект не столько для дела, сколько для тренировок, хотелось определиться с тем, что стоит прокачать. Вот мой предварительный список для прокачки с помощью CardFlow:

  • CQRS/ES, в том числе хочется побаловаться с Event Store. 
  • Полная асинхронность. Интересно с точки зрения более полного (и скромного!) использование машинных ресурсов. Да и ES с таким подходом хорошо дружит.
  • Для связи использовать какую-либо MQ/ESB (разницу между MQ и ESB хорошо расписал Udi Dahan) Скорее всего это будет Rhino ESB. Бесплатна, и готова к работе. Документация конечно хромает, но необходимое по интернету насобирать можно.
  • Проектирование с учётом тестирования. Каждый этап. В идеале – возможность запускать приёмочные тесты как в полностью синхронном внутрипроцессном режиме, так и в полностью рабочей среде.
  • Работа с удалёнными сервисами. Интересно в разрезе написания тестов, и реальной работы. Какой именно удалённый сервис будет пока не придумал, но думаю в пути мысль возникнет.
  • XCOPY-развёртывание, привет TopShelf.
  • BDD в общем, и SpecFlow в частности.
  • Управление зависимостями с помощью Nuget. Не идеален, но лучше, нежели таскать сборки руками.

Пока хватит. В следующий раз приступим к реализации. Гибкие методологии рекомендуют начинать с приёмочного теста. Он и станет первой строчкой кода в проекте.

четверг, 30 августа 2012 г.

CardFlow.

Почитывая книгу Leading Lean Software Development, увидел пример Kanban Board. Идея сразу запала в душу, и спустя пару дней навязала желание сделать учебный проект именно Kanban Board. Писать, что такое Kanban, и причем тут доска не буду. Лучше почитайте книгу выше, или другую специализированную литературу. Но одно стоит заметить: Kanban – переводится как карточка, и методология крутиться вокруг движения карточек. Что логично приводит к подходящему названию – CardFlow.

После самого главного (название), надо определиться с функциональностью проекта. Проект учебный, поэтому требуется как можно проще описать функционал. Главное в этом процессе – сделать поменьше всякого функционала. Итак, пока будет только две Функции:

  1. Редактирование доски: количество, расположение слотов, переходы между ними.
  2. Создание и передвижение карточек по доске.

Для полноценного менеджера задач конечно недостаточно: нет участников, нет авторизации, много чего нет… Но хватит, чтобы побаловаться с CQRS и Event Sourcing Подмигивающая рожица

Я постараюсь описать каждый шаг разработки, и почему он сделан именно так.

воскресенье, 29 июля 2012 г.

Положи Windows Service на полку.

  Кому приходилось писать сервисы windows следуя заветам Microsoft осознаёт тяжесть (“корпоративность”) подхода. Долго, муторно, неудобно для тестирования, для отладки требуется ultimate версия студии. Возникает ощущение бега с препятствиями. Видимо схожие мысли посетили и авторов Mass Transit и Fubu Project. Они уже решили проблему создания сервисов, убрав все препятствия с трассы. Спешите освоить -  Topshelf.  

  Раздел документации на сайте не блещет полнотой, что поначалу несколько расстраивает. Хорошая новость: ее достаточно. Topshelf может работать в двух режимах:

  • installing - создание полноценной службы windows.
  • shelving – хостинг сервисов в отдельном домене приложения.

 

Installing

  Этот режим используется для создания полноценной службы windows, которой можно управлять через Service Manager. Идеологически Topshelf выполняет две операции:

  • Берёт на себя сложности связанные инфраструктурными проблемами хостинга windows-сервиса.
  • Хостинг бизнес-сервиса, выполняющего работу самого сервиса.

Эти две функции нам и надо прописать в коде:

static class Program
{
    static void Main()
    {
        HostFactory.Run(x =>
        {
            //Настройка windows-сервиса. 
            x.UseNLog();
            x.BeforeStartingServices(() => Console.WriteLine("[FooHost] Preparing to start host services"));
            x.AfterStartingServices(() => Console.WriteLine("[FooHost] All services have been started"));
            x.AfterStoppingServices(() => Console.WriteLine("[FooHost] All services have been stopped"));
            x.SetServiceName("FooHost");
            x.SetDisplayName("FooHost");
            x.SetDescription("FooHost ");
            //Сейчас запускаем как Local System, но можно запустить под любым пользователем. 
            x.RunAsLocalSystem();
            x.EnableDashboard();
            //Настройка нашего бизнес-сервиса.  
            x.Service<BarService>(y =>
            {
                y.SetServiceName("FooService");
                y.ConstructUsing(() => new FooService());
                y.WhenStarted(foo => foo.Start());
                y.WhenStopped(foo => foo.Stop());
            });
        });
        Logger.Shutdown();
    }
}

Не буду подробно останавливаться на коде, он говорит сам за себя. Добавлю: Topshelf поддерживает работу с двумя наиболее популярными логгерами – log4net и NLog2. Расширенное описание командной строки есть в исходнихах.


Преимущество режима  по сравнению с обычным сервисом:



  • Нет нужды наследовать и вручную писать ServiceInstaller и ServiceBase, что повышает тестопригодность.
  • Возможность запускать сервис в режиме консоли “из коробки”, что облегчает его отладку.
  • Внятное (даже без документации) API.

Создание нового windows-сервиса легко, но есть лучший способ хостить сервисы:


Shelving


В большинстве случаев нам не нужен полноценный windows-сервис, но мы вынуждены его писать, чтобы удовлетворить требования Windows. Отсюда вытекает логичная мысль: сделать инфраструктурный windows-сервис, который бы мог хостить бизнес-сервисы. Эту идею и реализует Topshelf.


Для начала нам понадобиться Topshelf.Host.exe. Его нет в nuget, но можно найти в исходниках проекта (я использую несколько модифицированную версию). Topshelf.Host.exe является таким же windows-сервисом, как и описанный в предыдущем разделе, и следовательно его можно запускать и как сервис, и как консоль. После запуска сервис автоматически создать подпапку Services. Topshelf мониторит эту папку, и при обнаружении изменений по необходимости подгружает


  Для дальнейшей работы необходимо реализовать Bootstrapper для своего сервиса, и сконфигурировать параметры запуска. У меня это выглядит так:

public class MazayBootstrapper : Bootstrapper<MazayService>
{
    public void InitializeHostedService(IServiceConfigurator<MazayService> cfg)
    {
        cfg.SetServiceName("Mazay");
        cfg.Named("Mazay");
        cfg.HowToBuildService(x => new MazayService(SectionHandler.Instance.Folders));
        cfg.SetServiceName("Mazay");
        cfg.WhenStarted(x => x.Start());
        cfg.WhenStopped(x => x.Stop());
    }
}

  После этого надо создать подпапку с названием сервиса в папке Service\FooService, и скопировать в нее наш сервис. Согласно описанию, сервис должен был заработать. Но заработал  только после продолжительных мытарств. Что еще раз напомнило мне о древней мудрости:  если ничего не помогает – прочитайте наконец документацию. Если и это не помогает – внимательно прочитайте документацию Улыбка Дело оказалось в двух проблемах:




  1. Добавить конфигурационный файл сервиса. Внимание: название файла должно соответствовать шаблону ‘имя_папки_сервиса.dll’, в которую мы положили сервис. Для моего случая это FooService.config, а не FooService.dll.config. Topshelf запускает сервис в отдельном домене приложения, и принудительно назначает ему этот конфигурационный файл.


  2. В конфигурационном файле необходимо указать ссылку на Bootstrapper сервиса:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section
      name="ShelfConfiguration"
      type="Topshelf.Shelving.ShelfConfiguration, TopShelf" />
  </configSections>
 
 <ShelfConfiguration
    Bootstrapper="Mazay.MazayBootstrapper, Mazay" />
</configuration>

После этого сервис заработал в штатном режиме.


Dashboard


Маленький бонус от создателей. Topshelf содержит небольшой проект Dashboard, который позволяет из браузера посмотреть, и управлять рабой сервисов. Для его подключения понадобиться nuget пакет Topshelf.DashBoard, и вписать строчку и вызвать метода расширения x.EnableDashboad() при конфигурации сервиса. Dashboard не поддаётся конфигурации, и найти его можно по адресу http://localhost:8483/TopShelf/Topshelf.Host


image


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

среда, 18 июля 2012 г.

На пути к веб-разработке.

Жажда, жажда…

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

  Долгое время мне приходилось писать WinForms приложения, лопатить тонны кода хранимых процедур и заниматься разными побочными языками вроде x++. Каждый раз наступал момент, когда работа исчерпывала себя, и душа требовала свежего ветра. Сейчас пишу сервисы на C#, а жажда свежего ветра потихоньку начинает брать своё. В .NET мире осталась одна нехоженая мню дорога – Веб. Весьма востребован в наши дни. Это не попытка открыть Америку, а предисловие к тому факту, что долгое время я пытался ее обплыть стороной. Пришло время и мне стать Колумбом.

Возможно, самый удачный вариант – начать изучение протокола HTTP. Но логика прагматика подсказывает мне, что лучше остановится на одном из доступных фреймворков. Сделать выбор непросто. После официальной работы, и домашних забот остаётся не так уж много времени для работы над собой. Тратить нужно осторожно, на всё не хватит. Я вижу несколько интересных игроков на .net web-арене.

Кто следующий?

 ASP.NET MVCx – самый монструозный и самый популярный фреймворк, обязанный своей популярностью маркетингу (и умению копировать) Microsoft. Сотни вакансий  по сравнению с абсолютным  нулём для всех остальных конкурентов вместе взятых. Казалось бы идеальный выбор, но… Во-первых, Microsoft в очередной раз не изобрела ничего нового, а лишь ответственно подошла к копированию и модернизации Castle Monorail (на тот момент возможно лучший фреймворк для веб). Увы, вместе с достоинствами прихватили и недостатки: необходимость наследовать инфраструктурные классы контроллеров, и возвращаемые мутные ActionResult. Во-вторых, тяготы работы с “корпоративным” фреймворком – скачай то, доставь это, настрой сё и т.д. и т.п. Почему я не могу просто написать Install-Package, и насладиться красотой Hello World по asp.net-овски?  Увы, из-за популярности на рынке мне придётся им заняться, хотя совсем-совсем не хочется. Этот как рыбий жир: противно, но полезно.

 FubuMVC – собственно, не только веб-фреймворк, но и несколько сопутствующих проектов. Вкратце, своё отношение могу высказать фразой “я Пастернака не читал, но одобряю”. Фреймворк построен не тех принципах, которые мне очень близки: Convention Over Configuration, POCO и многое-многое другое, что позволяет освободиться от инфраструктурной тягомутины, и писать собственно приложение. Chad Myers хорошо написал, почему я (заочно) люблю FubuMVC. Не только Chad Myers, а вообще весь состав разработчиков внушает мне полное доверие. Будучи давним читателем CodeBetter, полностью разделяю их подходы, и философию программирования. Fubu точно займёт своё место в моём учебном плане.

 OpenRasta – темная лошадка в моём списке. Не подумайте, он неплохо документирован, и уже достаточно давно обживает белый свет. Но есть есть нюансы. Почти весь фреймворк написал Sebastian Lambla. Себ без сомнений круче Чака Норриса, но всё-таки не Брюс Всемогущий. Не успевает наводить марафет, что частенько вводит в расстройство даже опытных бойцов. Кроме того, у Себа свой особый взгляд на версионирование и управление зависимостями. Они не плохи, просто левая резьба: свои преимущества есть, но  пока не готов бросится вплавь против течения.

 NancyFX – наиболее легковесный, с хорошей документацией и растущим сообществом. Не могу сказать много, но беглый взгляд по документации оставил самые радужные впечатления: всё выглядит понятным, даже для не самого опытного веб-разработчика вроде меня. И это весьма, весьма радует. Похоже, именно Nancy возглавит моё движение в веб.

Легко в учении.

Учить документацию смысла нет – нужна практика. Вот целях обучения напишу скромное двух-трёхзвенное приложение, веб-интерфейс для которого попробую реализовать на заслуживающих внимания фреймворках. Бек-енд тоже не обойдётся без освоения нового – модные ныне CQRS и Event Sourcing уже давно тешат воображение. Пора оседлать их в реале. О своих успехах постараюсь рассказать на страницах блога.

понедельник, 7 мая 2012 г.

AssemblyVersion и git: маленькое дополнение.

  Я уже описывал вариант работы с git’ом для генерации файла SolutionVersion.cs. Этот файл добавляется в .gitignore, чтобы не коммитить его в контроль версий. А в проекты VIsual Studion добавлен BeforeBuild шаг генерации заглушки файла перед компиляцией, дабы работать  с проектом без дополнительных телодвижений. Основная идея – контроль версий не должен содержать сгенерированную AssemblyVersion и прочие атрибуты сборки, сохраняя удобство работы с кодом проекта.

  Сегодня у меня возникла еще одна  идея, как можно упростить процесс. Всё аналогично прошлой статье, за исключением пары других действий. Во-первых, сознательно кладём в контроль версий пустой файл SolutionVersion.cs. Во-вторых, в локальном репозитории выполняем команду:

git update-index –assume-unchanged SolutionVersion.cs

  Эта команда заставит git считать файл локально неизменённым, даже если в нём сделаны изменения. Случайно отправить в контроль версий непустой SolutionVersion.cs не получится. Эта команда влияет только на git-репозиторий, в котором выполнена. Если клонируем репозиторий заново – то и команду придётся выполнить еще раз.

  Какой из двух способов выбрать? Первый способ более надёжен, но требует больше манипуляций с файлами проектов, и вызывает большее количество обращений к диску при компиляции. Во втором случае проще начальная настройка, меньше обращений к диску, но есть риск всё-таки положить в контроль версий сгенерированный файл. Впрочем, с git’ом это не такая большая проблема, т.к. мы еще можем поработать над историей. Улыбка

  Лично я с текущего момент предпочитаю второй способ. Время компиляции (а оно вносит задержку перед запуском тестов) для меня важнее, а в случае проблем еще можно всё исправить.

четверг, 3 мая 2012 г.

Рефакторинг “выделить параметр” весёлым путём.

  Большое число параметров в методе – дурной запах в коде. Фаулер уже давно издал на эту тему рекомендацию – если параметров много, то надо выделить их в особый класс-параметр. И совсем недавно мне пришлось видеть код, который вызывал лёгкую ухмылку удивления:

public Person(string firstName, string secondName, string lastName)
{
    _firstName = firstName;
    _secondName = secondName;
    _lastName = lastName;
}

  Выделить параметр не так сложно, он напрашивается сам по себе. Но реализация повеселила:

public PersonName(string firstName, string secondName, string lastName)
{
    _firstName = firstName;
    _secondName = secondName;
    _lastName = lastName;
}
public Person(PersonName personName)
{
    //WTF???
    _firstName = personName.FirstName;
    _secondName = personName.SecondName;
    _lastName = personName.LastName;
}

  О да! Таскать строки по одной так весело и интересно, что лучше делать это дважды! Сначала затолкав их в PersonName и затем скопировав в Person. Бессмысленная и беспощадная трата человеко-часов. Правильная и очевидная реализация:

        public Person(PersonName personName)
        {
            _personName = personName;
        }

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

вторник, 1 мая 2012 г.

NHibernate Future – оптимизация сетевых издержек.

  Куда уходит время.

  Думаю многие знакомы с проблемами распределённых вычислений. Межпроцессные вызовы выполняются на 3-4 порядка медленнее внутрипроцессных. Межмашинные вызовы длятся на 5-6 порядков медленнее внутрипроцессных. Сетевые  издержки накладываются на каждый межмашинный вызов, приводят к бесполезным потерям времени ожидающими потоками. Напрасный расход машинных ресурсов. Эта проблема была изначальным стимулом к созданию DTO-объектов – вместо многих вызовов, запрашивающих по чуть-чуть, запросить сразу все необходимые данные.

  Эта проблема знакома и создателям NHibernate. NH старается как можно реже обращаться к базе данных, откладывая запросы на последний момент,  выполняет их в пакетном режиме по возможности. Впрочем, в некоторых случаях мы всё равно вынуждены брать контроль в свои руки.  Один из таких случаев – управление временем выполнения запроса (моментом, когда запрос необходимо отправить в базу данных). По понятным причинам, создать DTO-объект для работы с базой данных невозможно. Но и не нужно – NHibernate уже предоставляет хорошее API для этих целей.

  Запроси завтра то, что можно запросить сегодня.

  Цель – уменьшить количество вызовов  к базе данных. Для этого в NHibernate есть специальное API – Futures. В каждом API (IQuery, ISQLQuery, ICriteria, IQueryOver) есть два специальных метода Future и FutureValue:

var count = session.CreateQuery("select count(*) from Cat c").FutureValue<long>();
var peter = session.CreateQuery("from Cat c where c.Name = 'Peter'").Future<Cat>();

  Оба метода на самом деле не выполняют запрос, а лишь добавляют его к списку запросов, которые будут выполнены  потом. “Потом” наступает в момент перечисления коллекции (Future) или получения единственное значение (FutureValue). Все запросы будут отправлены в базу данных пачкой за один вызов.


  Несмотря на явные преимущества, в ответ на предложение использовать Future,  я слышал в ответ нечто вроде “А зачем? Не так уж дорого сделать еще один запрос в базу”. Насколько “дёшево” сходить еще раз в базу я и решил проверить численно. На этот раз мне было лень выдумывать “примеры из жизни”, поэтому будет тренироваться на абстрактных кошках, и сравнивать время выполнения двух методов:

private void QueryDirectly(string timeRegion)
{
    using (var session = _factory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        using (StopwatchManager.Start(timeRegion, elapsed => Console.WriteLine("{0} total time: {1}", timeRegion, elapsed.Total)))
        {
            var count = session.CreateQuery("select count(*) from Cat c").UniqueResult<long>();
            var peter = session.CreateQuery("from Cat c where c.Name = 'Peter'").List<Cat>();
            var bazil = session.CreateQuery("from Cat c where c.Name = 'Bazil'").UniqueResult<Cat>();
        }
        tx.Commit();
    }
}
private void QueryUsingFuture(string timeRegion)
{
    using (var session = _factory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        using (StopwatchManager.Start(timeRegion, elapsed => Console.WriteLine("{0} total time: {1}", timeRegion,  elapsed.Total)))
        {
            var count = session.CreateQuery("select count(*) from Cat c").FutureValue<long>();
            var peter = session.CreateQuery("from Cat c where c.Name = 'Peter'").Future<Cat>();
            var bazil = session.CreateQuery("from Cat c where c.Name = 'Bazil'").FutureValue<Cat>();
            var value = count.Value;
        }
        tx.Commit();
    }
}

  Перед запуском на время, каждый метод прогоняется отдельно, чтобы исключить влияние JIT-компилятора. После этого оба метода выполняются в цикле по 1000 раз, и c помощью Stopwatch замерялось время выполнения только самих запросов. Итак, результаты:


image


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


  Стоит отметить, что и в более ранних версиях существовал функционал для группировки запросов – IMultiCriteria и IMultiQuery. Но Future предоставляет более удобный и типобезопасный способ получения результатов.


  Итого, не игнорируйте возможность увеличения быстродействия своего приложения за счёт уменьшения издержек на сеть. С NHibernate Futures сделать это просто, а результат всегда гарантирован.

воскресенье, 29 апреля 2012 г.

Не используйте Fluent Migrator для интеграционных тестов.

  Библиотеки  миграций баз данных вроде Fluent Migrator и Migrator.Net прочно заняли место среди must have инструментов .net-разработчика. Простота использования подкупает настолько, что появляется соблазн использовать миграции и для создания полноценной тестовой базы данных. Но я не рекомендую так делать по нескольким причинам.

Проблемы.

  Как вы знаете, чтобы быть успешными тесты должны быть быстры. Молниеносно быстры. А прогон миграций на базе данных требует время. Трансформировать миграции в скрипты БД, выполнить их за N-обращений к базе данных – всё это включает в себя затраты самой базы данных, и временные пени за обращение к удалённому сервису. Понятно, что с ростом числа миграций ситуация будет становится всё хуже и хуже.

  А что если в процессе миграций мы заполняем базу данных некоторыми значениями по-умолчанию? Скажем, вносим справочник валют, данные организаций и т.д.? Получаем еще одну проблему: заполненная база данных может создавать неподходящий тестам контекст. В этом случае в коде тестов приходится править данные. Это как минимум непродуктивно. Но хуже того, создаёт ненужную связность между тестами и миграциями, что в свою очередь негативно сказывается на детерминированности и простоте поддержки тестов. Использование разных профилей для развёртывания и тестирования облегчит страдания, но полностью проблему не снимет. Создание единого контекста для всех тестов весьма трудоёмко, и требует особого способа написания тестов. Проще говоря единый контекст для всех тестов – это что-то из области вечного двигателя.  И конечно же, наличие данных лишь усугубляет ситуацию с временными потерями.

Решение.

  Нет необходимости прогонять миграции в каждом тесте. Мы тестируем не миграции, а некий другой функционал, связанный с базой данных. Всё, что необходимо для тестов – это наличии актуальной схемы базы данных. Если бы мы были счастливчиками, и работали с ruby on rails – мы могли использовать функционал schema dumping встроенный в библиотеку миграций. Увы, для fluent-migrator’а проблему придётся решать собственноручно: написать некий schema dumper, или кодогенератор на основе модели предметной области. Впрочем, если вы работаете с NHibernate – то можно воспользоваться встроенным классом SchemaExport для создания пустой базы для тестов. Аналогичные возможности есть и у Entity Framework’а и некоторых других средств доступа к данным.

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

  На мой взгляд, такой подход наиболее оптимален.

NHibernate transactions – обязательны при всех операциях с базой данных.

  Соблазн не использовать транзакции всегда велик. Вроде бы и ресурсов они расходуют, и риск взаимоблокировок всегда велик. По этим причинам многие отказываются от транзакций. Но не стоит следовать этой практике. И сейчас я расскажу как минимум о трёх причинах, по которым транзакции следует использовать при любом обращении к базе данных через NHibernate.

Почему надо использовать транзакции.

  1. РСУБД транзакционны по своей природе. Даже если вы не создаёте транзакцию явно, СУБД сделает это сама. При этом  транзакцию создаётся на каждый оператор (statement), и риск взаимоблокировок сохраняется. Создание транзакции не бесплатный процесс, и требует ресурсов. Отсюда вытекает простая логика: меньше транзакций – меньше расходуется ресурсов сервера.

  2. Согласованность данных. Думаю, никому не надо объяснять – это основа основ работы с БД. Без использования транзакций в БД может попасть всякий мусор, влияние которого на поведение системы порой бывает трудно предсказать. Чтобы особенно болезненно если ссылочная целостность проверяется только на уровне приложения (например связи many-to-any в NHibernate). Всегда проще откатить транзакцию, нежели разбираться с битыми данными.

  3. Кеш второго уровня. NHibernate за кадром делает много работы, чтобы поддерживать кэш в актуальном состоянии. Отсутствие транзакций в коде заставляет NHibernate игнорировать кэш второго уровня, и всегда делать запрос в базу. Причина в неизвестности результата DML-операций: при обновлении базы без транзакции в случае ошибки база оказывается в недетерминированном состоянии. Поэтому NHibernate вынужден инвалидировать данные в кэше, и направлять запрос в базу. В этом легко убедиться. Посмотрим код:

[Test]
public void Query_cache_invalidated()
{
    using (var session = factory.OpenSession())
    //using (var tx = session.BeginTransaction())
    {
        session.SaveOrUpdate(new Cat {Name = "Bazil"});
        session.Flush();
        //  tx.Commit();
    }
    using (var session = factory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        var reloadedPeter = session.Get<Cat>(50L);
        tx.Commit();
    }
    Console.WriteLine("HitCount: {0}", factory.Statistics.SecondLevelCacheHitCount);
}   

И вот что мы увидим в логах:


image


  Но стоит только снять комментарии с кода транзакции, и картина сразу меняется в лучшую сторону:


image


  Как видно из логов, в момент фиксации транзакции NHibernate кладёт в кэш изменённые данные, и в следующий раз даже не понадобится делать запрос в БД для загрузки. Поиск по ключу в кэше намного быстрее, чем запрос к БД.


  Полагаю, мне удалось убедить вас в необходимости использования транзакций. А теперь посмотрим на еще одну вещь, о которой стоит знать при использовании NHibernate с транзакциями.


Некоторые последствия.


  При использовании транзакций NHibernate старается обеспечивать согласованность данных. Давай рассмотри такой код:

[Test]
public void Sudden_insert_happened_on_read_operation2()
{
    using(var session = _sessionManager.OpenSession())
    using(var tx = session.BeginTransaction())
    {
        session.SaveOrUpdate(new Cat {Name = "Bazil"});
        tx.Commit();
    }
    using (var session = _sessionManager.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        var peter = new Cat { Name = "Peter" };
        session.Persist(peter);
        var reloadedPeter = session.Get<Cat>(50L);
        reloadedPeter.Name = "Johnny";
        var totalCats = session
            .QueryOver<Cat>()
            .Where(x => x.Name != string.Empty)
            .RowCount();
    }
}

  В одной сессии создали сущность, и сохранили ее в базу, в другой добавили в сессию одну сущность, и изменили одно из свойств другой. Затем выполнили запрос. А теперь посмотрим, что же было в логах NHibernate?


image


    Я не делал ни Session.Flush(), ни tx.Commit(), но Nhibernate выполнил Insert и Update. Причина в том, что NHibernate понимает, что сделанные в сессии изменения могут повлиять на результат запроса. Поэтому предварительно приводит БД в согласованное с сессией состояние. Если убрать транзакцию, то предварительного обновления происходить не будет (всё равно БД будет в недетерминированном состоянии).


  Если еще не используете транзакции для работы с NHibernate – то самое время начать. Подмигивающая рожица