воскресенье, апреля 19, 2015

Orleans Silo Host + TopShelf windows service

TopShelf - хорошо сделанная библиотека для создания windows сервисов. Чтобы с ней сделать windows service достаточно просто добавить пакет Topshelf из NuGet и написать несколько строчек в Main(). Получается сервис, который можно запускать как консольное приложение, а можно установить в службы, запустив с командой install.

Orleans - интересная реализация системы акторов для .NET. Некоторых вещей в orleans пока сильно не хватает, например возможности управлять запущенной нодой (Silo). То есть, чтобы обновить код работающего под управлением Orleans приложения, мне придётся сначала грохнуть процесс, потом переписать файлы и запустить процесс заново.

Решил совместить оба варианта, чтобы можно было управлять запущенной нодой как windows-сервисом и останавливать-запускать из командной строки. Заодно появляется возможность разворачивать сервис с помощью, например, удобного Octopus Deploy.

Черновик на github.

Чтобы не дублировать код, берём из sdk orleans готовый OrleansHost.exe и добавляем как reference в наш проект. Для того, чтобы остановить запуск сервиса при ошибках запуска Orleans, используем предоставленный TopShelf экземпляр HostControl, и вызываем .Stop(), после того как отработает OrleansHost.Run().

пятница, марта 20, 2015

Разворачиваем приложение на Erlang (cowboy) с помощью Docker.

note: делалось это не для production кода, а с целью самообучения. Некоторые решения могут быть странными. Это получилось от того, что с докером я познакомился буквально на днях. Если кому что-то будет резать глаз - не поленитесь в комментариях написать про ошибку и как сделать правильно, спасибо. Также предполагается, что вы знаете зачем нужен docker.

1. Структура проекта.

Под каждый контейнер выделяю отдельную папку. В корне проекта положим docker-compose.yml (настройки бывшего fig, нынешнего docker-compose).

Получаем такое дерево, в каждом каталоге лежит свой Dockerfile:

  • back - наше erlang приложение
  • front - статические файлы и javascript, в моём случае это React-приложение, которое собирается с помощью webpack.
  • nginx - конфиги для nginx, который будет работать как reverse proxy.
  • pg - скрипты для создания базы данных в postgresql
  • redis - контейнер с redis.
  • sources - контейнер с исходниками.

2. Первый контейнер, sources.

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

(Здесь и далее gist с docker-compose.yml это кусочек общего большого файла.)

3. Второй контейнер, pg.

От postgres мне пока требуется всего две вещи: работать с заданным мною паролём и хранить данные в указанной папке. Для этого возьмём docker image из стандартного репозитория. В нём оба вопроса решаются переменными окружения.

4. Третий контейнер, redis.

С редисом всё просто до неприличия. Мне даже неинтересно, где он данные разместит и что с ними будет после перезапуска. (Что и как настроить в противном случае, описано здесь)

5. Четвёртый контейнер, backend.

Тут у меня будет строиться и стартовать erlang-приложение. Исходники приложения будут лежать в /app/backend. Приложение представляет из себя rest и вебсокет хэндлеры на cowboy. Если мне захочется чтобы исходники перегружались на лету в процессе разработки, я подключу в зависимостях пакет sync или live.

6. Пятый контейнер, frontend.

С фронтом всё совсем просто: запускаем webpack в режиме watch, чтобы построив bundle он висел дальше, слушал изменения и достраивал по необходимости.

7. Шестой контейнер, nginx.

Теперь нам нужно поставить на входе nginx, который примет соединение и отправит его в зависимости от типа либо к бэкенду, либо на статический файл. Вопрос лишь в том, как nginx'у узнать адреса серверов. Проблема в том, что переменные окружения в nginx можно использовать только в главном конфиге, но нельзя в модулях, например upstream и location. Поэтому сделаем так, чтобы на запуске контейнера с nginx на лету формировался файл с конфигом:

8. Всё готово.

docker-compose build

docker-compose up

При необходимости прицепиться к запущенным контейнерам и посмотреть логи, пишем docker-compose logs. Если требуется что-то выполнить внутри запущенного контейнера, пишем docker ps, смотрим название контейнера в правом столбце "NAMES", например myapp_nginx_1. И выполняем: docker exec myapp_nginx_1 ls -lR /etc/nginx/


среда, февраля 26, 2014

relx + erlang.mk, релизы в Erlang для самых маленьких, шпаргалка.

