{"id":5886,"date":"2016-11-24T16:35:26","date_gmt":"2016-11-24T18:35:26","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=5886"},"modified":"2016-11-24T18:39:21","modified_gmt":"2016-11-24T20:39:21","slug":"replacing-genevent-by-a-supervisor-genserver","status":"publish","type":"post","link":"https:\/\/blog.plataformatec.com.br\/2016\/11\/replacing-genevent-by-a-supervisor-genserver\/","title":{"rendered":"Replacing GenEvent by a Supervisor + GenServer"},"content":{"rendered":"
The downsides of GenEvent have been extensively<\/a> documented<\/a>. For those reasons, the Elixir team has a long term plan of deprecating GenEvent. Meanwhile, we are introducing tools, such as Registry<\/a> (upcoming on Elixir v1.4) and GenStage<\/a>, which better address domains developers would consider using GenEvent for.<\/p>\n However, there is a very minimal replacement for GenEvent which can be achieved today in Elixir that uses a Supervisor and multiple GenServers. We have recently used this technique on ExUnit<\/a>, Elixir’s built-in test framework, as we prepare for an eventual deprecation of GenEvent.<\/p>\n Let’s explore this solution.<\/p>\n ExUnit ships with an event manager that emits notifications any time a test cases and test suite start and finish. For example, if you implement a custom ExUnit formatter, which controls how ExUnit prints output as your test suite runs, you do so by implementing a GenEvent handler and adding it to the event manager.<\/p>\n The implementation of the event manager with GenEvent is quite straight-forward:<\/p>\n The semantics in this case are didacted by GenEvent:<\/p>\n Events are dispatched asynchronously, with the Multiple handlers are processed serially, ExUnit’s event manager is a very simple, low-profile, use case of a GenEvent. In any case, we decided it would be better to move ExUnit away from GenEvent to promote good patterns.<\/p>\n Given the semantics above, we have decided to replace GenEvent by a simple one for one Supervisor, where each handler is a separate GenServer added as a child of the supervisor, and each event is dispatched asynchronously to each handler using The changes to the codebase are minimal. The semantics now are:<\/p>\n Events are dispatched asynchronously, with the Multiple handlers are now processed concurrently<\/p>\n<\/li>\n<\/ol>\n On the handler side, the changes are also minimal. When using GenEvent, a handler had to implement a callback such as:<\/p>\n Now with a GenServer:<\/p>\n Overall, using GenServers is a plus since it is more likely developers are acquainted with its APIs and callbacks. Furthermore, we also gained concurrency between handlers.<\/p>\n The replacement above is straight-forward because the original code was a simple and low-profile usage of GenEvent. For example, both old and new implementation can afford to use asynchronous communication with handlers because we can reasonably assume most time is spent on the test suite and not on the handlers themselves.<\/p>\n In other words, both old and new implementations above do not provide back-pressure<\/strong>. So if you expect any of your handlers to perform tons of work, they will have an ever growing queue of messages to process. If desired, you can provide back-pressure by replacing Another decision we took is to use In any case, there you go! A short example of how to replace a GenEvent by a Supervisor and GenServer and the design decisions we took along the way.<\/p>\n The downsides of GenEvent have been extensively documented. For those reasons, the Elixir team has a long term plan of deprecating GenEvent. Meanwhile, we are introducing tools, such as Registry (upcoming on Elixir v1.4) and GenStage, which better address domains developers would consider using GenEvent for. However, there is a very minimal replacement for GenEvent … \u00bb<\/a><\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[1],"tags":[143],"aioseo_notices":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5886"}],"collection":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/comments?post=5886"}],"version-history":[{"count":9,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5886\/revisions"}],"predecessor-version":[{"id":5899,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5886\/revisions\/5899"}],"wp:attachment":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/media?parent=5886"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/categories?post=5886"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/tags?post=5886"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}The old event manager<\/h2>\n
defmodule ExUnit.EventManager do\n def start_link() do\n GenEvent.start_link()\n end\n\n def stop(pid) do\n GenEvent.stop(pid)\n end\n\n def add_handler(pid, handler, opts) do\n GenEvent.add_handler(pid, handler, opts)\n end\n\n def suite_started(pid, opts) do\n notify(pid, {:suite_started, opts})\n end\n\n def suite_finished(pid, run_us, load_us) do\n notify(pid, {:suite_finished, run_us, load_us})\n end\n\n def case_started(pid, test_case) do\n notify(pid, {:case_started, test_case})\n end\n\n def case_finished(pid, test_case) do\n notify(pid, {:case_finished, test_case})\n end\n\n def test_started(pid, test) do\n notify(pid, {:test_started, test})\n end\n\n def test_finished(pid, test) do\n notify(pid, {:test_finished, test})\n end\n\n defp notify(pid, msg) do\n GenEvent.notify(pid, msg)\n end\nend\n<\/code><\/pre>\n
\n
GenEvent.notify\/2<\/code> function<\/p>\n<\/li>\n
GenEvent<\/code> is unable to exploit concurrency out of the box<\/p>\n<\/li>\n<\/ol>\n
The new event manager<\/h2>\n
GenServer.cast\/2<\/code>. Let’s see the new code.<\/p>\n
defmodule ExUnit.EventManager do\n @timeout 30_000\n\n def start_link() do\n import Supervisor.Spec\n child = worker(GenServer, [], restart: :temporary)\n Supervisor.start_link([child], strategy: :simple_one_for_one)\n end\n\n def stop(sup) do\n for {_, pid, _, _} <- Supervisor.which_children(sup) do\n GenServer.stop(pid, :normal, @timeout)\n end\n Supervisor.stop(sup)\n end\n\n def add_handler(sup, handler, opts) do\n Supervisor.start_child(sup, [handler, opts])\n end\n\n def suite_started(sup, opts) do\n notify(sup, {:suite_started, opts})\n end\n\n def suite_finished(sup, run_us, load_us) do\n notify(sup, {:suite_finished, run_us, load_us})\n end\n\n def case_started(sup, test_case) do\n notify(sup, {:case_started, test_case})\n end\n\n def case_finished(sup, test_case) do\n notify(sup, {:case_finished, test_case})\n end\n\n def test_started(sup, test) do\n notify(sup, {:test_started, test})\n end\n\n def test_finished(sup, test) do\n notify(sup, {:test_finished, test})\n end\n\n defp notify(sup, msg) do\n for {_, pid, _, _} <- Supervisor.which_children(sup) do\n GenServer.cast(pid, msg)\n end\n :ok\n end\nend\n<\/code><\/pre>\n
\n
:restart<\/code> strategy was set to
:temporary<\/code>. A custom formatter will be restarted only when the test suite runs again<\/p>\n<\/li>\n
GenServer.cast\/2<\/code> function<\/p>\n<\/li>\n
def handle_event({:test_finished, %ExUnit.Test{}}, state) do\n ...\n {:ok, new_state}\nend\n<\/code><\/pre>\n
def handle_cast({:test_finished, %ExUnit.Test{}}, state) do\n ...\n {:noreply, new_state}\nend\n<\/code><\/pre>\n
Watch out!<\/h2>\n
GenServer.cast\/2<\/code> by
GenServer.call\/3<\/code>. But then execution will be serial unless you call each handler inside a task:<\/p>\n
|> sup\n|> Supervisor.which_children()\n|> Enum.map(fn {_, pid, _, _} -> Task.async(GenServer, :call, [pid, msg]) end)\n|> Enum.map(&Task.await\/1)\n<\/code><\/pre>\n
GenServer.stop\/3<\/code> to synchronously terminate handlers. This only works because we set
:restart<\/code> to
:temporary<\/code>. Otherwise directly shutting down handlers would cause the supervisor to restart them. Alternatively, you could also skip the
GenServer.stop\/3<\/code> altogether and simply let
Supervisor.stop\/1<\/code> do the work of shutting down all children with exit signals. Then if a particular child needs synchronous termination, it can trap exits. We avoided this on purpose because we expect all handlers to require synchronous termination. Your mileage may vary.<\/p>\n
\n