Хабрахабр

Clojure приложение в стиле Erlang

Не пытаясь оспаривать этот факт, приведем пример Clojure приложения в стиле Erlang, используя библиотеку Otplike. Как известно в кругу Erlang разработчиков: только Erlang разработчики знают как "жить" правильно а все остальные "живут" — неправильно.

Для того, чтобы разобраться со всем остальным у среднестатистического Clojure разработчика есть все что нужно: код с примерами, REPL и "бубен". Для понимая статьи читателю возможно понадобится знание основ Clojure (а разве еще есть люди, которые не знают Clojure?...) и базовые принципы Erlang/OTP (процессы, отправка сообщений, gen_server и supervisor behaviours).

Почему Clojure?

Приведем наши любимые: В действительности есть много ответов на вопрос "почему Clojure".

Clojure — очень эффективный язык для макетирования. №1. По сравнению с Java, написать макет приложения на Clojure очень просто: очень легко разрабатывать модели данных и собирать их вместе.

В Clojure очень просто тестировать приложение: REPL + удобство макетирования тут решают. №2. Каков бы ни был тестовый кейс в приложении, достаточно просто сконструировать контекст, в котором протестировать нужный кейс.

Но мы только начали перечислять… Первые два пункта ускоряют разработку и поддержку приложения (попадающую под заданные условия) раза в 2.

Clojure полностью интероперабелен с Java/JVM. №3. Так же это означает, что весь накопленный код человечества для JVM доступен для Clojure приложения. Это означает, в частности, что можно использовать классы в Clojure приложении и экспортировать Clojure приложение как классы (например, Интегрируем clojure-библиотеку в java-приложение). А значит язык Clojure идет не альтернативно и не вразрез развитию JVM, а как дополнение к JVM (очень важное дополнение, хочется сказать).

Ну а теперь, почему все таки Clojure... Итак, мы упомянули, что через Clojure удобно добраться до любой части "наследия человечества" в JVM и удобно потестировать.

Clojure — язык, которые разрабатывался для того, чтобы сложные вещи сделать простыми и, на наш взгляд, у них получилось, благодаря опыту и гению Рича Хики, который сформулировал основные идеи языка (которые, в свою очередь, можно прочитать, например, тут: Почему стоит изучить Clojure?) №4.

Ну и персонально моя любимая причина....

Программировать на Clojure — это fun, т.е. №5. Просто загружаешь проект, просто читаешь код, просто думаешь, просто пишешь и просто отгружаешь единорогов выдаешь результат. "живо", интересно и без стресса.

Почему Erlang/OTP для Clojure?

В прошлом разделе мы выяснили, что Clojure — это "серебряная пуля".

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

Практический опыт однако показывает, что хотя сама библиотека — хороша, но для практики нужны "строительные блоки" более высокого уровня. Стандартное решение для многопоточных приложений на Clojure в данный момент — это библиотека core.async (например, Готовим многопоточность с core.async).

Erlang/OTP впитал в себя значительный опыт разработки многопоточных приложений, пожалуй, как никакой другой язык. И тут мы возвращаемся к счастливым Erlang разработчикам и к их "прелести". Имея реализацию базовых идей Erlang/OTP с помощью библиотеки Otplike, мы получаем:

  • Erlang-like процессы и обмен сообщениями между процессами;
  • реализацию OTP процесса типа supervisor (supervisor behaviour): процесса, который следит за другими процессами и перегружает их, в зависимости от настроек;
  • реализацию OTP процесса типа gen_server (gen_server behaviour): процесса, реализующего микросервис;
  • запуск процессов по таймеру (scheduling).

И чтобы не быть голословным, приведем...

Минималистичный пример приложения с микросервисом

Сервис для списка задач TODO

Мы хотим сделать микросервис для списка задач TODO, который принимает следующие команды:

  1. создать сущность todo: create-todo [params] -> [:ok todo] | [:error reason]
  2. вернуть todo по ID: find-todo-by-id [id] -> [:ok todo] | [:error reason]
  3. завершить todo: terminate-todo [id] -> [:ok updated_todo] | [:error reason]
  4. удалить todo: delete-todo [id] -> [:ok nil] | [:error reason]
  5. вернуть список активных todo: enumerate-active-todos [] -> [:ok todos] | [:error reason]

Как ни странно, но из этого описания прямо следует публичное API для сервиса TODO (полный пример кода, можно посмотреть на GitHub тут):

(defn create-todo [params] (call* [:create-todo params])) (defn find-todo-by-id [id] (call* [:find-todo-by-id id])) (defn terminate-todo [id] (call* [:terminate-todo id])) (defn delete-todo [id] (call* [:delete-todo id])) (defn enumerate-active-todos [] (call* :enumerate-active-todos))

Вызов call* означает просто отправление сообщения сервису.

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

Для разработки Otplike gen_server сервиса доступны для реализации привычные для OTP сallbacks:

  1. обработчик инициализации state сервиса: init [args] -> [:ok initial_state]
  2. обработчик сообщений, которые должны вернуть результат клиентскому процессу (синхронных сообщений): handle-call [message from state] -> [:reply reply updated_state] | [:noreply updated_state]
  3. обработчик сообщений без результата клиентскому процессу (асинхронных сообщений): handle-cast [message state] -> [:noreply updated_state]
  4. обработчик системных сообщений: handle-info [message state] -> [:noreply updated_state]
  5. обработчик завершения сервиса: terminate [reason state] -> nil

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