Когда я начинал знакомство с эрлангом - с релизами у меня сразу не сложилось.
Раз конфиг, два конфиг, прорва нюансов, не самая понятная документация.
Решил что мне и без них живётся неплохо.

А тут вдруг попалось мне чудесное сочетание erlang.mk авторства Loïc Hoguin и relx от Erlware.

И с ними вдруг оказалось что создать релиз - дело пяти минут.
Конспект-шпаргалка с самого нуля:

1. Создаём папку
mkdir tst
cd tst

2. Скачиваем erlang.mk

wget https://raw.github.com/extend/erlang.mk/master/erlang.mk

3. Создаём каркас приложения

make -f erlang.mk bootstrap
make -f erlang.mk bootstrap-rel
(подробности см. в readme репозитория erlang.mk)

4. Смотрим в файл Makefile

Он содержит в себе необходимый минимум:

PROJECT=tst
include erlang.mk

5. Проверяем:
make

 ERLC   tst_sup.erl tst_app.erl
 APP    tst.app.src
.....

Отлично, всё компилируется.

6. Добавим в Makefile каких-нибудь зависимостей, просто чтоб показать как это делается:

PROJECT = tst
DEPS=mongodb gproc

dep_mongodb = git https://github.com/comtihon/mongodb-erlang.git HEAD
include erlang.mk

обращаем внимание: про часть библиотек erlang.mk "знает". Например, gproc. Подробности опять же в readme. А те, про которые он не разумеет - необходимо добавлять руками, пример одного из способов - выше.

7. проверяем:

make

Видим как все зависимости скачались и скомпилировались вместе с нашим приложением

8. по отдельности:

make clean
make deps
make app

Всё работает!

9. [много вырезано] Все шаги которые тут были ранее, по скачиванию и настройке relx, ныне делает сам erlang.mk

10. Добавляем приложения, от которых мы зависим в список applications файла src/tst.app.src. Тогда они будут запущены при старте автоматически.Получаем в итоге:
{application, tst,
 [
  {description, ""},
  {vsn, "0.0.1"},
  {registered, []},
  {applications, [
                  kernel,
                  stdlib,
                  gproc,
                 ]},
  {mod, { tst_app, []}},
  {modules, [ tst_app, tst_sup ]},
  {env, []}
 ]}.

11. А вот и всё. Если мы запускали make -f erlang.mk bootstrap-rel, то релиз у нас уже собирается автоматически и результат лежит в папке _rel

12. Проверяем созданный релиз:

cd _rel/tst_release
ls

bin  erts-6.0  lib  releases

Запускаем:

bin/tst_release console - запустить вместе с erlang shell, наглядно

bin/tst_release start - запустить демоном.

bin/tst_release stop
bin/tst_release restart
значение достаточно очевидно :)

bin/tst_release attach - запустить шелл и прицепиться к запущенному в фоне процессу.

остальные параметры можно посмотреть просто запустив bin/tst_release (а ещё лучше - просто прочитав исходник)

Вот собственно и всё, вот он - готовый рабочий релиз :)
Логи лежат в папке log, можно их поглядеть например командой
tail -f log/erlang.log.1

13. Ещё чуточку добавлю.
Довольно важная опция в relx.config - можно добавить {include_erts, "/path/to/alternate/erlang"} или {include_erts, false}.
Тогда runtime system не будет добавлена в релиз, а это значит две вещи:
а) на машине куда будет поставлен релиз необходимо наличие установленного эрланга
б) нам будет всё равно какая архитектура на целевой машине
Если включать erts, то целевая машина должна быть той же архитектуры как и та на которой собран релиз.

Кажется, всё?

(проверено и приведено к актуальному виду 14 декабря 2014 года)

вторник, февраля 25, 2014

dialyzer + erlang.mk, статический анализ одной командой

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

Так вот давеча решил: пора навести порядок в коде.
И сделал ещё один подход к dialyzer`у.

Как-то у нас не сложилось, когда я только знакомился с эрлангом.
Но в этот раз использую erlang.mk и процесс упростился донельзя.
Шаг первый, собираем информацию из системных модулей:

dialyzer --build_plt --apps kernel stdlib crypto mnesia sasl common_test eunit
(создаст ~/.dialyzer_plt)
Шаг второй, строим plt для своего проекта:

make build-plt
(создаст .имяпроекта.plt)

Шаг третий - всё, можно приступать:

make dialyze

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

Из-за используемого lager`а мешали записи вида:
Call to missing or unexported function lager:warning/3

