As it is known in the circle of Erlang developers: only Erlang developers know how to "live" correctly and everyone else "live" is wrong . Without attempting to dispute this fact, let us give an example of a Clojure application in the Erlang style using the Otplike library.
For understanding the article, the reader may need to know the basics of Clojure (Are there still people who do not know Clojure? ...) and the basic principles of Erlang / OTP (processes, sending messages, gen_server and supervisor behaviors). In order to deal with everything else, the average Clojure developer has everything you need: code with examples , REPL and a "tambourine".
In fact, there are many answers to the question "Why Clojure". We give our favorite:
№1. Clojure is a very effective language for prototyping. Compared to Java, writing a Clojure application layout is very simple: it is very easy to develop data models and assemble them together.
№2. Clojure is very easy to test the application: REPL + ease of layout is decided here. Whatever the test case in the application, it is sufficient to simply construct the context in which to test the desired case.
The first two points accelerate the development and support of the application (which falls under the specified conditions) by a factor of 2. But we just started listing ...
Number 3. Clojure is fully interoperable with Java / JVM. This means, in particular, that you can use classes in a Clojure application and export a Clojure application as classes (for example, we integrate a clojure library into a java application ). It also means that all the accumulated code of humanity for the JVM is available for the Clojure application. So, the Clojure language is not alternative and does not contradict the development of the JVM, but as an addition to the JVM (a very important addition, I would like to say).
So, we mentioned that through Clojure it is convenient to get to any part of the "heritage of mankind" in the JVM and it is convenient to test. And now, why all the same Clojure ...
№4. Clojure is a language designed to make complex things simple and, in our opinion, they succeeded thanks to the experience and genius of Rich Hickey, who formulated the main ideas of the language (which, in turn, can be read, for example, here: Why study Clojure? )
Well, personally, my favorite reason ....
№5. Clojure programming is fun , i.e. "lively", interesting and stress free. Just download the project, just read the code, just think, just write and just ship unicorns You give the result.
In the last section, we found out that Clojure is a silver bullet.
However, for industrial programming in the era of multi-threaded applications, in addition to a convenient and cool language, a practical methodology is needed for developing these multi-threaded applications themselves.
The standard solution for multithreaded applications on Clojure at the moment is the core.async
library (for example, Prepare multithreading with core.async ). Practical experience however shows that although the library itself is good, for practice, higher-level “building blocks” are needed.
And here we return to the happy Erlang developers and to their "charms". Erlang / OTP has absorbed considerable experience in developing multi-threaded applications, perhaps, like no other language. Having the implementation of the basic ideas of Erlang / OTP using the Otplike library, we get:
supervisor
( supervisor behaviour
): a process that monitors and overloads other processes, depending on the settings;gen_server
type ( gen_server behaviour
): a process implementing microservice;And not to be unfounded, we give ...
We want to create a microservice for the TODO task list, which accepts the following commands:
create-todo [params] -> [:ok todo] | [:error reason]
create-todo [params] -> [:ok todo] | [:error reason]
find-todo-by-id [id] -> [:ok todo] | [:error reason]
find-todo-by-id [id] -> [:ok todo] | [:error reason]
terminate-todo [id] -> [:ok updated_todo] | [:error reason]
terminate-todo [id] -> [:ok updated_todo] | [:error reason]
delete-todo [id] -> [:ok nil] | [:error reason]
delete-todo [id] -> [:ok nil] | [:error reason]
enumerate-active-todos [] -> [:ok todos] | [:error reason]
enumerate-active-todos [] -> [:ok todos] | [:error reason]
Strangely enough, but from this description, the public API for the TODO service follows directly (you can see the complete code sample on GitHub here ):
(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))
Calling a call*
means simply sending a message to the service.
The essence of the processing of messages for the gen_server
service is that all these messages are processed sequentially, passing the value of the state
service from one message processing to another. Even if many processes in parallel send their requests to our service, it will not break the consistency of our state
, since all these requests will be executed sequentially. In practice, this simplifies the development of the service life cycle.
For the development of the Otplike service gen_server
usual OTP callbacks are available for implementation:
state
service: init [args] -> [:ok initial_state]
handle-call [message from state] -> [:reply reply updated_state] | [:noreply updated_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
handler: terminate [reason state] -> nil
Suppose we implemented a TODO service, and from the REPL we launched it in a new process. After that we made changes to the code of this service and now we want to run the modified code in order to test it. How do we do this?
One solution is to kill the process with the old code and run the new code in the new process.
However, these services we can have a lot and they can depend on one another. In addition, the order of launching services can also be important.
Therefore, there is another radical solution that comes from OTP: you need to have a supervisor process that will monitor our services and who will be able to overload them.
In addition, we want to be able to overload the supervisor himself so that we can bring our application from the REPL to a known initial state.
For these requirements, we get the following code for 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)))
The full code can be viewed on GitHub here.
In the app-sup
function, we list the child processes for our main supervisor.
And the rest of the code is the workaround for restarting the supervisor.
Well, finally ...
Head over to the REPL and see how our TODO service and application restart work.
We start the REPL from the console from the project root:
lein repl
We start the application:
erl-like-app.server=> (erl-like-app.server/start) <proc1@1> 18-05-11 14:29:24 INFO - todo server initialized
Create a TODO pair and mark the first TODO as done:
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}]
What remained active 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})]
What is the value of the state
service:
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}}}
Reload the application:
erl-like-app.server=> (erl-like-app.server/restart) true 18-05-11 14:30:28 INFO - ------------------- RESTARTING ------------------- 18-05-11 14:30:28 INFO - todo server stopped 18-05-11 14:30:28 INFO - -------------------------------------------------- 18-05-11 14:30:28 INFO - todo server initialized
What is the state
value of the service now?
erl-like-app.server=> (erl-like-app.todo.todo-server/get-state) {:counter 0, :db {}}
And what is the result:
I can't know if you need this door, but from our experience I can say that behind this door you get a cool code (which means effective and easy).
Good coding!
Source: https://habr.com/ru/post/354984/
All Articles