После чего мы внесли правки в код этого сервиса и теперь мы хотим запустить модифицированный код, чтобы его потестировать. Допустим мы реализовали TODO сервис, и из REPL запустили его в новом процессе. Как нам это сделать?

Кроме того, порядок запуска сервисов тоже может быть важен. Одно из решений — это убить процесс со старым кодом и запустить новый код в новом процессе.
Однако этих сервисов у нас может быть много и они могут зависеть один от другого.

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

Для этих требований получился следующий код для Otplike:

;;;;;;;;;;;;;;;;;;;;;;;;; supervision-tree (defn- app-sup [_config] [:ok [ [{:id :todo-server :start [todo-server/start-link [{}]]}]]]) ;;;;;;;;;;;;;;;;;;;;;;;;; boot-proc (defn- start-app-sup-link [config] (supervisor/start-link :app-sup app-sup [config])) (defn- start-boot-sup-link [config] (supervisor/start-link :boot-sup (fn [cfg] [:ok [{:strategy :one-for-all} [{:id :app-sup :start [start-app-sup-link [cfg]]}]]]) [config])) (defn start [] (if-let [pid (process/whereis :boot-proc)] (log/info "already started" pid) (let [config (config/get-config)] (process/spawn-opt (process/proc-fn [] (match (start-boot-sup-link config) [:ok pid] (loop [] (process/receive! :restart (do (log/info "------------------- RESTARTING -------------------") (supervisor/terminate-child pid :app-sup) (log/info "--------------------------------------------------") (supervisor/restart-child pid :app-sup) (recur)) :stop (process/exit :normal))) [:error reason] (log/error "cannot start root supervisor: " {:reason reason}))) {:register :boot-proc})))) (defn stop [] (if-let [pid (process/whereis :boot-proc)] (process/! pid :stop) (log/info "already stopped"))) (defn restart [] (if-let [pid (process/whereis :boot-proc)] (process/! pid :restart) (start)))

Полный код можно посмотреть на GitHub тут

В функции app-sup мы перечисляем дочерние процессы для нашего главного супервайзера.
А остальной код — это workaround для рестарта супервайзера.

Ну и наконец...

Тестирование

Зайдем в REPL и посмотрим как работает наш TODO сервис и рестарт приложения.

Стартуем REPL из консоли из корня проекта:

lein repl

Стартуем приложение:

erl-like-app.server=> (erl-like-app.server/start)
<proc1@1>
erl-like-app.server=> 18-05-11 14:29:24 andrey-pc INFO [erl-like-app.todo.todo-server:44] - todo server initialized

Создадим пару TODO и отметим первое TODO как сделанное:

erl-like-app.server=> (erl-like-app.todo.todo-server/create-todo {:title "task #1", :description "create task #2"})
[:ok {:title "task #1", :description "create task #2", :id "1", :created 1526049427586, :updated 1526049427586, :status :active}] erl-like-app.server=> (erl-like-app.todo.todo-server/create-todo {:title "task #2"})
[:ok {:title "task #2", :id "2", :created 1526049434985, :updated 1526049434985, :status :active}] erl-like-app.server=> (erl-like-app.todo.todo-server/terminate-todo "1")
[:ok {:title "task #1", :description "create task #2", :id "1", :created 1526049427586, :updated 1526049443912, :status :terminated}]

Какие остались активные TODO:

erl-like-app.server=> (erl-like-app.todo.todo-server/enumerate-active-todos)
[:ok ({:title "task #2", :id "2", :created 1526049434985, :updated 1526049434985, :status :active})]

Какое значение state сервиса:

erl-like-app.server=> (erl-like-app.todo.todo-server/get-state)
{:counter 2, :db {"1" {:title "task #1", :description "create task #2", :id "1", :created 1526049427586, :updated 1526049443912, :status :terminated}, "2" {:title "task #2", :id "2", :created 1526049434985, :updated 1526049434985, :status :active}}}

Перегрузим приложение:

erl-like-app.server=> (erl-like-app.server/restart)
true
18-05-11 14:30:28 andrey-pc INFO [erl-like-app.server:59] - ------------------- RESTARTING -------------------
erl-like-app.server=> 18-05-11 14:30:28 andrey-pc INFO [erl-like-app.todo.todo-server:144] - todo server stopped
18-05-11 14:30:28 andrey-pc INFO [erl-like-app.server:61] - --------------------------------------------------
18-05-11 14:30:28 andrey-pc INFO [erl-like-app.todo.todo-server:44] - todo server initialized

Какое сейчас значение state сервиса:

erl-like-app.server=> (erl-like-app.todo.todo-server/get-state)
{:counter 0, :db {}}

Итог

А что в итоге:

  • есть библиотека Otplike, которая открывает в Clojure "дверку" в Erlang, и
  • есть пример приложения, которое открывает "дверку" в Otplike.

Не могу знать, надо ли вам в эту "дверку", но из нашего опыта могу сказать, что за этой "дверкой" получается прикольный код (а значит и эффективный, и простой).

Удачного кодинга!

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»