Связано это с тем, что в исходниках lager`а упомянутых методов нету - они создаются во время компиляции.
Поэтому я немного поправил erlang.mk, заменив

dialyze:
 @dialyzer --src src --plt .$(PROJECT).plt --no_native $(DIALYZER_OPTS) 

на

dialyze:
 @dialyzer --src src --plt .$(PROJECT).plt --no_native $(DIALYZER_OPTS) | fgrep -v -f ./dialyzer.ignore-warnings

Создаём файл dialyzer.ignore-warnings:

Call to missing or unexported function lager:warning/3
Call to missing or unexported function lager:warning/1
Call to missing or unexported function lager:warning/2
Call to missing or unexported function lager:info/1
Call to missing or unexported function lager:info/2
Call to missing or unexported function lager:notice/2
Call to missing or unexported function lager:notice/1
Call to missing or unexported function lager:error/1
Call to missing or unexported function lager:error/2

Не забываем, что каждая строка этого файла будет сравниваться со строкой вывода dialyzer`а.
Смотрим чтобы не было лишних пробелов в конце строк.

Дальше самый долгий процесс - добавление информации о типах в проект.
Лучше не откладывать "на потом".
В процессе описания типов найдётся о чём поразмыслить.

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

В некоторых сторонних приложениях уже есть описания используемых типов - некоторые могут пригодиться.

Например в .hrl файле:
-include("deps/sqlite3/include/sqlite3.hrl").
(sqlite3 хороший пример приложения в котором всё тщательно описано)

суббота, марта 02, 2013

Rest handler в web-сервере Cowboy, нюансы.

Этот пост - краткий итог моих ковыряний в недрах вебсервера cowboy 0.8, куцых примерах и обрывках документации.
Написано чайником, как конспект самому себе.

Обозначенные жирным шрифтом названия функций - вызываются ковбоем, если он находит их в export`ах модуля. (и естественно, должны быть экспортированы)
Обозначенные курсивом - называем как хотим, т.к. их названия отдаём ковбою мы.

  1. Как обозначить, что наш хэндлер - именно rest handler
    Определяем функцию init/3:

    init(_Transport, _Req, _Opts) -> {upgrade, protocol, cowboy_rest}.


  2. Как получить что-то из middleware в State обработчика  (сессию например):
    Определяем метод rest_init/2.

    rest_init( Req, State ) -> { ok, Req, State }.

  3. Как вернуть что-то на GET запрос:
    1. Cowboy будет проверять поддерживается ли http method запроса нашим хэндлером.
      Чтобы обозначить список поддерживаемых методов, определяем allowed_methods/2:

          allowed_methods( Req, State )->
              {[<<"GET">>, <<"PUT">>, <<"DELETE">>], Req, State}.

    2. определяем тип возвращаемых данных:

      content_types_provided(Req, State) ->
          {[{<<"application/json">>, наш_json_ответ}], Req, State}.
    3. определяем обозначенный обработчик наш_json_ответ/2:
          наш_json_ответ(Req, State) ->
              ... ...
              Json = mochijson2:encode({struct, ResponseObject}),
             {Json, Req, State}.

  4. Как принять PUT запрос.

    1. Cowboy возьмет content-type реквеста и проверит, поддерживается ли он нашим обработчиком.
      Для этого определяем следующую функцию:

      content_types_accepted(Req, State) ->
          {[{{<<"application">>, <<"json">>,
              [{<<"charset">>, <<"UTF-8">>}]}, некий_обработчик}], Req, State }.
      (можно использовать '*')

    2. реализуем обозначенный выше некий_обработчик:
         
      некий_обработчик(Req, State) ->
          Достаем json из тела запроса
          {ok, Body, Req1} = cowboy_req:body(Req),
          Декодируем json
          {struct, Object} = mochijson2:decode(Body),
          делаем что нам нужно,
          ....
          Json = формируем json ответа,
          Req3 = cowboy_req:set_resp_body( НашJsonОтвет, Req1 ),
          { true, Req3, State2 }.

  5. Как принять DELETE request.

    необходимо определить функции delete_resource/2, delete_completed/2
        сначала будет вызван первый, после второй.
       
        Из delete_resource/2 возвращаем {true, Req, State} (true = удаляем, если false будет возвращен 500)

        В delete_completed/2 при необходимости обозначаем, что хотим отдать клиенту, например
        Req2 = cowboy_req:set_resp_body( <<"{}">>, Req ),
        Возвращаем {true, Req2, State}.