Repository: commanded/commanded
Pull request #568 (2024-09-20 19:36:16)

Initialize with handler state
This PR replaces the `init/0` callback with `after_start/1` (using the old callback still works). In this way, you can for example use ecto dynamic repos with `Commanded.Projections.Ecto`, e.g.

```elixir
defmodule Myhandler do
  use Commanded.Projections.Ecto,
    name: "MyHandler",
    repo: MyRepo

  @impl true
  def after_start(state) do
    Ecto.Repo.put_dynamic_repo(state.tenant)
    {:ok, %{new_stuff: true}}
  end
end
```

Follows on @dvic's work in #526 


Patch:
diff --git a/guides/Events.md b/guides/Events.md
index 7636be8..bdc8a5d 100644
--- a/guides/Events.md
+++ b/guides/Events.md
@@ -88,8 +88,8 @@ This will ensure the handler only receives events appended to that stream.
 
 ### Event handler callbacks
 
-- `c:Commanded.Event.Handler.init/0` - (optional) initialisation callback function called when the handler starts.
 - `c:Commanded.Event.Handler.init/1` - (optional) used to configure the handler before it starts.
+- `c:Commanded.Event.Handler.after_start/1` - (optional) initialisation callback function called in the process of the started handler.
 - `c:Commanded.Event.Handler.error/3` - (optional) called when an event handle/2 callback returns an error.
 
 ### Metadata
diff --git a/guides/Usage.md b/guides/Usage.md
index 4d8db96..7fc77bc 100644
--- a/guides/Usage.md
+++ b/guides/Usage.md
@@ -104,7 +104,7 @@ Here's an example bank account opening feature built using Commanded to demonstr
         application: BankApp,
         name: __MODULE__
 
-      def init do
+      def after_start(_state) do
         with {:ok, _pid} <- Agent.start_link(fn -> 0 end, name: __MODULE__) do
           :ok
         end
diff --git a/lib/commanded/event/handler.ex b/lib/commanded/event/handler.ex
index 8b8b1d6..7f23d2a 100644
--- a/lib/commanded/event/handler.ex
+++ b/lib/commanded/event/handler.ex
@@ -344,14 +344,17 @@ defmodule Commanded.Event.Handler do
   @type subscribe_from :: :origin | :current | non_neg_integer()
   @type consistency :: :eventual | :strong
 
+  @doc deprecated: "Use the after_start/1 callback instead."
+  @callback init() :: :ok | {:stop, reason :: any()}
+
   @doc """
   Optional initialisation callback function called when the handler starts.
 
   Can be used to start any related processes when the event handler is started.
 
-  This callback function must return `:ok`, or `{:stop, reason}` to stop the
-  handler process. Any other return value will terminate the event handler with
-  an error.
+  This callback function must return `:ok`, `{:ok, state}` to return new state,
+  or `{:stop, reason}` to stop the handler process. Any other return value
+  will terminate the event handler with an error.
 
   ### Example
 
@@ -361,8 +364,9 @@ defmodule Commanded.Event.Handler do
           name: "ExampleHandler"
 
         # Optional initialisation
-        def init do
-          :ok
+        def after_start(handler_state) do
+          new_handler_state = Map.put(handler_state, :foo, "bar")
+          {:ok, new_handler_state}
         end
 
         def handle(%AnEvent{..}, _metadata) do
@@ -372,7 +376,8 @@ defmodule Commanded.Event.Handler do
       end
 
   """
