Страницы

воскресенье, 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 сделать это просто, а результат всегда гарантирован.