Repository: commanded/commanded
Pull request #431 (2021-01-27 20:59:53)

Graceful shutdown of event handlers #430


Patch:
diff --git a/lib/commanded/event/handler.ex b/lib/commanded/event/handler.ex
index ca305dd..9700e4a 100644
--- a/lib/commanded/event/handler.ex
+++ b/lib/commanded/event/handler.ex
@@ -651,9 +651,15 @@ defmodule Commanded.Event.Handler do
   @doc false
   @impl GenServer
   def init(%Handler{} = state) do
+    Process.flag(:trap_exit, true)
     {:ok, state, {:continue, :subscribe_to_events}}
   end
 
+  @impl GenServer
+  def terminate(reason, state) do
+    Logger.debug(fn -> describe(state) <> " is shutting down due to #{inspect(reason)}" end)
+  end
+
   @doc false
   @impl GenServer
   def handle_continue(:subscribe_to_events, %Handler{} = state) do


Test patch:
diff --git a/test/event/event_handler_graceful_shutdown_test.exs b/test/event/event_handler_graceful_shutdown_test.exs
new file mode 100644
index 0000000..496f7cc
--- /dev/null
+++ b/test/event/event_handler_graceful_shutdown_test.exs
@@ -0,0 +1,58 @@
+defmodule Commanded.Event.EventHandlerGracefulShutdownTest do
+  use ExUnit.Case
+
+  alias Commanded.Event.GracefulShutdownHandler
+  alias Commanded.DefaultApp
+  alias Commanded.Helpers.EventFactory
+
+  defmodule AnEvent do
+    @derive Jason.Encoder
+    defstruct [:reply_to, :sleep_for]
+  end
+
+  defmodule EventHandlersSupervisor do
+    @moduledoc false
+
+    use Supervisor
+
+    alias Commanded.Event.GracefulShutdownHandler
+
+    def start_link(arg) do
+      Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+    end
+
+    def init(_arg) do
+      Supervisor.init(
+        [
+          GracefulShutdownHandler
+        ],
+        strategy: :one_for_one
+      )
+    end
+  end
+
+  describe "event handler state" do
+    setup do
+      start_supervised!(DefaultApp)
+      :ok
+    end
+
+    test "stop the event handler while it is handling an event" do
+      supervisor = start_supervised!(EventHandlersSupervisor, restart: :transient)
+      {_, handler, _, _} = supervisor |> Supervisor.which_children() |> List.first()
+
+      event = %AnEvent{reply_to: self(), sleep_for: 200}
+      send_events_to_handler(handler, [event])
+      Supervisor.stop(supervisor, {:shutdown, :graceful})
+
+      assert Process.whereis(EventHandlersSupervisor) == nil
+      assert_received {:event, ^event, _metadata}
+    end
+  end
+
+  defp send_events_to_handler(handler, events, initial_event_number \\ 1) do
+    recorded_events = EventFactory.map_to_recorded_events(events, initial_event_number)
+
+    send(handler, {:events, recorded_events})
+  end
+end
diff --git a/test/event/support/graceful_shutdown/graceful_shutdown_handler.ex b/test/event/support/graceful_shutdown/graceful_shutdown_handler.ex
new file mode 100644
index 0000000..6554735
--- /dev/null
+++ b/test/event/support/graceful_shutdown/graceful_shutdown_handler.ex
@@ -0,0 +1,15 @@
+defmodule Commanded.Event.GracefulShutdownHandler do
+  use Commanded.Event.Handler,
+    application: Commanded.DefaultApp,
+    name: __MODULE__
+
+  def handle(%{sleep_for: milliseconds} = event, metadata) do
+    %{reply_to: reply_to} = event
+
+    Process.sleep(milliseconds)
+
+    send(reply_to, {:event, event, metadata})
+
+    :ok
+  end
+end