-  @callback init() :: :ok | {:stop, reason :: any()}
+  @callback after_start(handler_state :: term()) ::
+              :ok | {:ok, state :: map()} | {:stop, reason :: any()}
 
   @doc """
   Optional callback function called to configure the handler before it starts.
@@ -606,7 +611,14 @@ defmodule Commanded.Event.Handler do
       end
 
       @doc false
-      def init, do: :ok
+      def after_start(_state) do
+        # TODO: remove this when we remove init/0
+        if function_exported?(__MODULE__, :init, 0) do
+          apply(__MODULE__, :init, [])
+        else
+          :ok
+        end
+      end
 
       @doc false
       def init(config), do: {:ok, config}
@@ -614,7 +626,7 @@ defmodule Commanded.Event.Handler do
       @doc false
       def before_reset, do: :ok
 
-      defoverridable init: 0, init: 1, before_reset: 0
+      defoverridable init: 1, after_start: 1, before_reset: 0
     end
   end
 
@@ -782,12 +794,20 @@ defmodule Commanded.Event.Handler do
 
     %Handler{handler_module: handler_module} = state
 
-    case handler_module.init() do
+    if function_exported?(handler_module, :init, 0) do
+      Logger.warning("#{inspect(handler_module)}.init/0 is deprecated, use after_start/1 instead")
+    end
+
+    case handler_module.after_start(state.handler_state) do
       :ok ->
         {:noreply, state}
 
+      {:ok, %{} = new_handler_state} ->
+        new_state = %{state | handler_state: new_handler_state}
+        {:noreply, new_state}
+
       {:stop, reason} ->
-        Logger.debug(describe(state) <> " `init/0` callback has requested to stop")
+        Logger.debug(describe(state) <> " `after_start/1` callback has requested to stop")
 
         {:stop, reason, state}
     end


Test patch:
diff --git a/test/event/support/appending_event_handler.ex b/test/event/support/appending_event_handler.ex
index 989a440..ced4da2 100644
--- a/test/event/support/appending_event_handler.ex
+++ b/test/event/support/appending_event_handler.ex
@@ -5,7 +5,7 @@ defmodule Commanded.Event.AppendingEventHandler do
     application: Commanded.DefaultApp,
     name: __MODULE__
 
-  def init do
+  def after_start(_state) do
     with {:ok, _pid} <- Agent.start_link(fn -> %{events: [], metadata: []} end, name: __MODULE__) do
       :ok
     end
diff --git a/test/event/support/concurrency/concurrent_event_handler.ex b/test/event/support/concurrency/concurrent_event_handler.ex
index 017304a..7d44969 100644
--- a/test/event/support/concurrency/concurrent_event_handler.ex
+++ b/test/event/support/concurrency/concurrent_event_handler.ex
@@ -8,7 +8,7 @@ defmodule Commanded.Event.ConcurrentEventHandler do
     concurrency: 5
 
   @impl Commanded.Event.Handler
-  def init do
+  def after_start(_state) do
     Process.send(:test, {:init, self()}, [])
   end
 
diff --git a/test/event/support/concurrency/partition_event_handler.ex b/test/event/support/concurrency/partition_event_handler.ex
index fe7a005..b1ad10c 100644
--- a/test/event/support/concurrency/partition_event_handler.ex
+++ b/test/event/support/concurrency/partition_event_handler.ex
@@ -8,7 +8,7 @@ defmodule Commanded.Event.PartitionEventHandler do
     concurrency: 5
 
   @impl Commanded.Event.Handler
-  def init do
+  def after_start(_state) do
     Process.send(:test, {:init, self()}, [])
   end
 
diff --git a/test/event_handler_after_start_test.exs b/test/event_handler_after_start_test.exs
new file mode 100644
index 0000000..782f0b1
--- /dev/null
+++ b/test/event_handler_after_start_test.exs
@@ -0,0 +1,102 @@
+defmodule Commanded.Event.HandlerAfterStartTest do
+  use Commanded.MockEventStoreCase
+
+  import Mox
+  import ExUnit.CaptureLog
+
+  alias Commanded.EventStore.Adapters.Mock, as: MockEventStore
+
+  setup do
+    stub(MockEventStore, :subscribe_to, fn
+      _event_store, :all, _handler_name, handler, _subscribe_from, _opts ->
+        {:ok, handler}
+    end)
+
+    :ok
+  end
+
+  describe "event handler `init/0` callback" do
+    # TODO: remove these test when we remove init/0
+
+    setup(%{test: test}) do
+      # HACK: generate a module that can communicate back to our test process
+      true = Process.register(self(), test)
+
+      Code.eval_string("""
+        defmodule DeprecatedHandler do
+          use Commanded.Event.Handler,
+            application: Commanded.MockedApp,
+            name: __MODULE__
+
+          @impl Commanded.Event.Handler
+          def init() do
+            process_name = :"#{test}"
+            Process.send(process_name, {:init, self()}, [])
+          end
+        end
+      """)
+
+      [handler: start_supervised!(DeprecatedHandler)]
+    end
+
+    test "should be called and a deprecation warning raised after handler subscribbed", %{
+      handler: handler
+    } do
+      warning =
+        capture_log([level: :warning], fn ->
+          # When the handler subscribes to the eventstore
+          send_subscribed(handler)
+
+          # Then we expect init/0 to have been called for us
+          assert_receive {:init, ^handler}
+        end)
+
+      # And we expect a deprecation warning to have been logged
+      assert warning =~ "DeprecatedHandler.init/0 is deprecated, use after_start/1 instead"
+    end
+  end
+
+  describe "event handler `after_start/1` callback" do
+    defmodule AfterStartHandler do
+      use Commanded.Event.Handler,
+        application: Commanded.MockedApp,
+        name: __MODULE__
+
+      def after_start(state) do
+        test_pid = Map.fetch!(state, :test)
+        ref = Map.get_lazy(state, :ref, &make_ref/0)
+        reply = Map.get(state, :reply, :ok)
+
+        Process.send(test_pid, {ref, :after_start, reply}, [])
+        reply
+      end
+    end
+
+    test "should be called after handler subscribed" do
+      ref = make_ref()
+      state = %{test: self(), ref: ref}
+      handler = start_supervised!({AfterStartHandler, state: state})
+
+      refute_receive {^ref, :after_start, :ok}
+
+      send_subscribed(handler)
+
+      assert_receive {^ref, :after_start, :ok}
+    end
+
+    test "should reply with new state" do
+      ref = make_ref()
+      state = %{test: self(), ref: ref, reply: {:ok, %{something: :new}}}
+      handler = start_supervised!({AfterStartHandler, state: state})
+
+      send_subscribed(handler)
+
+      assert_receive {^ref, :after_start, {:ok, new_state}}
+      assert new_state == %{something: :new}
+    end
+  end
+
+  defp send_subscribed(handler) do
+    send(handler, {:subscribed, handler})
+  end
+end
diff --git a/test/example_domain/bank_account/account_balance_handler.ex b/test/example_domain/bank_account/account_balance_handler.ex
index 58cbf97..32c401a 100644
--- a/test/example_domain/bank_account/account_balance_handler.ex
+++ b/test/example_domain/bank_account/account_balance_handler.ex
@@ -9,7 +9,7 @@ defmodule Commanded.ExampleDomain.BankAccount.AccountBalanceHandler do
   alias Commanded.ExampleDomain.BankAccount.Events.MoneyDeposited
   alias Commanded.ExampleDomain.BankAccount.Events.MoneyWithdrawn
 
-  def init do
+  def after_start(_state) do
     with {:ok, _pid} <- Agent.start_link(fn -> 0 end, name: __MODULE__) do
       :ok
     end
diff --git a/test/example_domain/bank_account/bank_account_handler.ex b/test/example_domain/bank_account/bank_account_handler.ex
index aec5c29..9bee95b 100644
--- a/test/example_domain/bank_account/bank_account_handler.ex
+++ b/test/example_domain/bank_account/bank_account_handler.ex
@@ -10,7 +10,7 @@ defmodule Commanded.ExampleDomain.BankAccount.BankAccountHandler do
   alias Commanded.ExampleDomain.BankAccount.Events.BankAccountOpened
 
   @impl Handler
-  def init do
+  def after_start(_state) do
     case Agent.start_link(fn -> %{prefix: "", accounts: []} end, name: __MODULE__) do
       {:ok, _} -> :ok
       {:error, {:already_started, _}} -> :ok