- PVSM.RU - https://www.pvsm.ru -
Как известно в кругу Erlang разработчиков: только Erlang разработчики знают как "жить" правильно а все остальные "живут" — неправильно. Не пытаясь оспаривать этот факт, приведем пример Clojure приложения в стиле Erlang, используя библиотеку Otplike [1].
Для понимая статьи читателю возможно понадобится знание основ Clojure (а разве еще есть люди, которые не знают Clojure?...) и базовые принципы Erlang/OTP (процессы, отправка сообщений, gen_server [2] и supervisor [3] behaviours). Для того, чтобы разобраться со всем остальным у среднестатистического Clojure разработчика есть все что нужно: код с примерами [4], REPL и "бубен".
В действительности есть много ответов на вопрос "почему Clojure". Приведем наши любимые:
№1. Clojure — очень эффективный язык для макетирования. По сравнению с Java, написать макет приложения на Clojure очень просто: очень легко разрабатывать модели данных и собирать их вместе.
№2. В Clojure очень просто тестировать приложение: REPL + удобство макетирования тут решают. Каков бы ни был тестовый кейс в приложении, достаточно просто сконструировать контекст, в котором протестировать нужный кейс.
Первые два пункта ускоряют разработку и поддержку приложения (попадающую под заданные условия) раза в 2. Но мы только начали перечислять…
№3. Clojure полностью интероперабелен с Java/JVM. Это означает, в частности, что можно использовать классы в Clojure приложении и экспортировать Clojure приложение как классы (например, Интегрируем clojure-библиотеку в java-приложение [5]). Так же это означает, что весь накопленный код человечества для JVM доступен для Clojure приложения. А значит язык Clojure идет не альтернативно и не вразрез развитию JVM, а как дополнение к JVM (очень важное дополнение, хочется сказать).
Итак, мы упомянули, что через Clojure удобно добраться до любой части "наследия человечества" в JVM и удобно потестировать. Ну а теперь, почему все таки Clojure...
№4. Clojure — язык, которые разрабатывался для того, чтобы сложные вещи сделать простыми и, на наш взгляд, у них получилось, благодаря опыту и гению Рича Хики, который сформулировал основные идеи языка (которые, в свою очередь, можно прочитать, например, тут: Почему стоит изучить Clojure? [6])
Ну и персонально моя любимая причина....
№5. Программировать на Clojure — это fun, т.е. "живо", интересно и без стресса. Просто загружаешь проект, просто читаешь код, просто думаешь, просто пишешь и просто отгружаешь единорогов выдаешь результат.
В прошлом разделе мы выяснили, что Clojure — это "серебряная пуля".
Однако для промышленного программирования в эпоху многопоточных приложений, помимо удобного и прикольного языка, нужна практическая методология для разработки этих самих многопоточных приложений.
Стандартное решение для многопоточных приложений на Clojure в данный момент — это библиотека core.async
(например, Готовим многопоточность с core.async [7]). Практический опыт однако показывает, что хотя сама библиотека — хороша, но для практики нужны "строительные блоки" более высокого уровня.
И тут мы возвращаемся к счастливым Erlang разработчикам и к их "прелести". Erlang/OTP впитал в себя значительный опыт разработки многопоточных приложений, пожалуй, как никакой другой язык. Имея реализацию базовых идей Erlang/OTP с помощью библиотеки Otplike [1], мы получаем:
supervisor
(supervisor behaviour
): процесса, который следит за другими процессами и перегружает их, в зависимости от настроек;gen_server
(gen_server behaviour
): процесса, реализующего микросервис;И чтобы не быть голословным, приведем...
Мы хотим сделать микросервис для списка задач TODO, который принимает следующие команды:
create-todo [params] -> [:ok todo] | [:error reason]
find-todo-by-id [id] -> [:ok todo] | [:error reason]
terminate-todo [id] -> [:ok updated_todo] | [:error reason]
delete-todo [id] -> [:ok nil] | [:error reason]
enumerate-active-todos [] -> [:ok todos] | [:error reason]
Как ни странно, но из этого описания прямо следует публичное API для сервиса TODO (полный пример кода, можно посмотреть на GitHub тут [8]):
(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*
означает просто отправление сообщения сервису.
Суть обработки сообщений для gen_server
сервиса состоит в том, что все эти сообщения обрабатываются последовательно, прокидывая значение состояния (state
) сервиса от одной обработки сообщения до другой. Даже если множество процессов параллельно отправят свои запросы в наш сервис, это не сломает нам consistency нашего state
, поскольку все эти запросы будут выполняться последовательно. На практике это упрощает разработку жизненного цикла сервиса.
Для разработки Otplike gen_server
сервиса доступны для реализации привычные для OTP сallbacks:
state
сервиса: init [args] -> [:ok initial_state]
handle-call [message from state] -> [:reply reply updated_state] | [:noreply updated_state]
handle-cast [message state] -> [:noreply updated_state]
handle-info [message state] -> [:noreply updated_state]
terminate [reason state] -> nil
Допустим мы реализовали TODO сервис, и из REPL запустили его в новом процессе. После чего мы внесли правки в код этого сервиса и теперь мы хотим запустить модифицированный код, чтобы его потестировать. Как нам это сделать?
Одно из решений — это убить процесс со старым кодом и запустить новый код в новом процессе.
Однако этих сервисов у нас может быть много и они могут зависеть один от другого. Кроме того, порядок запуска сервисов тоже может быть важен.
Поэтому есть другое радикальное решение родом из OTP: нужно завести процесс-супервайзер, который будет следить за нашими сервисами и который будет уметь перегружать их.
Кроме того, мы хотим уметь перегружать и самого супервайзера, чтобы мы могли из REPL приводить наше приложение к ясному начальному состоянию.
Для этих требований получился следующий код для Otplike:
;;;;;;;;;;;;;;;;;;;;;;;;; supervision-tree
(defn- app-sup [_config]
[:ok
[{:strategy :one-for-one}
[{: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 тут [9]
В функции app-sup
мы перечисляем дочерние процессы для нашего главного супервайзера.
А остальной код — это workaround для рестарта супервайзера.
Ну и наконец...
Зайдем в REPL и посмотрим как работает наш TODO сервис и рестарт приложения.
Стартуем REPL из консоли из корня проекта [4]:
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 {}}
А что в итоге:
Не могу знать, надо ли вам в эту "дверку", но из нашего опыта могу сказать, что за этой "дверкой" получается прикольный код (а значит и эффективный, и простой).
Удачного кодинга!
Автор: ady1981
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/clojure/279980
Ссылки в тексте:
[1] Otplike: https://github.com/suprematic/otplike
[2] gen_server: http://erlang.org/doc/design_principles/gen_server_concepts.html
[3] supervisor: http://erlang.org/doc/design_principles/sup_princ.html
[4] код с примерами: https://github.com/ady1981/erl-like-app.clj
[5] Интегрируем clojure-библиотеку в java-приложение: https://habr.com/post/171361/
[6] Почему стоит изучить Clojure?: https://habr.com/post/173071/
[7] Готовим многопоточность с core.async: https://habr.com/post/312748/
[8] тут: https://github.com/ady1981/erl-like-app.clj/blob/master/src/erl_like_app/todo/todo_server.clj
[9] тут: https://github.com/ady1981/erl-like-app.clj/blob/master/src/erl_like_app/server.clj
[10] Источник: https://habr.com/post/354984/?utm_source=habrahabr&utm_medium=rss&utm_campaign=354984
Нажмите здесь для печати.