diff --git a/config/test.exs b/config/test.exs index 5204b342..7e81c3f3 100644 --- a/config/test.exs +++ b/config/test.exs @@ -25,12 +25,7 @@ default_app_config = [ config :commanded, Commanded.Commands.ConsistencyApp, default_app_config config :commanded, Commanded.DefaultApp, [] config :commanded, Commanded.DistributedApp, [] -config :commanded, Commanded.Event.Upcast.ProcessManager.Application, default_app_config config :commanded, Commanded.Middleware.TenantApp, default_app_config -config :commanded, Commanded.ProcessManagers.ErrorApp, default_app_config -config :commanded, Commanded.ProcessManagers.ExampleApp, default_app_config -config :commanded, Commanded.ProcessManagers.ResumeApp, default_app_config -config :commanded, Commanded.ProcessManagers.TodoApp, default_app_config config :commanded, Commanded.TestApplication, default_app_config config :commanded, event_stores: [TestEventStore] diff --git a/guides/explanations/commands.md b/guides/explanations/commands.md index f147b571..681ddc34 100644 --- a/guides/explanations/commands.md +++ b/guides/explanations/commands.md @@ -243,7 +243,7 @@ In Commanded, the available options during command dispatch are: :ok = BankApp.dispatch(command, consistency: :eventual) ``` -- `:strong` - block command dispatch until all strongly consistent event handlers and process managers have successfully processed all events created by the command. +- `:strong` - block command dispatch until all strongly consistent event handlers have successfully processed all events created by the command. ```elixir :ok = BankApp.dispatch(command, consistency: :strong) @@ -251,7 +251,7 @@ In Commanded, the available options during command dispatch are: Dispatching a command using `:strong` consistency but without any strongly consistent event handlers configured will have no effect. -- Provide an explicit list of event handler and process manager modules (or their configured names), containing only those handlers you'd like to wait for. No other handlers will be awaited on, regardless of their own configured consistency setting. +- Provide an explicit list of event handler modules (or their configured names), containing only those handlers you'd like to wait for. No other handlers will be awaited on, regardless of their own configured consistency setting. ```elixir :ok = BankApp.dispatch(command, consistency: [ExampleHandler, AnotherHandler]) @@ -262,11 +262,11 @@ In Commanded, the available options during command dispatch are: #### Which consistency guarantee should I use? -When dispatching a command using `consistency: :strong` the dispatch will block until all of the strongly consistent event handlers and process managers have handled all events created by the command. This guarantees that when you receive the `:ok` response from dispatch, your strongly consistent read models will have been updated and can safely be queried. +When dispatching a command using `consistency: :strong` the dispatch will block until all of the strongly consistent event handlers have handled all events created by the command. This guarantees that when you receive the `:ok` response from dispatch, your strongly consistent read models will have been updated and can safely be queried. Strong consistency helps to alleviate problems and workarounds you would otherwise encounter when dealing with eventual consistency in your own application. Use `:strong` consistency when you want to query a read model immediately after dispatching a command. You **must** also configure the event handler to use `:strong` consistency. -Using `:eventual` consistency, or omitting the `consistency` option, will cause the command dispatch to immediately return without waiting for any event handlers or process managers. The handlers run independently, and asynchronously, in the background, therefore you will need to deal with potentially stale read model data. +Using `:eventual` consistency, or omitting the `consistency` option, will cause the command dispatch to immediately return without waiting for any event handlers. The handlers run independently, and asynchronously, in the background, therefore you will need to deal with potentially stale read model data. #### Configure default consistency @@ -276,7 +276,7 @@ You may override the default consistency (`:eventual`) by setting `default_consi config :commanded, default_consistency: :strong ``` -This will effect command dispatch, event handlers, and process managers where a consistency is not explicitly defined. +This will effect command dispatch and event handlers where a consistency is not explicitly defined. #### Consistency failures @@ -411,8 +411,6 @@ defmodule ExampleHandler do end ``` -Commands dispatched by a process manager will be automatically assigned the appropriate causation and correlation ids from the source domain event. - You can use [Commanded audit middleware](https://hex.pm/packages/commanded_audit_middleware) to record every dispatched command. This allows you to follow the chain of commands and events by using the causation id. The correlation id can be used to find all related commands and events. #### Configuring UUID provider diff --git a/guides/explanations/events.md b/guides/explanations/events.md index 4579296f..17f73204 100644 --- a/guides/explanations/events.md +++ b/guides/explanations/events.md @@ -227,7 +227,7 @@ An event handler is a `GenServer` process that subscribes to the configured even Commanded supports upcasting of events at runtime using the `Commanded.Event.Upcaster` protocol. -By implementing the upcaster protocol you can transform an event before it is used by a consumer. This might be an aggregate, an event handler, or a process manager. Because the upcaster changes the event at runtime, handlers only need to support the latest version. You can also use upcasting to change the type of event. +By implementing the upcaster protocol you can transform an event before it is used by a consumer. This might be an aggregate or an event handler. Because the upcaster changes the event at runtime, handlers only need to support the latest version. You can also use upcasting to change the type of event. ### Examples diff --git a/guides/explanations/process-managers.md b/guides/explanations/process-managers.md deleted file mode 100644 index b4e940e6..00000000 --- a/guides/explanations/process-managers.md +++ /dev/null @@ -1,226 +0,0 @@ -# Process managers - -A process manager is responsible for coordinating one or more aggregates. It handles events and dispatches commands in response. You can think of a process manager as the opposite of an aggregate: aggregates handle commands and create events; process managers handle events and create commands. Process managers have state that can be used to track which aggregates are being orchestrated. - -Use the `Commanded.ProcessManagers.ProcessManager` macro in your process manager module and implement the callback functions defined in the behaviour: `c:Commanded.ProcessManagers.ProcessManager.interested?/1`, `c:Commanded.ProcessManagers.ProcessManager.handle/2`, `c:Commanded.ProcessManagers.ProcessManager.apply/2`, and `c:Commanded.ProcessManagers.ProcessManager.error/3`. - -## `interested?/1` - -The `c:Commanded.ProcessManagers.ProcessManager.interested?/1` function is used to indicate which events the process manager handles. The response is used to route the event to an existing instance or start a new process instance: - -- `{:start, process_uuid}` - create a new instance of the process manager. -- `{:start!, process_uuid}` - create a new instance of the process manager (strict). -- `{:continue, process_uuid}` - continue execution of an existing process manager. -- `{:continue!, process_uuid}` - continue execution of an existing process manager (strict). -- `{:stop, process_uuid}` - stop an existing process manager, shutdown its - process, and delete its persisted state. -- `false` - ignore the event. - -You can return a list of process identifiers when a single domain event is to be handled by multiple process instances. - -### Strict process routing - -Using strict routing, with `:start!` or `:continue!`, enforces the following validation checks: - -- `{:start!, process_uuid}` - validate process does not already exist. -- `{:continue!, process_uuid}` - validate process already exists. - -If the check fails an error will be passed to the `error/3` callback function: - -- `{:error, {:start!, :process_already_started}}` -- `{:error, {:continue!, :process_not_started}}` - -The `c:Commanded.ProcessManagers.ProcessManager.error/3` function can choose to `:stop` the process or `:skip` the problematic event. - -## `handle/2` - -A `handle/2` function can be defined for each `:start` and `:continue` tagged event previously specified. It receives the process manager's state and the event to be handled. It must return the commands to be dispatched. This may be none, a single command, or many commands. - -The `handle/2` function can be omitted if you do not need to dispatch a command and are only mutating the process manager's state. - -## `apply/2` - -The `c:Commanded.ProcessManagers.ProcessManager.apply/2` function is used to mutate the process manager's state. It receives the current state and the domain event, and must return the modified state. - -This callback function is optional, the default behaviour is to retain the process manager's current state. - -## `error/3` - -You can define an `c:Commanded.ProcessManagers.ProcessManager.error/3` callback function to handle any errors or exceptions during event handling or returned by commands dispatched from your process manager. The function is passed the error (e.g. `{:error, :failure}`), the failed event or command, and a failure context. See `Commanded.ProcessManagers.FailureContext` for details. - -Use pattern matching on the error and/or failed event/command to explicitly handle certain errors, events, or commands. You can choose to retry, skip, ignore, or stop the process manager after a command dispatch error. - -The default behaviour, if you don't provide an `c:Commanded.ProcessManagers.ProcessManager.error/3` callback, is to stop the process manager using the exact error reason returned from the event handler function or command dispatch. - -The `c:Commanded.ProcessManagers.ProcessManager.error/3` callback function must return one of the following responses depending upon the severity of error and how you choose to handle it: - -- `{:retry, context}` - retry the failed command, provide a context map containing any state passed to subsequent failures. This could be used to count the number of retries, failing after too many attempts. - -- `{:retry, delay, context}` - retry the failed command, after sleeping for the requested delay (in milliseconds). Context is a map as described in `{:retry, context}` above. - -- `{:stop, reason}` - stop the process manager with the given reason. - -For event handling failures, when failure source is an event, you can also return: - -- `:skip` - to skip the problematic event. No commands will be dispatched. - -For command dispatch failures, when failure source is a command, you can also return: - -- `{:continue, commands, context}` - continue dispatching the given commands. This allows you to retry the failed command, modify it and retry, drop it, or drop all pending commands by passing an empty list `[]`. - -- `{:skip, :discard_pending}` - discard the failed command and any pending commands. - -- `{:skip, :continue_pending}` - skip the failed command, but continue dispatching any pending commands. - -## Supervision - -Supervise your process managers to ensure they are restarted on error. - -```elixir -defmodule Bank.Payments.Supervisor do - use Supervisor - - def start_link(_arg) do - Supervisor.start_link(__MODULE__, [], name: __MODULE__) - end - - def init(_arg) do - Supervisor.init( - [ - Bank.Payments.TransferMoneyProcessManager - ], - strategy: :one_for_one - ) - end -end -``` - -### Supervision caveats - -The default error handling strategy is to stop the process manager. When supervised, the process will be restarted and will attempt to handle the same event again, which will likely result in the same error. This could lead to too many restarts of the supervisor, which may eventually cause the application to stop, depending upon your supervision tree and its strategy. - -To prevent this you can choose to define the default error handling strategy as `:skip` to skip over any problematic events. - -```elixir -defmodule ExampleProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: __MODULE__ - - require Logger - - # By default skip any problematic events - def error(error, _command_or_event, _failure_context) do - Logger.error(fn -> "#{__MODULE__} encountered an error: " <> inspect(error) end) - - :skip - end -end -``` - -Alternatively you can define the restart strategy of your process manager as `:temporary` to prevent it from being restarted on termination. This approach will require manual intervention to fix the stopped process manager, but ensures that it won't miss any events nor crash the application. - -### Error handling example - -Define an `error/3` callback function to determine how to handle errors during event handling and command dispatch. - -```elixir -defmodule ExampleProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "ExampleProcessManager" - - # Stop process manager after three failures - def error({:error, _failure}, _failed_message, %{context: %{failures: failures}}) - when failures >= 2 - do - {:stop, :too_many_failures} - end - - # Retry command, record failure count in context map - def error({:error, _failure}, _failed_message, %{context: context}) do - context = Map.update(context, :failures, 1, fn failures -> failures + 1 end) - {:retry, context} - end -end -``` - -The default behaviour if you don't provide an `error/3` callback is to stop the process manager using the same error reason returned from the failed command dispatch. - -## Example process manager - -```elixir -defmodule TransferMoneyProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "TransferMoneyProcessManager" - - @derive Jason.Encoder - defstruct [ - :transfer_uuid, - :debit_account, - :credit_account, - :amount, - :status - ] - - # Process routing - - def interested?(%MoneyTransferRequested{transfer_uuid: transfer_uuid}), do: {:start, transfer_uuid} - def interested?(%MoneyWithdrawn{transfer_uuid: transfer_uuid}), do: {:continue, transfer_uuid} - def interested?(%MoneyDeposited{transfer_uuid: transfer_uuid}), do: {:stop, transfer_uuid} - def interested?(_event), do: false - - # Command dispatch - - def handle(%TransferMoneyProcessManager{}, %MoneyTransferRequested{} = event) do - %MoneyTransferRequested{transfer_uuid: transfer_uuid, debit_account: debit_account, amount: amount} = event - - %WithdrawMoney{account_number: debit_account, transfer_uuid: transfer_uuid, amount: amount} - end - - def handle(%TransferMoneyProcessManager{} = pm, %MoneyWithdrawn{}) do - %TransferMoneyProcessManager{transfer_uuid: transfer_uuid, credit_account: credit_account, amount: amount} = pm - - %DepositMoney{account_number: credit_account, transfer_uuid: transfer_uuid, amount: amount} - end - - # State mutators - - def apply(%TransferMoneyProcessManager{} = transfer, %MoneyTransferRequested{} = event) do - %MoneyTransferRequested{transfer_uuid: transfer_uuid, debit_account: debit_account, credit_account: credit_account, amount: amount} = event - - %TransferMoneyProcessManager{transfer | - transfer_uuid: transfer_uuid, - debit_account: debit_account, - credit_account: credit_account, - amount: amount, - status: :withdraw_money_from_debit_account - } - end - - def apply(%TransferMoneyProcessManager{} = transfer, %MoneyWithdrawn{}) do - %TransferMoneyProcessManager{transfer | - status: :deposit_money_in_credit_account - } - end -end -``` - -The name given to the process manager *must* be unique. This is used when subscribing to events from the event store to track the last seen event and ensure they are only received once. - -```elixir -{:ok, _pid} = TransferMoneyProcessManager.start_link(start_from: :current) -``` - -You can choose to start the process router's event store subscription from the `:origin`, `:current` position or an exact event number using the `start_from` option. The default is to use `:origin` so it will receive all events. You typically use `:current` when adding a new process manager to an already deployed system containing historical events. - -Process manager instance state is persisted to storage after each handled event. This allows the process manager to resume should the host process terminate. - -## Configuration options - -- `consistency` - defined as one of either `:strong` or `:eventual` (default) for event handling. -- `event_timeout` - a timeout for event handling to ensure that events are processed in a timely manner without getting stuck. -- `idle_timeout` - to reduce memory usage you can configure an idle timeout, in milliseconds, after which an inactive process instance will be shutdown. - -Refer to the `Commanded.ProcessManagers.ProcessManager` module docs for more details. diff --git a/guides/explanations/serialization.md b/guides/explanations/serialization.md index a7b031fa..3c2942c2 100644 --- a/guides/explanations/serialization.md +++ b/guides/explanations/serialization.md @@ -2,7 +2,7 @@ ## Default JSON serializer -JSON serialization can be used for event data & metadata, and aggregate and process manager snapshots. +JSON serialization can be used for event data & metadata, and aggregate snapshots. To enable JSON serialization with the included `Commanded.Serialization.JsonSerializer` module add the `jason` library to your deps: diff --git a/guides/explanations/supervision.md b/guides/explanations/supervision.md index 41f0b818..da64777a 100644 --- a/guides/explanations/supervision.md +++ b/guides/explanations/supervision.md @@ -1,6 +1,6 @@ # Supervision -Use an OTP supervisor to host your Commanded application, process managers, event handlers, and read model projectors. +Use an OTP supervisor to host your Commanded application, event handlers, and read model projectors. ```elixir defmodule Bank.Supervisor do @@ -19,9 +19,6 @@ defmodule Bank.Supervisor do # Event handler AccountBalanceHandler, - # Process manager - TransferMoneyProcessManager, - # Read model projector AccountsProjector, diff --git a/guides/explanations/testing.md b/guides/explanations/testing.md index 242b9804..396f38a4 100644 --- a/guides/explanations/testing.md +++ b/guides/explanations/testing.md @@ -256,7 +256,7 @@ use Mix.Config config :my_app, consistency: :strong ``` -Then read the setting when defining your event handlers and process managers: +Then read the setting when defining your event handlers: ```elixir defmodule ExampleEventHandler do diff --git a/guides/howtos/migrating-from-v1-to-v2.md b/guides/howtos/migrating-from-v1-to-v2.md index a4170889..6e6b6e59 100644 --- a/guides/howtos/migrating-from-v1-to-v2.md +++ b/guides/howtos/migrating-from-v1-to-v2.md @@ -2,7 +2,7 @@ ## Overview -Commanded v2.0 introduces breaking changes to improve type safety and API clarity. The primary change is that metadata passed to event handlers and process managers has been changed from a plain map to the `Commanded.EventStore.EnrichedMetadata` struct. +Commanded v2.0 introduces breaking changes to improve type safety and API clarity. The primary change is that metadata passed to event handlers has been changed from a plain map to the `Commanded.EventStore.EnrichedMetadata` struct. The issues come when you need to propagate the used-provided metadata, and you need to drop some keys, example: @@ -80,16 +80,7 @@ end end ``` -2. **Update your process manager callbacks:** - - - `interested?/2` - - `handle/3` - - `apply/3` - - `after_command/3` - - All now receive `%EnrichedMetadata{}` instead of a plain map. - -3. **Update pattern matching:** +2. **Update pattern matching:** ```elixir # Before diff --git a/guides/howtos/usage.md b/guides/howtos/usage.md index c8b033b3..b13b1e00 100644 --- a/guides/howtos/usage.md +++ b/guides/howtos/usage.md @@ -8,7 +8,6 @@ A separate guide is provided for each of the components you can build: - [Aggregates](https://hexdocs.pm/commanded/aggregates.html) - [Commands, registration and dispatch](https://hexdocs.pm/commanded/commands.html) - [Events and handlers](https://hexdocs.pm/commanded/events.html) -- [Process managers](https://hexdocs.pm/commanded/process-managers.html) Commanded uses strong consistency for command dispatch (write model) and eventual consistency, by default, for the read model. Receiving an `:ok` reply from dispatch indicates the command was successfully handled and any created domain events fully persisted to your chosen event store. You may opt into strong consistency for individual event handlers and command dispatch as required. diff --git a/lib/application.ex b/lib/application.ex index ecd99570..2f62f41f 100644 --- a/lib/application.ex +++ b/lib/application.ex @@ -115,7 +115,7 @@ defmodule Commanded.Application do own separately configured and isolated event store. Each application must be started with a unique name. - Multiple instances of the same event handler or process manager can be + Multiple instances of the same event handler can be started by referring to a started application by its name. The event store operations can also be scoped to an application by referring to its name. @@ -320,9 +320,9 @@ defmodule Commanded.Application do - `:eventual` (default) - a successful command dispatch will return immediately. - `:strong` - a successful command dispatch will block until all strongly - consistent event handlers and process managers have handled all events created by the command. + consistent event handlers have handled all events created by the command. - - An explicit list of event handler and process manager modules (or their configured names), + - An explicit list of event handler modules (or their configured names), containing only those handlers you'd like to wait for. No other handlers will be awaited on, regardless of their own configured consistency setting. e.g. `[ExampleHandler, AnotherHandler]` or `["ExampleHandler", "AnotherHandler"]` diff --git a/lib/commanded.ex b/lib/commanded.ex index de0972bb..76e5bc30 100644 --- a/lib/commanded.ex +++ b/lib/commanded.ex @@ -8,7 +8,6 @@ defmodule Commanded do - [Command registration and dispatch](commands.html). - [Hosting and delegation to aggregates](aggregates.html). - [Event handling](events.html). - - [Long running process managers](process-managers.html). Use Commanded with one of the following event stores for persistence: diff --git a/lib/commanded/commands/router.ex b/lib/commanded/commands/router.ex index 494cc3c0..733bc6df 100644 --- a/lib/commanded/commands/router.ex +++ b/lib/commanded/commands/router.ex @@ -113,7 +113,7 @@ defmodule Commanded.Commands.Router do :ok = BankApp.dispatch(command, consistency: :eventual) - `:strong` - block command dispatch until all strongly - consistent event handlers and process managers have successfully processed + consistent event handlers have successfully processed all events created by the command. Use this when you have event handlers that update read models you need to @@ -121,7 +121,7 @@ defmodule Commanded.Commands.Router do :ok = BankApp.dispatch(command, consistency: :strong) - - Provide an explicit list of event handler and process manager modules (or + - Provide an explicit list of event handler modules (or their configured names), containing only those handlers you'd like to wait for. No other handlers will be awaited on, regardless of their own configured consistency setting. diff --git a/lib/commanded/event/handler.ex b/lib/commanded/event/handler.ex index b108ecf7..972050ea 100644 --- a/lib/commanded/event/handler.ex +++ b/lib/commanded/event/handler.ex @@ -131,7 +131,7 @@ defmodule Commanded.Event.Handler do ## Event handler name The name you specify is used when subscribing to the event store. You must use - a unique name for each event handler and process manager you start. Also, you + a unique name for each event handler you start. Also, you *should not* change the name once the handler has been deployed. A new subscription will be created if you change the name and the event handler will receive already handled events. diff --git a/lib/commanded/event/upcaster.ex b/lib/commanded/event/upcaster.ex index ebc6e202..ddf8e2c4 100644 --- a/lib/commanded/event/upcaster.ex +++ b/lib/commanded/event/upcaster.ex @@ -9,7 +9,7 @@ defprotocol Commanded.Event.Upcaster do Upcaster will run for new events and for historical events. Because the upcaster changes any historical event to the latest version, - consumers (aggregates, event handlers, and process managers) only need + consumers (aggregates and event handlers) only need to support the latest version. ## Example diff --git a/lib/commanded/process_managers/failure_context.ex b/lib/commanded/process_managers/failure_context.ex deleted file mode 100644 index eb0a7f31..00000000 --- a/lib/commanded/process_managers/failure_context.ex +++ /dev/null @@ -1,42 +0,0 @@ -defmodule Commanded.ProcessManagers.FailureContext do - @moduledoc """ - Data related to a process manager event handling or command dispatch failure. - - The available fields are: - - - `context` - the context map passed between each failure and may be used - to track state between retries, such as to count failures. - - - `enriched_metadata` - the enriched metadata associated with the event. - - - `last_event` - the last event the process manager received. - - - `pending_commands` - the pending commands that were not executed yet. - - - `process_manager_state` - the state the process manager would be in - if the event handling or command dispatch had not failed. - - - `stacktrace` - the stacktrace if the error was an unhandled exception. - - """ - alias Commanded.EventStore.EnrichedMetadata - alias Commanded.EventStore.RecordedEvent - - @type t :: %__MODULE__{ - context: map(), - enriched_metadata: EnrichedMetadata.t(), - last_event: RecordedEvent.t(), - pending_commands: [struct()], - process_manager_state: struct(), - stacktrace: Exception.stacktrace() | nil - } - - defstruct [ - :enriched_metadata, - :last_event, - :process_manager_state, - :stacktrace, - context: %{}, - pending_commands: [] - ] -end diff --git a/lib/commanded/process_managers/process_manager.ex b/lib/commanded/process_managers/process_manager.ex deleted file mode 100644 index 87a02487..00000000 --- a/lib/commanded/process_managers/process_manager.ex +++ /dev/null @@ -1,667 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManager do - use TelemetryRegistry - - telemetry_event(%{ - event: [:commanded, :process_manager, :handle, :start], - description: "Emitted when a process manager starts handling an event", - measurements: "%{system_time: integer()}", - metadata: """ - %{application: Commanded.Application.t(), - process_manager_name: String.t() | Inspect.t(), - process_manager_module: module(), - process_state: term(), - process_uuid: String.t()} - """ - }) - - telemetry_event(%{ - event: [:commanded, :process_manager, :handle, :stop], - description: "Emitted when a process manager stops handling an event", - measurements: "%{duration: non_neg_integer()}", - metadata: """ - %{application: Commanded.Application.t(), - commands: [struct()], - error: nil | any(), - process_manager_name: String.t() | Inspect.t(), - process_manager_module: module(), - process_state: term(), - process_uuid: String.t()} - """ - }) - - telemetry_event(%{ - event: [:commanded, :process_manager, :handle, :exception], - description: "Emitted when a process manager raises an exception", - measurements: "%{duration: non_neg_integer()}", - metadata: """ - %{application: Commanded.Application.t(), - process_manager_name: String.t() | Inspect.t(), - process_manager_module: module(), - process_state: term(), - process_uuid: String.t(), - kind: :throw | :error | :exit, - reason: any(), - stacktrace: list()} - """ - }) - - @moduledoc """ - Macro used to define a process manager. - - A process manager is responsible for coordinating one or more aggregates. - It handles events and dispatches commands in response. Process managers have - state that can be used to track which aggregates are being orchestrated. - - Process managers can be used to implement long-running transactions by - following the saga pattern. This is a sequence of commands and their - compensating commands which can be used to rollback on failure. - - Use the `Commanded.ProcessManagers.ProcessManager` macro in your process - manager module and implement the callback functions defined in the behaviour: - - - `c:interested?/1` - - `c:interested?/2` - - `c:handle/2` - - `c:handle/3` - - `c:apply/2` - - `c:after_command/2` - - `c:after_command/3` - - `c:error/3` - - Please read the [Process managers](process-managers.html) guide for more - detail. - - ### Example - - defmodule ExampleProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "ExampleProcessManager" - - defstruct [] - - def interested?(%AnEvent{uuid: uuid}), do: {:start, uuid} - - def interested?(%AnotherEvent{}, metadata), - do: {:continue, Map.fetch!(metadata, :correlation_id)} - - def handle(%ExampleProcessManager{}, %ExampleEvent{}) do - [ - %ExampleCommand{} - ] - end - - def handle(%ExampleProcessManager{}, %AnotherEvent{}, _metadata) do - [ - %AnotherCommand{} - ] - end - - def after_command(%ExampleProcessManager{}, %ExampleCommand{}) do - :continue - end - - def after_command(%ExampleProcessManager{}, %AnotherCommand{}, _metadata) do - :stop - end - - def error({:error, failure}, %ExampleEvent{}, _failure_context) do - # Retry, skip, ignore, or stop process manager on error handling event - :skip - end - - def error({:error, failure}, %ExampleCommand{}, _failure_context) do - # Retry, skip, ignore, or stop process manager on error dispatching command - :skip - end - end - - Start the process manager (or configure as a worker inside a [Supervisor](supervision.html)) - - {:ok, process_manager} = ExampleProcessManager.start_link() - - ## `c:init/1` callback - - An `c:init/1` function can be defined in your process manager which is used to - provide runtime configuration. This callback function must return - `{:ok, config}` with the updated config. - - ### Example - - The `c:init/1` function is used to define the process manager's application - and name based upon a value provided at runtime: - - defmodule ExampleProcessManager do - use Commanded.ProcessManagers.ProcessManager - - def init(config) do - {tenant, config} = Keyword.pop!(config, :tenant) - - config = - config - |> Keyword.put(:application, Module.concat([ExampleApp, tenant])) - |> Keyword.put(:name, Module.concat([__MODULE__, tenant])) - - {:ok, config} - end - end - - Usage: - - {:ok, _pid} = ExampleProcessManager.start_link(tenant: :tenant1) - - ## Error handling - - You can define an `c:error/3` callback function to handle any errors or - exceptions during event handling or returned by commands dispatched from your - process manager. The function is passed the error (e.g. `{:error, :failure}`), - the failed event or command, and a failure context. - See `Commanded.ProcessManagers.FailureContext` for details. - - Use pattern matching on the error and/or failed event/command to explicitly - handle certain errors, events, or commands. You can choose to retry, skip, - ignore, or stop the process manager after a command dispatch error. - - The default behaviour, if you don't provide an `c:error/3` callback, is to - stop the process manager using the exact error reason returned from the - event handler function or command dispatch. You should supervise your - process managers to ensure they are restarted on error. - - ### Example - - defmodule ExampleProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "ExampleProcessManager" - - # stop process manager after three failures - def error({:error, _failure}, _failed_command, %{context: %{failures: failures}}) - when failures >= 2 - do - {:stop, :too_many_failures} - end - - # retry command, record failure count in context map - def error({:error, _failure}, _failed_command, %{context: context}) do - context = Map.update(context, :failures, 1, fn failures -> failures + 1 end) - - {:retry, context} - end - end - - ## Idle process timeouts - - Each instance of a process manager will run indefinitely once started. To - reduce memory usage you can configure an idle timeout, in milliseconds, - after which the process will be shutdown. - - The process will be restarted whenever another event is routed to it and its - state will be rehydrated from the instance snapshot. - - ### Example - - defmodule ExampleProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "ExampleProcessManager" - idle_timeout: :timer.minutes(10) - end - - ## Event handling timeout - - You can configure a timeout for event handling to ensure that events are - processed in a timely manner without getting stuck. - - An `event_timeout` option, defined in milliseconds, may be provided when using - the `Commanded.ProcessManagers.ProcessManager` macro at compile time: - - defmodule TransferMoneyProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "TransferMoneyProcessManager", - router: BankRouter, - event_timeout: :timer.minutes(10) - end - - Or may be configured when starting a process manager: - - {:ok, _pid} = TransferMoneyProcessManager.start_link( - event_timeout: :timer.hours(1) - ) - - After the timeout has elapsed, indicating the process manager has not - processed an event within the configured period, the process manager is - stopped. The process manager will be restarted if supervised and will retry - the event, this should help resolve transient problems. - - ## Consistency - - For each process manager you can define its consistency, as one of either - `:strong` or `:eventual`. - - This setting is used when dispatching commands and specifying the - `consistency` option. - - When you dispatch a command using `:strong` consistency, after successful - command dispatch the process will block until all process managers configured - to use `:strong` consistency have processed the domain events created by the - command. - - The default setting is `:eventual` consistency. Command dispatch will return - immediately upon confirmation of event persistence, not waiting for any - process managers. - - ### Example - - Define a process manager with `:strong` consistency: - - defmodule ExampleProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "ExampleProcessManager", - consistency: :strong - end - - ## Dynamic application - - A process manager's application can be provided as an option to `start_link/1`. - This can be used to start the same process manager multiple times, each using a - separate Commanded application and event store. - - ### Example - - Start an process manager for each tenant in a multi-tenanted app, guaranteeing - that the data and processing remains isolated between tenants. - - for tenant <- [:tenant1, :tenant2, :tenant3] do - {:ok, _app} = MyApp.Application.start_link(name: tenant) - {:ok, _handler} = ExampleProcessManager.start_link(application: tenant) - end - - Typically you would start the event handlers using a supervisor: - - children = - for tenant <- [:tenant1, :tenant2, :tenant3] do - {ExampleProcessManager, application: tenant} - end - - Supervisor.start_link(children, strategy: :one_for_one) - - The above example requires three named Commanded applications to have already - been started. - - ## Telemetry - - #{telemetry_docs()} - - """ - - alias Commanded.EventStore.EnrichedMetadata - - alias Commanded.ProcessManagers.{ - FailureContext, - ProcessManager, - ProcessManagerInstance, - ProcessRouter - } - - @type domain_event :: struct() - @type command :: struct() - @type process_manager :: struct() - @type process_uuid :: String.t() | [String.t()] - @type consistency :: :eventual | :strong - - @doc """ - Optional callback function called to configure the process manager before it - starts. - - It is passed the merged compile-time and runtime config, and must return the - updated config. - """ - @callback init(config :: Keyword.t()) :: {:ok, Keyword.t()} - - @doc """ - Is the process manager interested in the given event? - - See `c:interested?/2` for details. - """ - @callback interested?(domain_event) :: - {:start, process_uuid} - | {:start!, process_uuid} - | {:continue, process_uuid} - | {:continue!, process_uuid} - | {:stop, process_uuid} - | false - - @doc """ - Is the process manager interested in the given event? - - The `c:interested?/2` function is used to indicate which events - the process manager receives. The response is used to route the event to an existing - instance or start a new process instance: - - - `{:start, process_uuid}` - create a new instance of the process manager. - - `{:start!, process_uuid}` - create a new instance of the process manager (strict). - - `{:continue, process_uuid}` - continue execution of an existing process manager. - - `{:continue!, process_uuid}` - continue execution of an existing process manager (strict). - - `{:stop, process_uuid}` - stop an existing process manager, shutdown its - process, and delete its persisted state. - - `false` - ignore the event. - - You can return a list of process identifiers when a single domain event is to - be handled by multiple process instances. - - ## Strict process routing - - Using strict routing, with `:start!` or `:continue`, enforces the following - validation checks: - - - `{:start!, process_uuid}` - validate process does not already exist. - - `{:continue!, process_uuid}` - validate process already exists. - - If the check fails an error will be passed to the `error/3` callback function: - - - `{:error, {:start!, :process_already_started}}` - - `{:error, {:continue!, :process_not_started}}` - - The `error/3` function can choose to `:stop` the process or `:skip` the - problematic event. - - """ - @callback interested?(domain_event, EnrichedMetadata.t()) :: - {:start, process_uuid} - | {:start!, process_uuid} - | {:continue, process_uuid} - | {:continue!, process_uuid} - | {:stop, process_uuid} - | false - - @doc """ - Stop the process manager instance after a command is successfully - dispatched. - - See `c:after_command/3` for details. - """ - @callback after_command(process_manager, command) :: :continue | :stop - - @doc """ - Stop the process manager instance after a command is successfully - dispatched. - - The `c:after_command/3` function can be omitted if you do not need to stop - after a specific command or if you would instead use the `c:interested?/2` - stop mechanism. - """ - @callback after_command(process_manager, command, EnrichedMetadata.t()) :: :continue | :stop - - @doc """ - Process manager instance handles a domain event, returning any commands to - dispatch. - - See `c:handle/3` function for details. - """ - @callback handle(process_manager, domain_event) :: command | list(command) | {:error, term} - - @doc """ - Process manager instance handles a domain event, returning any commands to - dispatch. - - A `c:handle/3` function can be defined for each `:start` and `:continue` - tagged event previously specified. It receives the process manager's state, - event to be handled, and the event's enriched metadata. It must return the - commands to be dispatched. This may be none, a single command, or many - commands. - - The `c:handle/3` function can be omitted if you do not need to dispatch a - command and are only mutating the process manager's state. - """ - @callback handle(process_manager, domain_event, EnrichedMetadata.t()) :: - command | list(command) | {:error, term} - - @doc """ - Mutate the process manager's state by applying the domain event. - - See `c:apply/3` function for details. - """ - @callback apply(process_manager, domain_event) :: process_manager - - @doc """ - Mutate the process manager's state by applying the domain event. - - The `c:apply/3` function is used to mutate the process manager's state. It - receives the current state, the domain event and the event metadata, and must - return the modified state. - - This callback function is optional, the default behaviour is to retain the - process manager's current state. - """ - @callback apply(process_manager, domain_event, EnrichedMetadata.t()) :: process_manager - - @doc """ - Called when a command dispatch or event handling returns an error. - - The `c:error/3` function allows you to control how event handling and command - dispatch and failures are handled. The function is passed the error (e.g. - `{:error, :failure}`), the failed event (during failed event handling) or - failed command (during failed dispatch), and a failure context struct (see - `Commanded.ProcessManagers.FailureContext` for details). - - The failure context contains a context map you can use to pass transient state - between failures. For example it can be used to count the number of failures. - - You can return one of the following responses depending upon the - error severity: - - - `{:retry, context}` - retry the failed command, provide a context - map or `Commanded.ProcessManagers.FailureContext` struct, containing any - state passed to subsequent failures. This could be used to count the number - of retries, failing after too many attempts. - - - `{:retry, delay, context}` - retry the failed command, after sleeping for - the requested delay (in milliseconds). Context is a map or - `Commanded.ProcessManagers.FailureContext` as described in - `{:retry, context}` above. - - - `{:stop, reason}` - stop the process manager with the given reason. - - For event handling failures, when failure source is an event, you can also - return: - - - `:skip` - to skip the problematic event. No commands will be dispatched. - - For command dispatch failures, when failure source is a command, you can also - return: - - - `:skip` - skip the failed command and continue dispatching any pending - commands. - - - `{:skip, :continue_pending}` - skip the failed command, but continue - dispatching any pending commands. - - - `{:skip, :discard_pending}` - discard the failed command and any pending - commands. - - - `{:continue, commands, context}` - continue dispatching the given commands. - This allows you to retry the failed command, modify it and retry, drop it - or drop all pending commands by passing an empty list `[]`. Context is a map - as described in `{:retry, context}` above. - - """ - @callback error( - error :: {:error, term()}, - failure_source :: command | domain_event, - failure_context :: FailureContext.t() - ) :: - {:continue, commands :: list(command), context :: map()} - | {:retry, context :: map() | FailureContext.t()} - | {:retry, delay :: non_neg_integer(), context :: map() | FailureContext.t()} - | :skip - | {:skip, :discard_pending} - | {:skip, :continue_pending} - | {:stop, reason :: term()} - - @optional_callbacks init: 1, - handle: 2, - handle: 3, - apply: 2, - apply: 3, - error: 3, - interested?: 1, - interested?: 2, - after_command: 2, - after_command: 3 - - @doc false - defmacro __using__(using_opts) do - quote location: :keep do - @before_compile unquote(__MODULE__) - @behaviour ProcessManager - - def start_link(opts \\ []) do - opts = Keyword.merge(unquote(using_opts), opts) - - {application, name, config} = ProcessManager.parse_config!(__MODULE__, opts) - - ProcessRouter.start_link(application, name, __MODULE__, config) - end - - @doc """ - Provides a child specification to allow the event handler to be easily - supervised. - - ## Example - - Supervisor.start_link([ - {ExampleProcessManager, []} - ], strategy: :one_for_one) - - """ - def child_spec(opts) do - default = %{ - id: {__MODULE__, opts}, - start: {__MODULE__, :start_link, [opts]}, - restart: :permanent, - type: :worker - } - - Supervisor.child_spec(default, []) - end - - @doc false - def init(config), do: {:ok, config} - - defoverridable init: 1 - end - end - - @doc false - defmacro __before_compile__(_env) do - # Include default fallback functions at end, with lowest precedence - quote generated: true do - @doc false - def after_command(process_manager, command, _metadata), - do: after_command(process_manager, command) - - @doc false - def after_command(_process_manager, _command), do: :continue - - # @doc false - def interested?(event, _metadata), - do: interested?(event) - - @doc false - def interested?(_event), do: false - - @doc false - def handle(process_manager, event, _metadata), - do: handle(process_manager, event) - - @doc false - def handle(_process_manager, _event), do: [] - - @doc false - def apply(process_manager, event, _metadata), - do: __MODULE__.apply(process_manager, event) - - @doc false - def apply(process_manager, _event), do: process_manager - - @doc false - def error({:error, reason}, _command, _failure_context), do: {:stop, reason} - end - end - - @doc """ - Get the identity of the current process instance. - - This must only be called within a process manager's `handle/2`, `handle/3`, `apply/2`, or - `apply/3` callback functions. - - ## Example - - defmodule ExampleProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: MyApp.Application, - name: __MODULE__ - - def interested?(%ProcessStarted{uuids: uuids}), do: {:start, uuids} - - def handle(%IdentityProcessManager{}, %ProcessStarted{} = event) do - # Identify which uuid is associated with the current instance from the - # list of uuids in the event. - uuid = Commanded.ProcessManagers.ProcessManager.identity() - - # ... - end - end - - """ - defdelegate identity, to: ProcessManagerInstance - - # GenServer start options - @start_opts [:debug, :name, :timeout, :spawn_opt, :hibernate_after] - - # Process manager configuration options - @handler_opts [ - :application, - :name, - :consistency, - :start_from, - :subscribe_to, - :subscription_opts, - :event_timeout, - :idle_timeout - ] - - def parse_config!(module, config) do - {:ok, config} = module.init(config) - - {_valid, invalid} = Keyword.split(config, @start_opts ++ @handler_opts) - - if Enum.any?(invalid) do - raise ArgumentError, - inspect(module) <> " specifies invalid options: " <> inspect(Keyword.keys(invalid)) - end - - {application, config} = Keyword.pop(config, :application) - - unless application do - raise ArgumentError, inspect(module) <> " expects :application option" - end - - {name, config} = Keyword.pop(config, :name) - - name = parse_name(name) - - unless name do - raise ArgumentError, inspect(module) <> " expects :name option" - end - - {application, name, config} - end - - @doc false - def parse_name(name) when name in [nil, ""], do: nil - def parse_name(name) when is_binary(name), do: name - def parse_name(name), do: inspect(name) -end diff --git a/lib/commanded/process_managers/process_manager_instance.ex b/lib/commanded/process_managers/process_manager_instance.ex deleted file mode 100644 index 3a7f193d..00000000 --- a/lib/commanded/process_managers/process_manager_instance.ex +++ /dev/null @@ -1,649 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerInstance do - @moduledoc false - - use GenServer, restart: :temporary - - require Logger - - alias Commanded.{Application, EventStore, Telemetry} - alias Commanded.EventStore.{RecordedEvent, SnapshotData, TypeProvider} - alias Commanded.ProcessManagers.{FailureContext, ProcessRouter} - - defmodule State do - @moduledoc false - - defstruct [ - :application, - :idle_timeout, - :process_router, - :process_manager_name, - :process_manager_module, - :process_uuid, - :process_state, - :last_seen_event - ] - end - - def start_link(opts) do - process_manager_module = Keyword.fetch!(opts, :process_manager_module) - - state = %State{ - application: Keyword.fetch!(opts, :application), - idle_timeout: Keyword.fetch!(opts, :idle_timeout), - process_router: Keyword.fetch!(opts, :process_router), - process_manager_name: Keyword.fetch!(opts, :process_manager_name), - process_manager_module: process_manager_module, - process_uuid: Keyword.fetch!(opts, :process_uuid), - process_state: struct(process_manager_module) - } - - GenServer.start_link(__MODULE__, state) - end - - @doc """ - Checks whether or not the process manager has already processed events - """ - def new?(instance) do - GenServer.call(instance, :new?) - end - - @doc """ - Handle the given event by delegating to the process manager module - """ - def process_event(instance, %RecordedEvent{} = event) do - GenServer.cast(instance, {:process_event, event}) - end - - @doc """ - Stop the given process manager and delete its persisted state. - - Typically called when it has reached its final state. - """ - def stop(instance) do - GenServer.call(instance, :stop) - end - - @doc """ - Fetch the process state of this instance - """ - def process_state(instance) do - GenServer.call(instance, :process_state) - end - - @doc """ - Get the current process manager instance's identity. - """ - def identity, do: Process.get(:process_uuid) - - @doc false - @impl GenServer - def init(%State{} = state) do - {:ok, state, {:continue, :fetch_state}} - end - - @doc """ - Attempt to fetch initial process state from snapshot storage. - """ - @impl GenServer - def handle_continue(:fetch_state, %State{} = state) do - %State{application: application, process_uuid: process_uuid} = state - - state = - case EventStore.read_snapshot(application, snapshot_uuid(state)) do - {:ok, snapshot} -> - %SnapshotData{data: data, source_version: source_version} = snapshot - - %State{ - state - | process_state: data, - last_seen_event: source_version - } - - {:error, :snapshot_not_found} -> - state - end - - Process.put(:process_uuid, process_uuid) - - {:noreply, state} - end - - @doc false - @impl GenServer - def handle_call(:stop, _from, %State{} = state) do - :ok = delete_state(state) - - # Stop the process with a normal reason - {:stop, :normal, :ok, state} - end - - @doc false - @impl GenServer - def handle_call(:process_state, _from, %State{} = state) do - %State{idle_timeout: idle_timeout, process_state: process_state} = state - - {:reply, process_state, state, idle_timeout} - end - - @doc false - @impl GenServer - def handle_call(:new?, _from, %State{} = state) do - %State{idle_timeout: idle_timeout, last_seen_event: last_seen_event} = state - - {:reply, is_nil(last_seen_event), state, idle_timeout} - end - - @doc """ - Handle the given event, using the process manager module, against the current process state - """ - @impl GenServer - def handle_cast({:process_event, event}, %State{} = state) do - if event_already_seen?(event, state) do - process_seen_event(event, state) - else - process_unseen_event(event, state) - end - end - - @doc false - @impl GenServer - def handle_info(:timeout, %State{} = state) do - Logger.debug(fn -> describe(state) <> " stopping due to inactivity timeout" end) - - {:stop, :normal, state} - end - - @doc false - @impl GenServer - def handle_info(message, state) do - Logger.error(fn -> - describe(state) <> " received unexpected message: " <> inspect(message) - end) - - {:noreply, state} - end - - defp event_already_seen?(%RecordedEvent{}, %State{last_seen_event: nil}), - do: false - - defp event_already_seen?(%RecordedEvent{} = event, %State{} = state) do - %RecordedEvent{event_number: event_number} = event - %State{last_seen_event: last_seen_event} = state - - event_number <= last_seen_event - end - - # Already seen event, so just ack. - defp process_seen_event(%RecordedEvent{} = event, %State{} = state) do - %State{idle_timeout: idle_timeout} = state - - :ok = ack_event(event, state) - - {:noreply, state, idle_timeout} - end - - defp process_unseen_event(%RecordedEvent{} = event, %State{} = state, context \\ %{}) do - %RecordedEvent{ - correlation_id: correlation_id, - event_id: event_id, - event_number: event_number, - metadata: metadata - } = event - - %State{process_state: process_state} = state - - telemetry_metadata = telemetry_metadata(event, state) - start_time = telemetry_start(telemetry_metadata) - - case handle_event(event, state) do - {:error, error} -> - failure_context = %FailureContext{ - context: context, - enriched_metadata: enrich_metadata(event, state), - last_event: event, - process_manager_state: process_state - } - - telemetry_stop(start_time, telemetry_metadata, {:error, error}) - - handle_event_error({:error, error}, event, failure_context, state) - - {:error, error, stacktrace} -> - failure_context = %FailureContext{ - context: context, - enriched_metadata: enrich_metadata(event, state), - last_event: event, - process_manager_state: process_state, - stacktrace: stacktrace - } - - telemetry_stop(start_time, telemetry_metadata, {:error, error, stacktrace}) - - handle_event_error({:error, error}, event, failure_context, state) - - commands -> - commands = List.wrap(commands) - - # Copy event id, as causation id, and correlation id from handled event. - opts = [ - causation_id: event_id, - correlation_id: correlation_id, - metadata: metadata || %{}, - returning: false - ] - - with :ok <- dispatch_commands(commands, opts, state, event) do - telemetry_stop(start_time, telemetry_metadata, {:ok, commands}) - - case mutate_state(event, state) do - {:error, error, stacktrace} -> - failure_context = %FailureContext{ - context: context, - enriched_metadata: enrich_metadata(event, state), - last_event: event, - process_manager_state: process_state, - stacktrace: stacktrace - } - - handle_event_error({:error, error}, event, failure_context, state) - - updated_process_state -> - state = %State{ - state - | process_state: updated_process_state, - last_seen_event: event_number - } - - :ok = persist_state(event_number, state) - :ok = ack_event(event, state) - - enriched_metadata = enrich_metadata(event, state) - - handle_after_command(commands, enriched_metadata, state) - end - else - {:stop, reason} -> - telemetry_stop(start_time, telemetry_metadata, {:error, reason}) - - {:stop, reason, state} - end - end - end - - # Process instance is given the event and returns applicable commands - # (may be none, one or many). - defp handle_event(%RecordedEvent{} = event, %State{} = state) do - %RecordedEvent{data: data} = event - - %State{ - process_manager_module: process_manager_module, - process_state: process_state - } = state - - enriched_metadata = enrich_metadata(event, state) - - try do - process_manager_module.handle(process_state, data, enriched_metadata) - rescue - error -> - stacktrace = __STACKTRACE__ - Logger.error(fn -> Exception.format(:error, error, stacktrace) end) - - {:error, error, stacktrace} - end - end - - defp handle_event_error( - {:error, _error} = error, - %RecordedEvent{} = failed_event, - %FailureContext{} = failure_context, - %State{} = state - ) do - %RecordedEvent{data: data} = failed_event - %State{idle_timeout: idle_timeout, process_manager_module: process_manager_module} = state - - log_event_error(error, failed_event, state) - - case process_manager_module.error(error, data, failure_context) do - {:retry, %FailureContext{context: context}} when is_map(context) -> - # Retry the failed event - Logger.info(fn -> describe(state) <> " is retrying failed event" end) - - process_unseen_event(failed_event, state, context) - - {:retry, context} when is_map(context) -> - # Retry the failed event - Logger.info(fn -> describe(state) <> " is retrying failed event" end) - - process_unseen_event(failed_event, state, context) - - {:retry, delay, %FailureContext{context: context}} - when is_map(context) and is_integer(delay) and delay >= 0 -> - # Retry the failed event after waiting for the given delay (milliseconds) - Logger.info(fn -> - describe(state) <> " is retrying failed event after #{inspect(delay)}ms" - end) - - :timer.sleep(delay) - - process_unseen_event(failed_event, state, context) - - {:retry, delay, context} when is_map(context) and is_integer(delay) and delay >= 0 -> - # Retry the failed event after waiting for the given delay (milliseconds) - Logger.info(fn -> - describe(state) <> " is retrying failed event after #{inspect(delay)}ms" - end) - - :timer.sleep(delay) - - process_unseen_event(failed_event, state, context) - - :skip -> - # Skip the failed event by confirming receipt - Logger.info(fn -> describe(state) <> " is skipping event" end) - - :ok = ack_event(failed_event, state) - - {:noreply, state, idle_timeout} - - {:stop, error} -> - # Stop the process manager instance - Logger.warning(fn -> describe(state) <> " has requested to stop: #{inspect(error)}" end) - - {:stop, error, state} - - invalid -> - Logger.warning(fn -> - describe(state) <> " returned an invalid error response: #{inspect(invalid)}" - end) - - # Stop process manager with original error - {:stop, error, state} - end - end - - defp log_event_error({:error, reason}, %RecordedEvent{} = failed_event, %State{} = state) do - Logger.error(fn -> - describe(state) <> - " failed to handle event " <> - inspect(failed_event, pretty: true) <> - " due to: " <> - inspect(reason, pretty: true) - end) - end - - defp handle_after_command([], _metadata, %State{} = state) do - %State{idle_timeout: idle_timeout} = state - - {:noreply, state, idle_timeout} - end - - defp handle_after_command([command | commands], metadata, %State{} = state) do - %State{ - process_manager_module: process_manager_module, - process_state: process_state - } = state - - case process_manager_module.after_command(process_state, command, metadata) do - :stop -> - Logger.debug(fn -> - describe(state) <> " has been stopped by command " <> inspect(command) - end) - - :ok = delete_state(state) - - {:stop, :normal, state} - - _ -> - handle_after_command(commands, metadata, state) - end - end - - # Update the process instance's state by applying the event. - defp mutate_state(%RecordedEvent{} = event, %State{} = state) do - %RecordedEvent{data: data} = event - - %State{ - process_manager_module: process_manager_module, - process_state: process_state - } = state - - enriched_metadata = enrich_metadata(event, state) - - try do - process_manager_module.apply(process_state, data, enriched_metadata) - rescue - error -> - stacktrace = __STACKTRACE__ - Logger.error(fn -> Exception.format(:error, error, stacktrace) end) - - {:error, error, stacktrace} - end - end - - defp dispatch_commands(commands, opts, state, last_event, context \\ %{}) - defp dispatch_commands([], _opts, _state, _last_event, _context), do: :ok - - defp dispatch_commands([command | pending_commands], opts, state, last_event, context) do - %State{application: application, process_state: initial_process_state} = state - - Logger.debug(describe(state) <> " attempting to dispatch command: " <> inspect(command)) - - case Application.dispatch(application, command, opts) do - :ok -> - dispatch_commands(pending_commands, opts, state, last_event) - - {:error, _error} = error -> - Logger.warning( - describe(state) <> - " failed to dispatch command " <> inspect(command) <> " due to: " <> inspect(error) - ) - - process_manager_state = - case mutate_state(last_event, state) do - {:error, _, _} -> initial_process_state - updated_manager_state -> updated_manager_state - end - - enriched_metadata = enrich_metadata(last_event, state) - - failure_context = %FailureContext{ - context: context, - enriched_metadata: enriched_metadata, - last_event: last_event, - pending_commands: pending_commands, - process_manager_state: process_manager_state - } - - dispatch_failure(error, command, opts, failure_context, state) - end - end - - defp dispatch_failure({:error, reason} = error, failed_command, opts, failure_context, state) do - %State{process_manager_module: process_manager_module} = state - %FailureContext{pending_commands: pending_commands, last_event: last_event} = failure_context - - case process_manager_module.error(error, failed_command, failure_context) do - {:continue, commands, %FailureContext{context: context}} - when is_list(commands) and is_map(context) -> - # Continue dispatching the given commands - Logger.info(fn -> describe(state) <> " is continuing with modified command(s)" end) - - dispatch_commands(commands, opts, state, last_event, context) - - {:continue, commands, context} when is_list(commands) and is_map(context) -> - # Continue dispatching the given commands - Logger.info(fn -> describe(state) <> " is continuing with modified command(s)" end) - - dispatch_commands(commands, opts, state, last_event, context) - - {:retry, %FailureContext{context: context}} when is_map(context) -> - # Retry the failed command immediately - Logger.info(fn -> describe(state) <> " is retrying failed command" end) - - dispatch_commands([failed_command | pending_commands], opts, state, last_event, context) - - {:retry, context} when is_map(context) -> - # Retry the failed command immediately - Logger.info(fn -> describe(state) <> " is retrying failed command" end) - - dispatch_commands([failed_command | pending_commands], opts, state, last_event, context) - - {:retry, delay, %FailureContext{context: context}} - when is_map(context) and is_integer(delay) and delay >= 0 -> - # retry the failed command after waiting for the given delay, in milliseconds - Logger.info(fn -> - describe(state) <> " is retrying failed command after #{inspect(delay)}ms" - end) - - :timer.sleep(delay) - - dispatch_commands([failed_command | pending_commands], opts, state, last_event, context) - - {:retry, delay, context} when is_map(context) and is_integer(delay) and delay >= 0 -> - # retry the failed command after waiting for the given delay, in milliseconds - Logger.info(fn -> - describe(state) <> " is retrying failed command after #{inspect(delay)}ms" - end) - - :timer.sleep(delay) - - dispatch_commands([failed_command | pending_commands], opts, state, last_event, context) - - :skip -> - # Skip the failed command, but continue dispatching any pending commands - Logger.info(fn -> describe(state) <> " is ignoring error dispatching command" end) - - dispatch_commands(pending_commands, opts, state, last_event) - - {:skip, :continue_pending} -> - # Skip the failed command, but continue dispatching any pending commands - Logger.info(fn -> describe(state) <> " is ignoring error dispatching command" end) - - dispatch_commands(pending_commands, opts, state, last_event) - - {:skip, :discard_pending} -> - # Skip the failed command and discard any pending commands - Logger.info(fn -> - describe(state) <> - " is skipping event and #{length(pending_commands)} pending command(s)" - end) - - :ok - - {:stop, reason} = reply -> - # Stop process manager - Logger.warning(fn -> describe(state) <> " has requested to stop: #{inspect(reason)}" end) - - reply - - invalid -> - Logger.warning(fn -> - describe(state) <> " returned an invalid error response: #{inspect(invalid)}" - end) - - # Stop process manager with original error reason - {:stop, reason} - end - end - - defp describe(%State{process_manager_module: process_manager_module}), - do: inspect(process_manager_module) - - defp persist_state(source_version, %State{} = state) do - %State{ - application: application, - process_state: process_state - } = state - - snapshot = %SnapshotData{ - source_uuid: snapshot_uuid(state), - source_version: source_version, - source_type: TypeProvider.to_string(process_state), - data: process_state - } - - EventStore.record_snapshot(application, snapshot) - end - - defp delete_state(%State{} = state) do - %State{application: application} = state - - EventStore.delete_snapshot(application, snapshot_uuid(state)) - end - - defp ack_event(%RecordedEvent{} = event, %State{} = state) do - %State{process_router: process_router} = state - - ProcessRouter.ack_event(process_router, event, self()) - end - - defp snapshot_uuid(%State{} = state) do - %State{process_manager_name: process_manager_name, process_uuid: process_uuid} = state - - inspect(process_manager_name) <> "-" <> inspect(process_uuid) - end - - defp enrich_metadata(%RecordedEvent{} = event, %State{} = state) do - %State{application: application} = state - - RecordedEvent.enrich_metadata(event, - additional_metadata: %{ - application: application - } - ) - end - - defp telemetry_start(telemetry_metadata) do - Telemetry.start([:commanded, :process_manager, :handle], telemetry_metadata) - end - - defp telemetry_stop(start_time, telemetry_metadata, handle_result) do - event_prefix = [:commanded, :process_manager, :handle] - - case handle_result do - {:ok, commands} -> - telemetry_metadata = - telemetry_metadata |> Map.put(:commands, commands) |> Map.put(:error, nil) - - Telemetry.stop(event_prefix, start_time, telemetry_metadata) - - {:error, error} -> - telemetry_metadata = - telemetry_metadata - |> Map.put(:error, error) - |> Map.put_new(:commands, []) - - Telemetry.stop(event_prefix, start_time, telemetry_metadata) - - {:error, error, stacktrace} -> - Telemetry.exception( - event_prefix, - start_time, - :error, - error, - stacktrace, - telemetry_metadata - ) - end - end - - defp telemetry_metadata(%RecordedEvent{} = event, %State{} = state) do - %State{ - application: application, - process_manager_name: process_manager_name, - process_manager_module: process_manager_module, - process_state: process_state, - process_uuid: process_uuid - } = state - - %{ - application: application, - process_manager_name: process_manager_name, - process_manager_module: process_manager_module, - process_state: process_state, - process_uuid: process_uuid, - recorded_event: event - } - end -end diff --git a/lib/commanded/process_managers/process_router.ex b/lib/commanded/process_managers/process_router.ex deleted file mode 100644 index cde836ed..00000000 --- a/lib/commanded/process_managers/process_router.ex +++ /dev/null @@ -1,589 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessRouter do - @moduledoc false - - use GenServer - use Commanded.Registration - - require Logger - - alias Commanded.Event.Upcast - alias Commanded.EventStore.RecordedEvent - alias Commanded.EventStore.Subscription - alias Commanded.ProcessManagers.FailureContext - alias Commanded.ProcessManagers.ProcessManagerInstance - alias Commanded.ProcessManagers.Supervisor - alias Commanded.Subscriptions - - defmodule State do - @moduledoc false - - defstruct [ - :application, - :consistency, - :event_timeout, - :idle_timeout, - :process_manager_name, - :process_manager_module, - :supervisor, - :subscription, - :subscribe_timer, - :last_seen_event, - :process_event_timer, - process_managers: %{}, - pending_acks: %{}, - pending_events: [] - ] - end - - def start_link(application, process_name, process_module, opts \\ []) do - {start_opts, router_opts} = - Keyword.split(opts, [:debug, :name, :timeout, :spawn_opt, :hibernate_after]) - - name = name(application, process_name) - consistency = Keyword.get(router_opts, :consistency, :eventual) - - subscription = - Subscription.new( - application: application, - subscription_name: process_name, - subscribe_from: Keyword.get(router_opts, :start_from, :origin), - subscribe_to: Keyword.get(router_opts, :subscribe_to, :all), - subscription_opts: Keyword.get(router_opts, :subscription_opts, []) - ) - - state = %State{ - application: application, - process_manager_name: process_name, - process_manager_module: process_module, - consistency: consistency, - subscription: subscription, - event_timeout: Keyword.get(router_opts, :event_timeout), - idle_timeout: Keyword.get(router_opts, :idle_timeout, :infinity) - } - - with {:ok, pid} <- Registration.start_link(application, name, __MODULE__, state, start_opts) do - # Register the process manager as a subscription with the given consistency. - :ok = Subscriptions.register(application, process_name, process_module, pid, consistency) - - {:ok, pid} - end - end - - @doc false - def name(application, process_name), do: {application, __MODULE__, process_name} - - # Acknowledge successful handling of the given event by a process manager instance. - def ack_event(process_router, %RecordedEvent{} = event, instance) do - GenServer.cast(process_router, {:ack_event, event, instance}) - end - - def process_instance(process_router, process_uuid) do - GenServer.call(process_router, {:process_instance, process_uuid}) - end - - def process_instances(process_router) do - GenServer.call(process_router, :process_instances) - end - - @impl GenServer - def init(%State{} = state) do - {:ok, state, {:continue, :subscribe_to_events}} - end - - @impl GenServer - def handle_continue(:subscribe_to_events, %State{} = state) do - {:noreply, subscribe_to_events(state)} - end - - @impl GenServer - def handle_call(:process_instances, _from, %State{} = state) do - %State{process_managers: process_managers} = state - - reply = Enum.map(process_managers, fn {process_uuid, pid} -> {process_uuid, pid} end) - - {:reply, reply, state} - end - - @impl GenServer - def handle_call({:process_instance, process_uuid}, _from, %State{} = state) do - reply = get_process_manager(state, process_uuid) - - {:reply, reply, state} - end - - @impl GenServer - def handle_cast({:ack_event, event, instance}, %State{} = state) do - %State{pending_acks: pending_acks} = state - %RecordedEvent{event_number: event_number} = event - - state = - case pending_acks |> Map.get(event_number, []) |> List.delete(instance) do - [] -> - # Enqueue a message to continue processing any pending events - GenServer.cast(self(), :process_pending_events) - - state = %State{state | pending_acks: Map.delete(pending_acks, event_number)} - - # no pending acks so confirm receipt of event - confirm_receipt(event, state) - - pending -> - # pending acks, don't ack event but wait for outstanding instances - %State{state | pending_acks: Map.put(pending_acks, event_number, pending)} - end - - {:noreply, state} - end - - @impl GenServer - def handle_cast(:process_pending_events, %State{pending_events: []} = state), - do: {:noreply, state} - - @impl GenServer - def handle_cast(:process_pending_events, %State{} = state) do - %State{pending_events: [event | pending_events]} = state - - case length(pending_events) do - 0 -> - :ok - - 1 -> - Logger.debug(fn -> describe(state) <> " has 1 pending event to process" end) - - count -> - Logger.debug(fn -> describe(state) <> " has #{count} pending events to process" end) - end - - case handle_event(event, state) do - %State{} = state -> {:noreply, %State{state | pending_events: pending_events}} - reply -> reply - end - end - - @doc false - @impl GenServer - def handle_info(:subscribe_to_events, %State{} = state) do - {:noreply, subscribe_to_events(state)} - end - - @doc false - # Subscription to event store has successfully subscribed, init process router - @impl GenServer - def handle_info( - {:subscribed, subscription}, - %State{subscription: %Subscription{subscription_pid: subscription}} = state - ) do - Logger.debug(fn -> describe(state) <> " has successfully subscribed to event store" end) - - {:ok, supervisor} = Supervisor.start_link() - - {:noreply, %State{state | supervisor: supervisor}} - end - - @impl GenServer - def handle_info({:events, events}, %State{} = state) do - %State{application: application, pending_events: pending_events} = state - - Logger.debug(fn -> describe(state) <> " received #{length(events)} event(s)" end) - - # Exclude already seen events - unseen_events = - events - |> Enum.reject(&event_already_seen?(&1, state)) - |> Upcast.upcast_event_stream(additional_metadata: %{application: application}) - - state = - case {pending_events, unseen_events} do - {[], []} -> - # No pending or unseen events, so state is unmodified - state - - {[], _} -> - # No pending events, but some unseen events so start processing them - GenServer.cast(self(), :process_pending_events) - - %State{state | pending_events: unseen_events} - - {_, _} -> - # Already processing pending events, append the unseen events so they are processed afterwards - %State{state | pending_events: pending_events ++ unseen_events} - end - - {:noreply, state} - end - - # Shutdown process manager when processing an event has taken too long. - @impl GenServer - def handle_info({:event_timeout, event_number}, %State{} = state) do - %State{pending_acks: pending_acks, event_timeout: event_timeout} = state - - case Map.get(pending_acks, event_number, []) do - [] -> - {:noreply, state} - - _pending -> - Logger.error(fn -> - describe(state) <> - " has taken longer than " <> - inspect(event_timeout) <> - "ms to process event #" <> inspect(event_number) <> " and is now stopping" - end) - - {:stop, :event_timeout, state} - end - end - - # Stop process manager when event store subscription process terminates. - @impl GenServer - def handle_info( - {:DOWN, ref, :process, _pid, reason}, - %State{subscription: %Subscription{subscription_ref: ref}} = state - ) do - Logger.debug(fn -> describe(state) <> " subscription DOWN due to: #{inspect(reason)}" end) - - {:stop, reason, state} - end - - # Remove a process manager instance that has stopped with a normal exit reason. - @impl GenServer - def handle_info({:DOWN, _ref, :process, pid, :normal}, %State{} = state) do - %State{process_managers: process_managers} = state - - state = %State{state | process_managers: remove_process_manager(process_managers, pid)} - - {:noreply, state} - end - - # Stop process router when a process manager instance terminates abnormally. - @impl GenServer - def handle_info({:DOWN, _ref, :process, _pid, reason}, %State{} = state) do - Logger.warning(fn -> describe(state) <> " is stopping due to: #{inspect(reason)}" end) - - {:stop, reason, state} - end - - defp subscribe_to_events(%State{} = state) do - %State{subscription: subscription} = state - - case Subscription.subscribe(subscription, self()) do - {:ok, subscription} -> - %State{state | subscription: subscription, subscribe_timer: nil} - - {:error, error} -> - {backoff, subscription} = Subscription.backoff(subscription) - - Logger.info(fn -> - describe(state) <> - " failed to subscribe to event store due to: " <> - inspect(error) <> ", retrying in " <> inspect(backoff) <> "ms" - end) - - subscribe_timer = Process.send_after(self(), :subscribe_to_events, backoff) - - %State{state | subscription: subscription, subscribe_timer: subscribe_timer} - end - end - - defp event_already_seen?( - %RecordedEvent{event_number: event_number}, - %State{last_seen_event: last_seen_event} - ) do - not is_nil(last_seen_event) and event_number <= last_seen_event - end - - defp handle_event(%RecordedEvent{} = event, %State{} = state) do - %RecordedEvent{data: data} = event - %State{process_manager_module: process_manager_module} = state - - additional_metadata = Map.take(state, [:application]) - - enriched_metadata = - RecordedEvent.enrich_metadata(event, additional_metadata: additional_metadata) - - try do - case process_manager_module.interested?(data, enriched_metadata) do - {:start, []} -> - ack_and_continue(event, state) - - {:start, process_uuid} -> - Logger.debug(fn -> describe(state) <> " is interested in event " <> describe(event) end) - - process_uuid - |> List.wrap() - |> Enum.reduce(state, fn process_uuid, state -> - {process_instance, state} = start_or_continue_process_manager(process_uuid, state) - - delegate_event(process_instance, event, state) - end) - - {:start!, []} -> - ack_and_continue(event, state) - - {:start!, process_uuid} -> - Logger.debug(fn -> describe(state) <> " is interested in event " <> describe(event) end) - - {state, process_instances} = - process_uuid - |> List.wrap() - |> Enum.reduce({state, []}, fn process_uuid, {state, process_instances} -> - {process_instance, state} = start_or_continue_process_manager(process_uuid, state) - - if ProcessManagerInstance.new?(process_instance) do - {state, [process_instance | process_instances]} - else - error = {:error, {:start!, :process_already_started}} - reply = handle_routing_error(error, event, state) - - throw(reply) - end - end) - - process_instances - |> Enum.reverse() - |> Enum.reduce(state, &delegate_event(&1, event, &2)) - - {:continue, []} -> - ack_and_continue(event, state) - - {:continue, process_uuid} -> - Logger.debug(fn -> describe(state) <> " is interested in event " <> describe(event) end) - - process_uuid - |> List.wrap() - |> Enum.reduce(state, fn process_uuid, state -> - {process_instance, state} = start_or_continue_process_manager(process_uuid, state) - - delegate_event(process_instance, event, state) - end) - - {:continue!, []} -> - ack_and_continue(event, state) - - {:continue!, process_uuid} -> - Logger.debug(fn -> describe(state) <> " is interested in event " <> describe(event) end) - - {state, process_instances} = - process_uuid - |> List.wrap() - |> Enum.reduce({state, []}, fn process_uuid, {state, process_instances} -> - {process_instance, state} = start_or_continue_process_manager(process_uuid, state) - - if ProcessManagerInstance.new?(process_instance) do - error = {:error, {:continue!, :process_not_started}} - reply = handle_routing_error(error, event, state) - - throw(reply) - else - {state, [process_instance | process_instances]} - end - end) - - process_instances - |> Enum.reverse() - |> Enum.reduce(state, &delegate_event(&1, event, &2)) - - {:stop, process_uuid} -> - Logger.debug(fn -> - describe(state) <> " has been stopped by event " <> describe(event) - end) - - state = - process_uuid - |> List.wrap() - |> Enum.reduce(state, &stop_process_manager/2) - - ack_and_continue(event, state) - - false -> - Logger.debug(fn -> - describe(state) <> " is not interested in event " <> describe(event) - end) - - ack_and_continue(event, state) - end - rescue - e -> handle_routing_error({:error, e}, event, state) - catch - reply -> reply - end - end - - defp handle_routing_error(error, %RecordedEvent{} = failed_event, %State{} = state) do - %RecordedEvent{data: data} = failed_event - %State{process_manager_module: process_manager_module} = state - - failure_context = %FailureContext{last_event: failed_event} - - case process_manager_module.error(error, data, failure_context) do - :skip -> - # Skip the problematic event by confirming receipt - Logger.info(fn -> describe(state) <> " is skipping event" end) - - ack_and_continue(failed_event, state) - - {:stop, reason} -> - Logger.warning(fn -> describe(state) <> " has requested to stop: #{inspect(error)}" end) - - {:stop, reason, state} - - invalid -> - Logger.warning(fn -> - describe(state) <> " returned an invalid error response: #{inspect(invalid)}" - end) - - {:stop, error, state} - end - end - - # Continue processing any pending events and confirm receipt of the given event id - defp ack_and_continue(%RecordedEvent{} = event, %State{} = state) do - GenServer.cast(self(), :process_pending_events) - - confirm_receipt(event, state) - end - - # Confirm receipt of given event - defp confirm_receipt(%RecordedEvent{event_number: event_number} = event, %State{} = state) do - %State{ - application: application, - consistency: consistency, - process_manager_name: name, - subscription: subscription - } = state - - Logger.debug(fn -> - describe(state) <> " confirming receipt of event: #{inspect(event_number)}" - end) - - :ok = Subscription.ack_event(subscription, event) - :ok = Subscriptions.ack_event(application, name, consistency, event) - - %State{state | last_seen_event: event_number} - end - - defp start_or_continue_process_manager(process_uuid, %State{} = state) do - case get_process_manager(state, process_uuid) do - {:ok, process_manager} -> - {process_manager, state} - - {:error, :process_manager_not_found} -> - start_process_manager(process_uuid, state) - end - end - - defp start_process_manager(process_uuid, %State{} = state) do - %State{ - application: application, - idle_timeout: idle_timeout, - process_managers: process_managers, - process_manager_name: process_manager_name, - process_manager_module: process_manager_module, - supervisor: supervisor - } = state - - opts = [ - application: application, - idle_timeout: idle_timeout, - process_manager_name: process_manager_name, - process_manager_module: process_manager_module, - process_router: self(), - process_uuid: process_uuid - ] - - {:ok, process_manager} = Supervisor.start_process_manager(supervisor, opts) - - _ref = Process.monitor(process_manager) - - state = %State{ - state - | process_managers: Map.put(process_managers, process_uuid, process_manager) - } - - {process_manager, state} - end - - defp stop_process_manager(process_uuid, %State{} = state) do - %State{process_managers: process_managers} = state - - case get_process_manager(state, process_uuid) do - {:ok, process_manager} -> - :ok = ProcessManagerInstance.stop(process_manager) - - %State{state | process_managers: Map.delete(process_managers, process_uuid)} - - {:error, :process_manager_not_found} -> - state - end - end - - defp remove_process_manager(process_managers, pid) do - Enum.reduce(process_managers, process_managers, fn - {process_uuid, process_manager_pid}, acc when process_manager_pid == pid -> - Map.delete(acc, process_uuid) - - _, acc -> - acc - end) - end - - defp get_process_manager(%State{} = state, process_uuid) do - %State{process_managers: process_managers} = state - - case Map.get(process_managers, process_uuid) do - process_manager when is_pid(process_manager) -> {:ok, process_manager} - nil -> {:error, :process_manager_not_found} - end - end - - # Delegate event to process instance who will ack event processing on success - defp delegate_event(process_instance, %RecordedEvent{} = event, %State{} = state) do - %State{pending_acks: pending_acks} = state - %RecordedEvent{event_number: event_number} = event - - :ok = ProcessManagerInstance.process_event(process_instance, event) - - pending_acks = - Map.update(pending_acks, event_number, [process_instance], fn - pending -> [process_instance | pending] - end) - - state = %State{state | pending_acks: pending_acks} - - start_event_timer(event_number, state) - end - - # Event timeout not configured - defp start_event_timer(_event_number, %State{event_timeout: nil} = state), do: state - - defp start_event_timer(event_number, %State{process_event_timer: process_event_timer} = state) - when is_reference(process_event_timer) do - Process.cancel_timer(process_event_timer) - - state = %State{state | process_event_timer: nil} - - start_event_timer(event_number, state) - end - - defp start_event_timer(event_number, %State{event_timeout: event_timeout} = state) - when is_integer(event_timeout) do - %State{event_timeout: event_timeout} = state - - process_event_timer = - Process.send_after(self(), {:event_timeout, event_number}, event_timeout) - - %State{state | process_event_timer: process_event_timer} - end - - defp describe(%State{process_manager_module: process_manager_module}), - do: inspect(process_manager_module) - - defp describe(%RecordedEvent{} = event) do - %RecordedEvent{ - event_number: event_number, - stream_id: stream_id, - stream_version: stream_version - } = event - - "#{inspect(event_number)} (#{inspect(stream_id)}@#{inspect(stream_version)})" - end -end diff --git a/lib/commanded/process_managers/supervisor.ex b/lib/commanded/process_managers/supervisor.ex deleted file mode 100644 index e0b547ae..00000000 --- a/lib/commanded/process_managers/supervisor.ex +++ /dev/null @@ -1,20 +0,0 @@ -defmodule Commanded.ProcessManagers.Supervisor do - @moduledoc false - - use DynamicSupervisor - - alias Commanded.ProcessManagers.ProcessManagerInstance - - def start_link do - DynamicSupervisor.start_link(__MODULE__, []) - end - - def start_process_manager(supervisor, opts) do - DynamicSupervisor.start_child(supervisor, {ProcessManagerInstance, opts}) - end - - @impl true - def init(_init_arg) do - DynamicSupervisor.init(strategy: :one_for_one) - end -end diff --git a/mix.exs b/mix.exs index 77141517..a0a73720 100644 --- a/mix.exs +++ b/mix.exs @@ -54,7 +54,6 @@ defmodule Commanded.Mixfile do "test/example_domain", "test/middleware/support", "test/helpers", - "test/process_managers/support", "test/pubsub/support", "test/registration/support", "test/subscriptions/support", @@ -135,10 +134,6 @@ defmodule Commanded.Mixfile do Commanded.Event.Upcaster, Commanded.Event.EventId ], - "Process Managers": [ - Commanded.ProcessManagers.FailureContext, - Commanded.ProcessManagers.ProcessManager - ], "Event Store": [ Commanded.EventStore, Commanded.EventStore.Adapter, @@ -185,7 +180,6 @@ defmodule Commanded.Mixfile do Commanded.Aggregates, Commanded.Commands, Commanded.Event, - Commanded.ProcessManagers, Commanded.EventStore, Commanded.PubSub, Commanded.Registration, diff --git a/test/commands/correlation_causation_test.exs b/test/commands/correlation_causation_test.exs index a51c1107..5a92f0bd 100644 --- a/test/commands/correlation_causation_test.exs +++ b/test/commands/correlation_causation_test.exs @@ -9,8 +9,6 @@ defmodule Commanded.Commands.CorrelationCasuationTest do alias Commanded.ExampleDomain.BankAccount.Commands.{OpenAccount, WithdrawMoney} alias Commanded.ExampleDomain.BankAccount.Events.MoneyDeposited alias Commanded.ExampleDomain.BankRouter - alias Commanded.ExampleDomain.MoneyTransfer.Commands.TransferMoney - alias Commanded.ExampleDomain.TransferMoneyProcessManager alias Commanded.Helpers.CommandAuditMiddleware alias Commanded.Helpers.ProcessHelper alias Commanded.UUID @@ -18,7 +16,6 @@ defmodule Commanded.Commands.CorrelationCasuationTest do setup do start_supervised!(CommandAuditMiddleware) start_supervised!(BankApp) - start_supervised!(TransferMoneyProcessManager) :ok end @@ -53,42 +50,6 @@ defmodule Commanded.Commands.CorrelationCasuationTest do # an event's `causation_id` is the dispatched command's `command_uuid` assert event.causation_id == command_uuid end - - test "should be copied onto commands/events by process manager" do - transfer_uuid = UUID.uuid4() - - :ok = - BankRouter.dispatch(%OpenAccount{account_number: "ACC123", initial_balance: 500}, - application: BankApp - ) - - :ok = - BankRouter.dispatch(%OpenAccount{account_number: "ACC456", initial_balance: 100}, - application: BankApp - ) - - CommandAuditMiddleware.reset() - - :ok = - BankRouter.dispatch( - %TransferMoney{ - transfer_uuid: transfer_uuid, - debit_account: "ACC123", - credit_account: "ACC456", - amount: 100 - }, - application: BankApp - ) - - assert_receive_event(BankApp, MoneyDeposited, fn event -> - assert event.transfer_uuid == transfer_uuid - end) - - # withdraw money command's `causation_id` should be money transfer requested event's id - transfer_requested = EventStore.stream_forward(BankApp, transfer_uuid) |> Enum.at(0) - [_, causation_id, _] = CommandAuditMiddleware.dispatched_commands(& &1.causation_id) - assert causation_id == transfer_requested.event_id - end end describe "`correlation_id`" do @@ -132,46 +93,6 @@ defmodule Commanded.Commands.CorrelationCasuationTest do assert event.correlation_id == correlation_id end end - - test "should be copied onto commands/events by process manager" do - correlation_id = UUID.uuid4() - transfer_uuid = UUID.uuid4() - - :ok = - BankRouter.dispatch(%OpenAccount{account_number: "ACC123", initial_balance: 500}, - application: BankApp - ) - - :ok = - BankRouter.dispatch(%OpenAccount{account_number: "ACC456", initial_balance: 100}, - application: BankApp - ) - - CommandAuditMiddleware.reset() - - :ok = - BankRouter.dispatch( - %TransferMoney{ - transfer_uuid: transfer_uuid, - debit_account: "ACC123", - credit_account: "ACC456", - amount: 100 - }, - application: BankApp, - correlation_id: correlation_id - ) - - assert_receive_event(BankApp, MoneyDeposited, fn event -> - assert event.transfer_uuid == transfer_uuid - end) - - # `correlation_id` should be the same for all commands & events related to money transfer - assert [correlation_id, correlation_id, correlation_id] = - CommandAuditMiddleware.dispatched_commands(& &1.correlation_id) - - event = EventStore.stream_forward(BankApp, transfer_uuid) |> Enum.at(0) - assert event.correlation_id == correlation_id - end end describe "event handler dispatch command" do diff --git a/test/commands/dispatch_consistency_test.exs b/test/commands/dispatch_consistency_test.exs index e360ae9a..00986291 100644 --- a/test/commands/dispatch_consistency_test.exs +++ b/test/commands/dispatch_consistency_test.exs @@ -7,8 +7,7 @@ defmodule Commanded.Commands.DispatchConsistencyTest do EventuallyConsistentEventHandler, ExecutionResult, OptionalStronglyConsistentEventHandler, - StronglyConsistentEventHandler, - StronglyConsistentProcessManager + StronglyConsistentEventHandler } alias Commanded.Commands.ConsistencyApp @@ -117,16 +116,6 @@ defmodule Commanded.Commands.DispatchConsistencyTest do end end - describe "process manager consistency" do - setup :start_process_manager - - test "should successfully dispatch command" do - command = %RequestDispatchCommand{uuid: UUID.uuid4(), delay: 5_000} - - assert :ok = ConsistencyApp.dispatch(command, consistency: :strong) - end - end - def start_event_handlers(_context) do start_supervised!(StronglyConsistentEventHandler, shutdown: :brutal_kill) start_supervised!(EventuallyConsistentEventHandler, shutdown: :brutal_kill) @@ -139,10 +128,4 @@ defmodule Commanded.Commands.DispatchConsistencyTest do :ok end - - def start_process_manager(_context) do - start_supervised!(StronglyConsistentProcessManager) - - :ok - end end diff --git a/test/commands/support/consistency/strongly_consistent_process_manager.ex b/test/commands/support/consistency/strongly_consistent_process_manager.ex deleted file mode 100644 index ba180714..00000000 --- a/test/commands/support/consistency/strongly_consistent_process_manager.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule Commanded.Commands.StronglyConsistentProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: Commanded.Commands.ConsistencyApp, - name: __MODULE__, - consistency: :strong - - @derive Jason.Encoder - defstruct [ - :uuid - ] - - alias Commanded.Commands.ConsistencyAggregateRoot.{ - ConsistencyCommand, - ConsistencyEvent, - DispatchRequestedEvent, - NoOpCommand - } - - alias Commanded.Commands.StronglyConsistentProcessManager - - def interested?(%DispatchRequestedEvent{uuid: uuid}), do: {:start, uuid} - def interested?(%ConsistencyEvent{uuid: uuid}), do: {:continue, uuid} - - def handle(%StronglyConsistentProcessManager{}, %DispatchRequestedEvent{} = requested) do - %DispatchRequestedEvent{uuid: uuid, delay: delay} = requested - - %ConsistencyCommand{uuid: uuid, delay: delay} - end - - def handle(%StronglyConsistentProcessManager{}, %ConsistencyEvent{uuid: uuid, delay: delay}) do - :timer.sleep(delay) - - %NoOpCommand{uuid: uuid} - end - - def apply(%StronglyConsistentProcessManager{}, %DispatchRequestedEvent{uuid: uuid}) do - %StronglyConsistentProcessManager{uuid: uuid} - end - - def apply(%StronglyConsistentProcessManager{}, %ConsistencyEvent{uuid: uuid}) do - %StronglyConsistentProcessManager{uuid: uuid} - end -end diff --git a/test/event/support/upcast/process_manager.ex b/test/event/support/upcast/process_manager.ex deleted file mode 100644 index 2c702b3e..00000000 --- a/test/event/support/upcast/process_manager.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule Commanded.Event.Upcast.ProcessManager do - defmodule Ok do - defstruct [:id] - end - - defmodule Aggregate do - defstruct [] - - def execute(_, _), do: [] - def apply(agg, _), do: agg - end - - defmodule Router do - use Commanded.Commands.Router - - dispatch(Ok, to: Aggregate, identity: :id) - end - - defmodule Application do - use Commanded.Application, otp_app: :commanded - - router(Router) - end - - @derive Jason.Encoder - defstruct [] - - alias Commanded.Event.Upcast.Events.{EventFour, EventOne, EventThree, EventTwo, Stop} - alias Commanded.UUID - - use Commanded.ProcessManagers.ProcessManager, - application: Application, - name: "UpcastProcessManager" - - def interested?(%EventOne{process_id: process_id}), do: {:start, process_id} - def interested?(%EventTwo{process_id: process_id}), do: {:start, process_id} - def interested?(%EventThree{process_id: process_id}), do: {:start, process_id} - def interested?(%EventFour{process_id: process_id}), do: {:start, process_id} - def interested?(%Stop{process_id: process_id}), do: {:stop, process_id} - - def handle(_, %EventOne{} = e), do: send_reply(e) - def handle(_, %EventTwo{} = e), do: send_reply(e) - def handle(_, %EventThree{} = e), do: send_reply(e) - def handle(_, %EventFour{} = e), do: send_reply(e) - - defp send_reply(%{reply_to: reply_to} = e) do - send(:erlang.list_to_pid(reply_to), e) - - %Ok{id: UUID.uuid4()} - end -end diff --git a/test/event/upcaster_test.exs b/test/event/upcaster_test.exs index 4be746b3..2fa81fd4 100644 --- a/test/event/upcaster_test.exs +++ b/test/event/upcaster_test.exs @@ -4,7 +4,7 @@ defmodule Event.UpcasterTest do alias Commanded.Aggregates.Aggregate alias Commanded.{DefaultApp, EventStore} alias Commanded.EventStore.{EventData, RecordedEvent} - alias Commanded.Event.Upcast.Events.{EventFive, EventFour, EventOne, EventThree, EventTwo, Stop} + alias Commanded.Event.Upcast.Events.{EventFive, EventFour, EventOne, EventThree, EventTwo} alias Commanded.Event.Upcast.UpcastAggregate alias Commanded.UUID @@ -139,36 +139,6 @@ defmodule Event.UpcasterTest do end end - describe "upcast events received by process manager" do - alias Commanded.Event.Upcast.ProcessManager - alias Commanded.Event.Upcast.ProcessManager.Application - - setup do - start_supervised!(Application) - start_supervised!(ProcessManager) - - :ok - end - - test "will receive upcasted events" do - process_id = UUID.uuid4() - reply_to = :erlang.pid_to_list(self()) - - write_events(Application, [ - struct(EventOne, version: 1, reply_to: reply_to, process_id: process_id), - struct(EventTwo, version: 1, reply_to: reply_to, process_id: process_id), - struct(EventThree, version: 1, reply_to: reply_to, process_id: process_id), - struct(Stop, process_id: process_id) - ]) - - refute_receive %EventThree{} - - assert_receive %EventOne{version: 1} - assert_receive %EventTwo{version: 2} - assert_receive %EventFour{version: 2, name: "Chris"} - end - end - defp assert_metadata(metadata) do assert %{ application: Commanded.DefaultApp, diff --git a/test/example_domain/money_transfer/transfer_money_process_manager.ex b/test/example_domain/money_transfer/transfer_money_process_manager.ex deleted file mode 100644 index b64ff7fd..00000000 --- a/test/example_domain/money_transfer/transfer_money_process_manager.ex +++ /dev/null @@ -1,119 +0,0 @@ -defmodule Commanded.ExampleDomain.TransferMoneyProcessManager do - @moduledoc false - - use Commanded.ProcessManagers.ProcessManager, - application: Commanded.ExampleDomain.BankApp, - name: __MODULE__ - - alias Commanded.EventStore.EnrichedMetadata - alias Commanded.ExampleDomain.BankAccount.Commands.{DepositMoney, WithdrawMoney} - alias Commanded.ExampleDomain.BankAccount.Events.{MoneyDeposited, MoneyWithdrawn} - alias Commanded.ExampleDomain.MoneyTransfer.Events.MoneyTransferRequested - alias Commanded.ExampleDomain.TransferMoneyProcessManager - - @derive Jason.Encoder - defstruct [:transfer_uuid, :debit_account, :credit_account, :amount, :status, :user_uuid] - - def interested?(%MoneyTransferRequested{transfer_uuid: transfer_uuid}), - do: {:start, transfer_uuid} - - def interested?(%MoneyWithdrawn{transfer_uuid: transfer_uuid}), - do: {:continue, transfer_uuid} - - def interested?(%MoneyDeposited{transfer_uuid: transfer_uuid}), - do: {:continue, transfer_uuid} - - def handle( - %TransferMoneyProcessManager{}, - %MoneyTransferRequested{} = event, - %EnrichedMetadata{metadata: %{"user_uuid" => by_user}} - ) do - %MoneyTransferRequested{ - transfer_uuid: transfer_uuid, - debit_account: debit_account, - amount: amount - } = event - - %WithdrawMoney{ - account_number: debit_account, - transfer_uuid: transfer_uuid, - amount: amount, - by_user: by_user - } - end - - def handle(%TransferMoneyProcessManager{}, %MoneyTransferRequested{} = event) do - %MoneyTransferRequested{ - transfer_uuid: transfer_uuid, - debit_account: debit_account, - amount: amount - } = event - - %WithdrawMoney{ - account_number: debit_account, - transfer_uuid: transfer_uuid, - amount: amount - } - end - - def handle(%TransferMoneyProcessManager{} = pm, %MoneyWithdrawn{}) do - %TransferMoneyProcessManager{ - transfer_uuid: transfer_uuid, - credit_account: credit_account, - amount: amount - } = pm - - %DepositMoney{account_number: credit_account, transfer_uuid: transfer_uuid, amount: amount} - end - - ## State mutators - - def apply( - %TransferMoneyProcessManager{} = transfer, - %MoneyTransferRequested{} = event, - %EnrichedMetadata{metadata: %{"user_uuid" => user_uuid}} - ) do - %MoneyTransferRequested{ - transfer_uuid: transfer_uuid, - debit_account: debit_account, - credit_account: credit_account, - amount: amount - } = event - - %TransferMoneyProcessManager{ - transfer - | transfer_uuid: transfer_uuid, - debit_account: debit_account, - credit_account: credit_account, - amount: amount, - status: :withdraw_money_from_debit_account, - user_uuid: user_uuid - } - end - - def apply(%TransferMoneyProcessManager{} = transfer, %MoneyTransferRequested{} = event) do - %MoneyTransferRequested{ - transfer_uuid: transfer_uuid, - debit_account: debit_account, - credit_account: credit_account, - amount: amount - } = event - - %TransferMoneyProcessManager{ - transfer - | transfer_uuid: transfer_uuid, - debit_account: debit_account, - credit_account: credit_account, - amount: amount, - status: :withdraw_money_from_debit_account - } - end - - def apply(%TransferMoneyProcessManager{} = transfer, %MoneyWithdrawn{}) do - %TransferMoneyProcessManager{transfer | status: :deposit_money_in_credit_account} - end - - def apply(%TransferMoneyProcessManager{} = transfer, %MoneyDeposited{}) do - %TransferMoneyProcessManager{transfer | status: :transfer_complete} - end -end diff --git a/test/process_managers/dynamic_process_manager_application_test.exs b/test/process_managers/dynamic_process_manager_application_test.exs deleted file mode 100644 index 223698f8..00000000 --- a/test/process_managers/dynamic_process_manager_application_test.exs +++ /dev/null @@ -1,74 +0,0 @@ -defmodule Commanded.ProcessManager.DynamicProcessManagerApplicationTest do - use ExUnit.Case - - alias Commanded.ExampleDomain.BankAccount.Commands.OpenAccount - alias Commanded.ExampleDomain.BankAccount.Events.{BankAccountOpened, MoneyDeposited} - alias Commanded.ExampleDomain.BankApp - alias Commanded.Helpers.{CommandAuditMiddleware, Wait} - alias Commanded.ProcessManagers.{DynamicProcessManager, ProcessRouter} - alias Commanded.UUID - - setup_all do - start_supervised!(CommandAuditMiddleware) - :ok - end - - describe "dynamic application process manager" do - setup do - start_reply_to_agent!() - start_supervised!({BankApp, name: :example1}) - start_supervised!({BankApp, name: :example2}) - :ok - end - - test "should only receive events from named application" do - pid1 = start_supervised!({DynamicProcessManager, application: :example1}) - pid2 = start_supervised!({DynamicProcessManager, application: :example2}) - - {:ok, account_number} = open_account(:example1) - - instance1 = wait_for_process_instance(pid1, account_number) - - assert_receive {:event, ^instance1, %BankAccountOpened{account_number: ^account_number}} - assert_receive {:event, ^instance1, %MoneyDeposited{account_number: ^account_number}} - refute_receive {:event, _pid, _event} - - assert ProcessRouter.process_instance(pid2, account_number) == - {:error, :process_manager_not_found} - - {:ok, account_number} = open_account(:example2) - - instance2 = wait_for_process_instance(pid2, account_number) - - assert_receive {:event, ^instance2, %BankAccountOpened{account_number: ^account_number}} - assert_receive {:event, ^instance2, %MoneyDeposited{account_number: ^account_number}} - refute_receive {:event, ^instance1, _event} - end - end - - defp start_reply_to_agent! do - reply_to = self() - - start_supervised!(%{ - id: Agent, - start: {Agent, :start_link, [fn -> reply_to end, [name: :reply_to]]} - }) - end - - defp open_account(application, account_number \\ UUID.uuid4()) do - command = %OpenAccount{account_number: account_number, initial_balance: 1_000} - - :ok = BankApp.dispatch(command, application: application) - - {:ok, account_number} - end - - defp wait_for_process_instance(process_router, aggregate_uuid) do - Wait.until(fn -> - assert {:ok, process_instance} = - ProcessRouter.process_instance(process_router, aggregate_uuid) - - process_instance - end) - end -end diff --git a/test/process_managers/multi_routing_test.exs b/test/process_managers/multi_routing_test.exs deleted file mode 100644 index 8f3e7295..00000000 --- a/test/process_managers/multi_routing_test.exs +++ /dev/null @@ -1,82 +0,0 @@ -defmodule Commanded.ProcessManager.MultiRoutingTest do - use ExUnit.Case - - import Commanded.Assertions.EventAssertions - - alias Commanded.ProcessManagers.ProcessRouter - alias Commanded.ProcessManagers.Todo.Commands.CreateTodo - alias Commanded.ProcessManagers.Todo.Events.TodoDone - alias Commanded.ProcessManagers.{TodoApp, TodoProcessManager, TodoRouter} - alias Commanded.ProcessManagers.TodoList.Commands.{CreateList, MarkAllDone} - alias Commanded.ProcessManagers.TodoList.Events.ListAllDone - alias Commanded.UUID - - setup do - start_supervised!(TodoApp) - - :ok - end - - test "should create process instance for each identifier returned by `interested?/2`" do - {:ok, pm} = TodoProcessManager.start_link() - - todo1_uuid = create_todo() - todo2_uuid = create_todo() - todo3_uuid = create_todo() - - list_uuid = create_list_of_todos([todo1_uuid, todo2_uuid, todo3_uuid]) - - # mark list done should mark individual TODOs as done via process manager - :ok = TodoRouter.dispatch(%MarkAllDone{list_uuid: list_uuid}, application: TodoApp) - - assert_receive_event(TodoApp, ListAllDone, fn done -> assert done.list_uuid == list_uuid end) - - assert_receive_event( - TodoApp, - TodoDone, - fn done -> done.todo_uuid == todo1_uuid end, - fn done -> - assert done.todo_uuid == todo1_uuid - end - ) - - assert_receive_event( - TodoApp, - TodoDone, - fn done -> done.todo_uuid == todo2_uuid end, - fn done -> - assert done.todo_uuid == todo2_uuid - end - ) - - assert_receive_event( - TodoApp, - TodoDone, - fn done -> done.todo_uuid == todo3_uuid end, - fn done -> - assert done.todo_uuid == todo3_uuid - end - ) - - instances = ProcessRouter.process_instances(pm) - assert length(instances) == 3 - end - - defp create_todo do - todo_uuid = UUID.uuid4() - command = %CreateTodo{todo_uuid: todo_uuid} - - :ok = TodoRouter.dispatch(command, application: TodoApp) - - todo_uuid - end - - defp create_list_of_todos(todo_uuids) do - list_uuid = UUID.uuid4() - command = %CreateList{list_uuid: list_uuid, todo_uuids: todo_uuids} - - :ok = TodoRouter.dispatch(command, application: TodoApp) - - list_uuid - end -end diff --git a/test/process_managers/process_manager_after_command_test.exs b/test/process_managers/process_manager_after_command_test.exs deleted file mode 100644 index f4ef7f69..00000000 --- a/test/process_managers/process_manager_after_command_test.exs +++ /dev/null @@ -1,87 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerAfterCommandTest do - use ExUnit.Case - - import Commanded.Assertions.EventAssertions - import Commanded.Enumerable - - alias Commanded.EventStore - alias Commanded.Helpers.Wait - alias Commanded.ProcessManagers.AfterCommandProcessManager - alias Commanded.ProcessManagers.ExampleAggregate.Commands.Publish - alias Commanded.ProcessManagers.ExampleAggregate.Commands.Start - alias Commanded.ProcessManagers.ExampleAggregate.Events.Interested - alias Commanded.ProcessManagers.ExampleAggregate.Events.Started - alias Commanded.ProcessManagers.ExampleAggregate.Events.Stopped - alias Commanded.ProcessManagers.ExampleAggregate.Events.Uninterested - alias Commanded.ProcessManagers.ExampleApp - alias Commanded.ProcessManagers.ExampleRouter - alias Commanded.ProcessManagers.ProcessRouter - alias Commanded.UUID - - setup do - start_supervised!(ExampleApp) - - :ok - end - - test "should stop process manager instance after specified command and work with both after_command/2 and after_command/3 callbacks" do - aggregate_uuid = UUID.uuid4() - source_uuid = "\"AfterCommandProcessManager\"-\"#{aggregate_uuid}\"" - - notify_to = self() |> :erlang.pid_to_list() - - metadata = %{"notify_to" => notify_to} - - {:ok, process_router} = AfterCommandProcessManager.start_link() - - :ok = ExampleRouter.dispatch(%Start{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - # Dispatch command to publish multiple events and trigger dispatch of the stop command - :ok = - ExampleRouter.dispatch( - %Publish{ - aggregate_uuid: aggregate_uuid, - interesting: 10, - uninteresting: 1 - }, - application: ExampleApp, - metadata: metadata - ) - - # Process state snapshot should be created - assert {:ok, _} = EventStore.read_snapshot(ExampleApp, source_uuid) - - assert_receive_event(ExampleApp, Stopped, fn event -> - assert event.aggregate_uuid == aggregate_uuid - end) - - events = EventStore.stream_forward(ExampleApp, aggregate_uuid) |> Enum.to_list() - - assert pluck(events, :data) == [ - %Started{aggregate_uuid: aggregate_uuid}, - %Interested{aggregate_uuid: aggregate_uuid, index: 1}, - %Interested{aggregate_uuid: aggregate_uuid, index: 2}, - %Interested{aggregate_uuid: aggregate_uuid, index: 3}, - %Interested{aggregate_uuid: aggregate_uuid, index: 4}, - %Interested{aggregate_uuid: aggregate_uuid, index: 5}, - %Interested{aggregate_uuid: aggregate_uuid, index: 6}, - %Interested{aggregate_uuid: aggregate_uuid, index: 7}, - %Interested{aggregate_uuid: aggregate_uuid, index: 8}, - %Interested{aggregate_uuid: aggregate_uuid, index: 9}, - %Interested{aggregate_uuid: aggregate_uuid, index: 10}, - %Uninterested{aggregate_uuid: aggregate_uuid, index: 1}, - %Stopped{aggregate_uuid: aggregate_uuid} - ] - - assert_receive :metadata_available - - Wait.until(fn -> - # Process instance should be stopped - assert ProcessRouter.process_instance(process_router, aggregate_uuid) == - {:error, :process_manager_not_found} - - # Process state snapshot should be deleted - assert EventStore.read_snapshot(ExampleApp, source_uuid) == {:error, :snapshot_not_found} - end) - end -end diff --git a/test/process_managers/process_manager_error_handling_state_test.exs b/test/process_managers/process_manager_error_handling_state_test.exs deleted file mode 100644 index dfa244ae..00000000 --- a/test/process_managers/process_manager_error_handling_state_test.exs +++ /dev/null @@ -1,73 +0,0 @@ -defmodule Commanded.ProcessManager.ProcessManagerErrorHandlingStateTest do - use ExUnit.Case - - alias Commanded.ProcessManagers.ErrorAggregate.Commands.{AttemptProcess, StartProcess} - alias Commanded.ProcessManagers.ErrorAggregate.Events.ProcessStarted - - alias Commanded.ProcessManagers.{ - ErrorApp, - ErrorRouter, - FailureContext, - StateErrorHandlingProcessManager - } - - alias Commanded.EventStore.EnrichedMetadata - alias Commanded.EventStore.RecordedEvent - alias Commanded.UUID - - setup do - start_supervised!(ErrorApp) - start_supervised!(StateErrorHandlingProcessManager) - - :ok - end - - test "should receive the process instance state in the context" do - process_uuid = UUID.uuid4() - reply_to = reply_to() - - command = %StartProcess{process_uuid: process_uuid, reply_to: reply_to} - - assert :ok = - ErrorRouter.dispatch(command, - application: ErrorApp, - metadata: %{"user_id" => 1234} - ) - - assert_receive {:error, :failed, failed_command, failure_context} - - assert match?(%AttemptProcess{process_uuid: ^process_uuid}, failed_command) - - assert match?( - %FailureContext{ - enriched_metadata: %EnrichedMetadata{ - metadata: %{"user_id" => 1234}, - application: ErrorApp, - causation_id: _causation_id, - correlation_id: _correlation_id, - created_at: %DateTime{}, - event_id: _event_id, - event_number: _event_number, - stream_id: _stream_id, - stream_version: _stream_version - }, - last_event: %RecordedEvent{ - data: %ProcessStarted{ - process_uuid: ^process_uuid, - reply_to: ^reply_to - } - }, - process_manager_state: %StateErrorHandlingProcessManager{ - process_uuid: ^process_uuid, - reply_to: ^reply_to - }, - stacktrace: nil, - context: %{}, - pending_commands: [] - }, - failure_context - ) - end - - defp reply_to, do: :erlang.pid_to_list(self()) -end diff --git a/test/process_managers/process_manager_error_handling_test.exs b/test/process_managers/process_manager_error_handling_test.exs deleted file mode 100644 index 94818cbb..00000000 --- a/test/process_managers/process_manager_error_handling_test.exs +++ /dev/null @@ -1,321 +0,0 @@ -defmodule Commanded.ProcessManager.ProcessManagerErrorHandlingTest do - use ExUnit.Case - - alias Commanded.Helpers.EventFactory - alias Commanded.ProcessManagers.DefaultErrorHandlingProcessManager - alias Commanded.ProcessManagers.ErrorAggregate.Commands.StartProcess - - alias Commanded.ProcessManagers.ErrorAggregate.Events.{ - ProcessApplyException, - ProcessDispatchException, - ProcessError, - ProcessException - } - - alias Commanded.ProcessManagers.ErrorApp - alias Commanded.ProcessManagers.ErrorHandlingProcessManager - alias Commanded.ProcessManagers.ErrorRouter - alias Commanded.ProcessManagers.FailureContext - alias Commanded.UUID - - setup do - start_supervised!(ErrorApp) - - {:ok, process_router} = ErrorHandlingProcessManager.start_link() - - Process.unlink(process_router) - ref = Process.monitor(process_router) - - [process_router: process_router, ref: ref] - end - - describe "process manager event handling exception" do - test "should print the stack trace", %{process_router: process_router, ref: ref} do - import ExUnit.CaptureLog - - send_error_message = fn -> - send_exception_event(process_router) - - assert_receive {:DOWN, ^ref, :process, ^process_router, - %RuntimeError{message: "an exception"}} - end - - captured = capture_log(send_error_message) - - assert captured =~ "(RuntimeError) an exception" - assert captured =~ "error_handling_process_manager.ex" - assert captured =~ "Commanded.ProcessManagers.ErrorHandlingProcessManager.handle/2" - end - - test "should include the stack trace in failure context", %{ - process_router: process_router, - ref: ref - } do - send_exception_event(process_router) - - expected_runtime_error = %RuntimeError{message: "an exception"} - - assert_receive {:error, ^expected_runtime_error, %FailureContext{stacktrace: stacktrace}} - assert_receive {:DOWN, ^ref, :process, ^process_router, ^expected_runtime_error} - - refute is_nil(stacktrace) - end - end - - describe "process manager event handling error" do - test "should call `error/3` callback on error", %{process_router: process_router, ref: ref} do - send_error_event(process_router) - - assert_receive {:error, "an error", %FailureContext{stacktrace: stacktrace}} - assert_receive {:DOWN, ^ref, :process, ^process_router, "an error"} - - assert is_nil(stacktrace) - end - - test "should call `error/3` callback on exception", %{ - process_router: process_router, - ref: ref - } do - send_exception_event(process_router) - - expected_runtime_error = %RuntimeError{message: "an exception"} - - assert_receive {:error, ^expected_runtime_error, _failure_context} - assert_receive {:DOWN, ^ref, :process, ^process_router, ^expected_runtime_error} - end - end - - describe "process manager event applying error" do - test "should call `error/3` callback on exception", %{ - process_router: process_router, - ref: ref - } do - send_apply_exception_event(process_router) - - expected_runtime_error = %RuntimeError{message: "an exception"} - - assert_receive {:error, ^expected_runtime_error, _failure_context} - assert_receive {:DOWN, ^ref, :process, ^process_router, ^expected_runtime_error} - end - end - - describe "process manager dispatch command error" do - test "should retry the event until process manager requests stop" do - {:ok, process_router} = ErrorHandlingProcessManager.start_link() - - command = %StartProcess{ - process_uuid: UUID.uuid4(), - strategy: "retry", - reply_to: reply_to() - } - - Process.unlink(process_router) - ref = Process.monitor(process_router) - - assert :ok = ErrorRouter.dispatch(command, application: ErrorApp) - - assert_receive {:error, :failed, %{attempts: 1}, %FailureContext{}} - assert_receive {:error, :failed, %{attempts: 2}, %FailureContext{}} - assert_receive {:error, :too_many_attempts, %{attempts: 3}, %FailureContext{}} - - # Should shutdown process router - assert_receive {:DOWN, ^ref, :process, ^process_router, :too_many_attempts} - end - - test "should retry with failure context until process manager requests stop" do - {:ok, process_router} = ErrorHandlingProcessManager.start_link() - - command = %StartProcess{ - process_uuid: UUID.uuid4(), - strategy: "retry_failure_context", - reply_to: reply_to() - } - - Process.unlink(process_router) - ref = Process.monitor(process_router) - - assert :ok = ErrorRouter.dispatch(command, application: ErrorApp) - - assert_receive {:error, :failed, %FailureContext{context: %{attempts: 1}}} - assert_receive {:error, :failed, %FailureContext{context: %{attempts: 2}}} - assert_receive {:error, :too_many_attempts, %{attempts: 3}, %FailureContext{}} - - # Should shutdown process router - assert_receive {:DOWN, ^ref, :process, ^process_router, :too_many_attempts} - end - - test "should retry command with specified delay between attempts" do - command = %StartProcess{ - process_uuid: UUID.uuid4(), - strategy: "retry", - delay: 1, - reply_to: reply_to() - } - - {:ok, process_router} = ErrorHandlingProcessManager.start_link() - - Process.unlink(process_router) - ref = Process.monitor(process_router) - - assert :ok = ErrorRouter.dispatch(command, application: ErrorApp) - - assert_receive {:error, :failed, %{attempts: 1, delay: 1}, _failure_context} - assert_receive {:error, :failed, %{attempts: 2, delay: 1}, _failure_context} - assert_receive {:error, :too_many_attempts, %{attempts: 3}, _failure_context} - - # Should shutdown process router - assert_receive {:DOWN, ^ref, :process, ^process_router, :too_many_attempts} - end - - test "should retry command with failure context and specified delay between attempts" do - command = %StartProcess{ - process_uuid: UUID.uuid4(), - strategy: "retry_failure_context", - delay: 1, - reply_to: reply_to() - } - - {:ok, process_router} = ErrorHandlingProcessManager.start_link() - - Process.unlink(process_router) - ref = Process.monitor(process_router) - - assert :ok = ErrorRouter.dispatch(command, application: ErrorApp) - - assert_receive {:error, :failed, %FailureContext{context: %{attempts: 1, delay: 1}}} - assert_receive {:error, :failed, %FailureContext{context: %{attempts: 2, delay: 1}}} - assert_receive {:error, :too_many_attempts, %{attempts: 3}, %FailureContext{}} - - # Should shutdown process router - assert_receive {:DOWN, ^ref, :process, ^process_router, :too_many_attempts} - end - - test "should skip the command when error reply is `:skip`" do - command = %StartProcess{ - process_uuid: UUID.uuid4(), - strategy: "skip", - reply_to: reply_to() - } - - {:ok, process_router} = ErrorHandlingProcessManager.start_link() - - assert :ok = ErrorRouter.dispatch(command, application: ErrorApp) - - assert_receive {:error, :failed, %{attempts: 1}, _failure_context} - refute_receive {:error, :failed, %{attempts: 2}, _failure_context} - - # Should not shutdown process router - assert Process.alive?(process_router) - end - - test "should skip the command when error reply is `{:skip, :continue_pending}`" do - command = %StartProcess{ - process_uuid: UUID.uuid4(), - strategy: "skip_continue_pending", - reply_to: reply_to() - } - - {:ok, process_router} = ErrorHandlingProcessManager.start_link() - - assert :ok = ErrorRouter.dispatch(command, application: ErrorApp) - - assert_receive {:error, :failed, %{attempts: 1}, _failure_context} - refute_receive {:error, :failed, %{attempts: 2}, _failure_context} - - # Should not shutdown process router - assert Process.alive?(process_router) - end - - test "should continue with modified command" do - command = %StartProcess{ - process_uuid: UUID.uuid4(), - strategy: "continue", - reply_to: reply_to() - } - - {:ok, process_router} = ErrorHandlingProcessManager.start_link() - - assert :ok = ErrorRouter.dispatch(command, application: ErrorApp) - - assert_receive {:error, :failed, %{attempts: 1}, _failure_context} - assert_receive :process_continued - - # Should not shutdown process router - assert Process.alive?(process_router) - end - - test "should stop process manager on error by default" do - command = %StartProcess{process_uuid: UUID.uuid4(), reply_to: reply_to()} - - {:ok, process_router} = DefaultErrorHandlingProcessManager.start_link() - - Process.unlink(process_router) - ref = Process.monitor(process_router) - - assert :ok = ErrorRouter.dispatch(command, application: ErrorApp) - - # Should shutdown process router - assert_receive {:DOWN, ^ref, :process, ^process_router, :failed} - refute Process.alive?(process_router) - end - end - - describe "process manager dispatch command exception" do - test "should stop process manager", %{process_router: process_router, ref: ref} do - send_dispatch_exception_event(process_router) - - expected_runtime_error = %RuntimeError{message: "a dispatch exception"} - - assert_receive {:error, ^expected_runtime_error, %FailureContext{}} - assert_receive {:DOWN, ^ref, :process, ^process_router, ^expected_runtime_error} - end - end - - defp send_error_event(process_router) do - process_uuid = UUID.uuid4() - - send_events_to_process_router(process_router, [ - %ProcessError{process_uuid: process_uuid, reply_to: reply_to(), message: "an error"} - ]) - end - - defp send_exception_event(process_router) do - process_uuid = UUID.uuid4() - - send_events_to_process_router(process_router, [ - %ProcessException{process_uuid: process_uuid, reply_to: reply_to(), message: "an exception"} - ]) - end - - defp send_apply_exception_event(process_router) do - process_uuid = UUID.uuid4() - - send_events_to_process_router(process_router, [ - %ProcessApplyException{ - process_uuid: process_uuid, - reply_to: reply_to(), - message: "an exception" - } - ]) - end - - defp send_dispatch_exception_event(process_router) do - process_uuid = UUID.uuid4() - - send_events_to_process_router(process_router, [ - %ProcessDispatchException{ - process_uuid: process_uuid, - reply_to: reply_to(), - message: "a dispatch exception" - } - ]) - end - - defp send_events_to_process_router(process_router, events) do - recorded_events = EventFactory.map_to_recorded_events(events) - - send(process_router, {:events, recorded_events}) - end - - defp reply_to, do: :erlang.pid_to_list(self()) -end diff --git a/test/process_managers/process_manager_idle_timeout_test.exs b/test/process_managers/process_manager_idle_timeout_test.exs deleted file mode 100644 index b22c2045..00000000 --- a/test/process_managers/process_manager_idle_timeout_test.exs +++ /dev/null @@ -1,93 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerIdleTimeoutTest do - use ExUnit.Case - - alias Commanded.ProcessManagers.ExampleAggregate.Commands.{Start, Stop} - - alias Commanded.ProcessManagers.{ - ExampleApp, - ExampleRouter, - ProcessRouter, - TimeoutProcessManager - } - - alias Commanded.Helpers.Wait - alias Commanded.UUID - - setup do - start_supervised!(ExampleApp) - - :ok - end - - describe "process manager idle timeout" do - test "should shutdown instance after inactivity" do - {:ok, pm} = TimeoutProcessManager.start_link(idle_timeout: 50) - - aggregate_uuid = UUID.uuid4() - start = %Start{aggregate_uuid: aggregate_uuid} - - :ok = ExampleRouter.dispatch(start, application: ExampleApp) - - {instance, ref} = wait_for_process_instance(pm, aggregate_uuid) - - assert_receive {:DOWN, ^ref, :process, ^instance, :normal} - end - - test "should stop instance on demand" do - {:ok, pm} = TimeoutProcessManager.start_link(idle_timeout: 10_000) - - aggregate_uuid = UUID.uuid4() - start = %Start{aggregate_uuid: aggregate_uuid} - stop = %Stop{aggregate_uuid: aggregate_uuid} - - :ok = ExampleRouter.dispatch(start, application: ExampleApp) - - {instance, ref} = wait_for_process_instance(pm, aggregate_uuid) - - :ok = ExampleRouter.dispatch(stop, application: ExampleApp) - - assert_receive {:DOWN, ^ref, :process, ^instance, :normal} - end - end - - describe "process manager `:infinity` idle timeout" do - test "should not shutdown instance" do - {:ok, pm} = TimeoutProcessManager.start_link(idle_timeout: :infinity) - - aggregate_uuid = UUID.uuid4() - start = %Start{aggregate_uuid: aggregate_uuid} - - :ok = ExampleRouter.dispatch(start, application: ExampleApp) - - {instance, ref} = wait_for_process_instance(pm, aggregate_uuid) - - refute_receive {:DOWN, ^ref, :process, ^instance, :normal} - end - - test "should stop instance on demand" do - {:ok, pm} = TimeoutProcessManager.start_link(idle_timeout: :infinity) - - aggregate_uuid = UUID.uuid4() - start = %Start{aggregate_uuid: aggregate_uuid} - stop = %Stop{aggregate_uuid: aggregate_uuid} - - :ok = ExampleRouter.dispatch(start, application: ExampleApp) - - {instance, ref} = wait_for_process_instance(pm, aggregate_uuid) - - :ok = ExampleRouter.dispatch(stop, application: ExampleApp) - - assert_receive {:DOWN, ^ref, :process, ^instance, :normal} - end - end - - defp wait_for_process_instance(pm, process_uuid) do - Wait.until(fn -> - assert {:ok, instance} = ProcessRouter.process_instance(pm, process_uuid) - - ref = Process.monitor(instance) - - {instance, ref} - end) - end -end diff --git a/test/process_managers/process_manager_init_test.exs b/test/process_managers/process_manager_init_test.exs deleted file mode 100644 index b096c114..00000000 --- a/test/process_managers/process_manager_init_test.exs +++ /dev/null @@ -1,36 +0,0 @@ -defmodule Commanded.ProcessManager.ProcessManagerInitTest do - use ExUnit.Case - - alias Commanded.DefaultApp - alias Commanded.ProcessManagers.RuntimeConfigProcessManager - - describe "process manager `init/1` callback" do - setup do - for tenant <- [:tenant1, :tenant2, :tenant3] do - start_supervised!({DefaultApp, name: Module.concat([DefaultApp, tenant])}) - end - - :ok - end - - test "should be called at runtime" do - {:ok, _pm1} = RuntimeConfigProcessManager.start_link(tenant: :tenant1, reply_to: self()) - {:ok, _pm2} = RuntimeConfigProcessManager.start_link(tenant: :tenant2, reply_to: self()) - {:ok, _pm3} = RuntimeConfigProcessManager.start_link(tenant: :tenant3, reply_to: self()) - - assert_receive {:init, :tenant1} - assert_receive {:init, :tenant2} - assert_receive {:init, :tenant3} - end - - test "should be called on restart if the process crashes" do - pm = start_supervised!({RuntimeConfigProcessManager, tenant: :tenant1, reply_to: self()}) - - Process.exit(pm, :kill) - - assert_receive {:init, :tenant1} - assert_receive {:init, :tenant1} - refute_receive {:init, :tenant1} - end - end -end diff --git a/test/process_managers/process_manager_instance_exception_test.exs b/test/process_managers/process_manager_instance_exception_test.exs deleted file mode 100644 index 862d1b9d..00000000 --- a/test/process_managers/process_manager_instance_exception_test.exs +++ /dev/null @@ -1,43 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerInstanceExceptionTest do - use ExUnit.Case - - alias Commanded.ProcessManagers.{ExampleApp, ExampleProcessManager, ExampleRouter} - alias Commanded.ProcessManagers.ExampleAggregate.Commands.{Error, Raise, Start} - alias Commanded.UUID - - setup do - start_supervised!(ExampleApp) - - :ok - end - - test "should stop process router when handling event errors" do - aggregate_uuid = UUID.uuid4() - - {:ok, process_router} = ExampleProcessManager.start_link() - - Process.unlink(process_router) - ref = Process.monitor(process_router) - - :ok = ExampleRouter.dispatch(%Start{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - :ok = ExampleRouter.dispatch(%Error{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - # Should shutdown process - assert_receive {:DOWN, ^ref, _, _, _} - end - - test "should stop process router when handling event exception" do - aggregate_uuid = UUID.uuid4() - - {:ok, process_router} = ExampleProcessManager.start_link() - - Process.unlink(process_router) - ref = Process.monitor(process_router) - - :ok = ExampleRouter.dispatch(%Start{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - :ok = ExampleRouter.dispatch(%Raise{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - # Should shutdown process - assert_receive {:DOWN, ^ref, _, _, _} - end -end diff --git a/test/process_managers/process_manager_instance_test.exs b/test/process_managers/process_manager_instance_test.exs deleted file mode 100644 index f01e19a1..00000000 --- a/test/process_managers/process_manager_instance_test.exs +++ /dev/null @@ -1,257 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerInstanceTest do - use ExUnit.Case - - import Mox - - alias Commanded.Application.Config - alias Commanded.Application.Mock, as: MockApplication - alias Commanded.EventStore.Adapters.Mock, as: MockEventStore - alias Commanded.EventStore.{RecordedEvent, SnapshotData} - alias Commanded.ExampleDomain.BankAccount.Commands.WithdrawMoney - alias Commanded.ExampleDomain.MoneyTransfer.Events.MoneyTransferRequested - alias Commanded.ExampleDomain.TransferMoneyProcessManager - alias Commanded.Helpers.Wait - alias Commanded.ProcessManagers.IdentityProcessManager - alias Commanded.ProcessManagers.IdentityProcessManager.AnEvent - alias Commanded.ProcessManagers.{ProcessManagerInstance, ProcessRouter} - alias Commanded.Registration.LocalRegistry - alias Commanded.UUID - - alias Commanded.Serialization.ModuleNameTypeProvider - alias Commanded.Serialization.TypeProvider.Mock, as: MockTypeProvider - - setup :set_mox_global - setup :verify_on_exit! - - setup do - mock_event_store() - mock_type_provider() - - {:ok, registry_meta} = start_local_registry() - - Config.associate(self(), MockApplication, - application: MockApplication, - event_store: {MockEventStore, %{}}, - registry: {LocalRegistry, registry_meta} - ) - end - - describe "process manager instance" do - test "handles an event and dispatches a command copying existing metadata" do - transfer_uuid = UUID.uuid4() - debit_account = UUID.uuid4() - credit_account = UUID.uuid4() - expected_source_uuid = "\"TransferMoneyProcessManager\"-\"#{transfer_uuid}\"" - user_uuid = UUID.uuid4() - - metadata = %{"user_uuid" => user_uuid} - - expect(MockEventStore, :read_snapshot, fn _adapter_meta, ^expected_source_uuid -> - {:error, :snapshot_not_found} - end) - - # Ensure a type provider call is made - expect(MockTypeProvider, :to_string, fn process_state -> - assert %TransferMoneyProcessManager{} = process_state - ModuleNameTypeProvider.to_string(process_state) - end) - - expect(MockEventStore, :record_snapshot, fn _adapter_meta, snapshot -> - assert %SnapshotData{ - data: %TransferMoneyProcessManager{ - amount: 100, - credit_account: ^credit_account, - debit_account: ^debit_account, - status: :withdraw_money_from_debit_account, - transfer_uuid: ^transfer_uuid, - user_uuid: ^user_uuid - }, - source_type: "Elixir.Commanded.ExampleDomain.TransferMoneyProcessManager", - source_uuid: ^expected_source_uuid, - source_version: 1 - } = snapshot - - :ok - end) - - expect(MockApplication, :dispatch, fn command, opts -> - assert %WithdrawMoney{ - account_number: ^debit_account, - transfer_uuid: ^transfer_uuid, - amount: 100, - by_user: ^user_uuid - } = command - - assert metadata == Keyword.get(opts, :metadata) - - :ok - end) - - {:ok, instance} = start_process_manager_instance(transfer_uuid) - - event = - to_recorded_event( - %MoneyTransferRequested{ - transfer_uuid: transfer_uuid, - debit_account: debit_account, - credit_account: credit_account, - amount: 100 - }, - metadata - ) - - :ok = ProcessManagerInstance.process_event(instance, event) - - # Should send ack to process router after processing event - assert_receive({:"$gen_cast", {:ack_event, ^event, _instance}}, 1_000) - end - - test "get current process identity" do - {:ok, process_router} = - start_supervised({IdentityProcessManager, application: MockApplication}) - - process_uuids = Enum.sort([UUID.uuid4(), UUID.uuid4(), UUID.uuid4()]) - - event = to_recorded_event(%AnEvent{uuids: process_uuids, reply_to: self()}) - - send(process_router, {:events, [event]}) - - process_instances = - Wait.until(fn -> - process_instances = ProcessRouter.process_instances(process_router) - - assert process_instances - |> Enum.map(fn {process_uuid, _pid} -> process_uuid end) - |> Enum.sort() == process_uuids - - process_instances - end) - - for {process_uuid, pid} <- process_instances do - assert_receive {:identity, ^process_uuid, ^pid} - end - end - - test "ignore unexpected messages" do - import ExUnit.CaptureLog - - transfer_uuid = UUID.uuid4() - - expect(MockEventStore, :read_snapshot, fn _adapter_meta, _source_uuid -> - {:error, :snapshot_not_found} - end) - - {:ok, instance} = start_process_manager_instance(transfer_uuid) - - ref = Process.monitor(instance) - - send_unexpected_mesage = fn -> - send(instance, :unexpected_message) - - refute_receive {:DOWN, ^ref, :process, ^instance, _} - end - - assert capture_log(send_unexpected_mesage) =~ - "Commanded.ExampleDomain.TransferMoneyProcessManager received unexpected message: :unexpected_message" - end - - defmodule NoAppProcessManager do - use Commanded.ProcessManagers.ProcessManager, name: __MODULE__ - end - - test "should ensure a process manager application is provided" do - expected_error = - "Commanded.ProcessManagers.ProcessManagerInstanceTest.NoAppProcessManager expects :application option" - - assert_raise ArgumentError, expected_error, fn -> - NoAppProcessManager.start_link() - end - end - - defmodule UnnamedProcessManager do - use Commanded.ProcessManagers.ProcessManager, application: Commanded.DefaultApp - end - - test "should ensure a process manager name is provided" do - expected_error = - "Commanded.ProcessManagers.ProcessManagerInstanceTest.UnnamedProcessManager expects :name option" - - assert_raise ArgumentError, expected_error, fn -> - UnnamedProcessManager.start_link() - end - end - - defmodule MyProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: Commanded.DefaultApp, - name: __MODULE__ - end - - test "should allow using process manager module as name" do - start_supervised!(Commanded.DefaultApp) - - assert {:ok, _pid} = MyProcessManager.start_link() - end - end - - defp start_process_manager_instance(transfer_uuid) do - start_supervised( - {ProcessManagerInstance, - application: MockApplication, - idle_timeout: :infinity, - process_manager_name: "TransferMoneyProcessManager", - process_manager_module: TransferMoneyProcessManager, - process_router: self(), - process_uuid: transfer_uuid} - ) - end - - defp to_recorded_event(event) do - %RecordedEvent{event_number: 1, stream_id: "stream-id", stream_version: 1, data: event} - end - - defp to_recorded_event(event, metadata) do - %{to_recorded_event(event) | metadata: metadata} - end - - defp mock_event_store do - stub(MockEventStore, :subscribe_to, fn - _event_store, :all, name, pid, :origin, _opts -> - assert is_binary(name) - assert is_pid(pid) - - send(pid, {:subscribed, self()}) - - {:ok, self()} - end) - - stub(MockEventStore, :read_snapshot, fn _event_store, _snapshot_uuid -> - {:error, :snapshot_not_found} - end) - - stub(MockEventStore, :record_snapshot, fn _event_store, _snapshot -> :ok end) - stub(MockEventStore, :ack_event, fn _event_store, _pid, _event -> :ok end) - end - - defp mock_type_provider do - stub(MockTypeProvider, :to_string, fn struct -> ModuleNameTypeProvider.to_string(struct) end) - stub(MockTypeProvider, :to_struct, fn type -> ModuleNameTypeProvider.to_struct(type) end) - - current_type_provider = Application.get_env(:commanded, :type_provider) - Application.put_env(:commanded, :type_provider, MockTypeProvider) - - on_exit(fn -> - if current_type_provider, - do: Application.put_env(:commanded, :type_provider, current_type_provider), - else: Application.delete_env(:commanded, :type_provider) - end) - end - - defp start_local_registry do - {:ok, registry_child_spec, registry_meta} = LocalRegistry.child_spec(MockApplication, []) - - for child_spec <- registry_child_spec, do: start_supervised!(child_spec) - - {:ok, registry_meta} - end -end diff --git a/test/process_managers/process_manager_integration_test.exs b/test/process_managers/process_manager_integration_test.exs deleted file mode 100644 index 8fde3965..00000000 --- a/test/process_managers/process_manager_integration_test.exs +++ /dev/null @@ -1,73 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerIntegrationTest do - use ExUnit.Case - - import Commanded.Assertions.EventAssertions - - alias Commanded.ExampleDomain.BankAccount.Commands.OpenAccount - alias Commanded.ExampleDomain.BankAccount.Events.{MoneyDeposited, MoneyWithdrawn} - alias Commanded.ExampleDomain.{BankApp, BankRouter, TransferMoneyProcessManager} - alias Commanded.ExampleDomain.MoneyTransfer.Commands.TransferMoney - alias Commanded.ExampleDomain.MoneyTransfer.Events.MoneyTransferRequested - alias Commanded.Helpers.CommandAuditMiddleware - alias Commanded.ProcessManagers.ProcessRouter - alias Commanded.UUID - - setup do - start_supervised!(CommandAuditMiddleware) - start_supervised!(BankApp) - - :ok - end - - test "should start a process manager in response to an event" do - account_number1 = UUID.uuid4() - account_number2 = UUID.uuid4() - transfer_uuid = UUID.uuid4() - - process_router = start_supervised!(TransferMoneyProcessManager) - - # Create two bank accounts - :ok = open_account(account_number1, 1_000) - :ok = open_account(account_number2, 500) - - # Transfer funds between account 1 and account 2 - :ok = transfer_funds(transfer_uuid, account_number1, account_number2, 100) - - assert_receive_event(BankApp, MoneyTransferRequested, fn event -> - assert event.debit_account == account_number1 - assert event.credit_account == account_number2 - assert event.amount == 100 - end) - - assert_receive_event(BankApp, MoneyWithdrawn, fn event -> - assert event.account_number == account_number1 - assert event.amount == 100 - assert event.balance == 900 - end) - - assert_receive_event(BankApp, MoneyDeposited, fn event -> - assert event.account_number == account_number2 - assert event.amount == 100 - assert event.balance == 600 - end) - - assert [{^transfer_uuid, _}] = ProcessRouter.process_instances(process_router) - end - - defp open_account(account_number, initial_balance) do - command = %OpenAccount{account_number: account_number, initial_balance: initial_balance} - - BankRouter.dispatch(command, application: BankApp) - end - - defp transfer_funds(transfer_uuid, from_account_number, to_account_number2, amount) do - command = %TransferMoney{ - transfer_uuid: transfer_uuid, - debit_account: from_account_number, - credit_account: to_account_number2, - amount: amount - } - - BankRouter.dispatch(command, application: BankApp) - end -end diff --git a/test/process_managers/process_manager_routing_test.exs b/test/process_managers/process_manager_routing_test.exs deleted file mode 100644 index 8b58c327..00000000 --- a/test/process_managers/process_manager_routing_test.exs +++ /dev/null @@ -1,229 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerRoutingTest do - use Commanded.MockEventStoreCase - - alias Commanded.Helpers.EventFactory - alias Commanded.Helpers.Wait - alias Commanded.ProcessManagers.ProcessRouter - alias Commanded.ProcessManagers.RoutingProcessManager - alias Commanded.ProcessManagers.RoutingProcessManager.Continued - alias Commanded.ProcessManagers.RoutingProcessManager.Errored - alias Commanded.ProcessManagers.RoutingProcessManager.Started - alias Commanded.ProcessManagers.RoutingProcessManager.StartedFromMetadata - alias Commanded.ProcessManagers.RoutingProcessManager.Stopped - alias Commanded.UUID - - setup do - mock_event_store() - - {:ok, pid} = RoutingProcessManager.start_link() - - [pid: pid, process_uuid: UUID.uuid4()] - end - - describe "process manager routing" do - test "should start instance on `:start`", %{pid: pid, process_uuid: process_uuid} do - send_events(pid, [%Started{process_uuid: process_uuid, reply_to: self()}]) - - instance = wait_for_instance(pid, process_uuid) - - assert_receive {:started, ^instance} - end - - test "should start instance on `:start` and be able to access metadata", - %{ - pid: pid, - process_uuid: process_uuid - } do - metadata = %{"process_uuid" => process_uuid} - send_events(pid, [%StartedFromMetadata{reply_to: self()}], 1, metadata: metadata) - - instance = wait_for_instance(pid, process_uuid) - - assert_receive {:started, ^instance} - end - - test "should continue existing instance on `:start`", %{pid: pid, process_uuid: process_uuid} do - send_events(pid, [ - %Started{process_uuid: process_uuid, reply_to: self()}, - %Started{process_uuid: process_uuid, reply_to: self()} - ]) - - instance = wait_for_instance(pid, process_uuid) - - assert_receive {:started, ^instance} - assert_receive {:started, ^instance} - end - - test "should continue instance on `:continue`", %{pid: pid, process_uuid: process_uuid} do - send_events(pid, [ - %Started{process_uuid: process_uuid, reply_to: self()}, - %Continued{process_uuid: process_uuid, reply_to: self()} - ]) - - instance = wait_for_instance(pid, process_uuid) - - assert_receive {:started, ^instance} - assert_receive {:continued, ^instance} - end - - test "should start instance on `:continue`", %{pid: pid, process_uuid: process_uuid} do - send_events(pid, [%Continued{process_uuid: process_uuid, reply_to: self()}]) - - instance = wait_for_instance(pid, process_uuid) - - refute_receive {:started, ^instance} - assert_receive {:continued, ^instance} - end - - test "should stop instance on `:stop`", %{pid: pid, process_uuid: process_uuid} do - expect(MockEventStore, :delete_snapshot, fn _event_store, _process_uuid -> :ok end) - - send_events(pid, [%Started{process_uuid: process_uuid, reply_to: self()}]) - - instance = wait_for_instance(pid, process_uuid) - ref = Process.monitor(instance) - - send_events(pid, [%Stopped{process_uuid: process_uuid}], 2) - - assert_receive {:started, ^instance} - assert_receive {:DOWN, ^ref, :process, ^instance, :normal} - end - - test "should ignore an empty list returned on `:start`", %{ - pid: pid, - process_uuid: process_uuid - } do - send_events(pid, [ - %Started{process_uuid: [], reply_to: self()}, - %Started{process_uuid: process_uuid, reply_to: self()} - ]) - - instance = wait_for_instance(pid, process_uuid) - - assert_receive {:started, ^instance} - refute_receive {:started, _instance} - end - end - - describe "process manager strict routing" do - test "should start instance on `:start!`", %{pid: pid, process_uuid: process_uuid} do - send_events(pid, [%Started{process_uuid: process_uuid, reply_to: self(), strict?: true}]) - - instance = wait_for_instance(pid, process_uuid) - - assert_receive {:started, ^instance} - end - - test "should error on `:start!` when instance already started", %{ - pid: pid, - process_uuid: process_uuid - } do - Process.unlink(pid) - ref = Process.monitor(pid) - - send_events(pid, [ - %Started{process_uuid: process_uuid, reply_to: self(), strict?: true}, - %Started{process_uuid: process_uuid, reply_to: self(), strict?: true} - ]) - - assert_receive {:started, _instance} - refute_receive {:started, _instance} - assert_receive {:error, {:start!, :process_already_started}} - assert_receive {:DOWN, ^ref, :process, ^pid, {:start!, :process_already_started}} - end - - test "should continue instance on `:continue!`", %{pid: pid, process_uuid: process_uuid} do - send_events(pid, [ - %Started{process_uuid: process_uuid, reply_to: self(), strict?: true}, - %Continued{process_uuid: process_uuid, reply_to: self(), strict?: true} - ]) - - instance = wait_for_instance(pid, process_uuid) - - assert_receive {:started, ^instance} - assert_receive {:continued, ^instance} - end - - test "should error on `:continue` when instance not already started", %{ - pid: pid, - process_uuid: process_uuid - } do - Process.unlink(pid) - ref = Process.monitor(pid) - - send_events(pid, [%Continued{process_uuid: process_uuid, reply_to: self(), strict?: true}]) - - refute_receive {:continued, _instance} - assert_receive {:error, {:continue!, :process_not_started}} - assert_receive {:DOWN, ^ref, :process, ^pid, {:continue!, :process_not_started}} - end - - test "should rescue `interested?` errors", %{ - pid: pid, - process_uuid: process_uuid - } do - Process.unlink(pid) - ref = Process.monitor(pid) - - send_events(pid, [ - %Errored{process_uuid: process_uuid, reply_to: self(), on_error: {:stop, :error}} - ]) - - assert_receive {:error, %RuntimeError{message: "error"}} - assert_receive {:DOWN, ^ref, :process, ^pid, :error} - end - - test "should continue processing events after `:skip` error", %{ - pid: pid, - process_uuid: process_uuid - } do - ref = Process.monitor(pid) - - send_events(pid, [ - %Started{process_uuid: process_uuid, reply_to: self()}, - %Errored{process_uuid: process_uuid, reply_to: self(), on_error: :skip}, - %Continued{process_uuid: process_uuid, reply_to: self()} - ]) - - instance = wait_for_instance(pid, process_uuid) - - assert_receive {:started, ^instance} - assert_receive {:error, %RuntimeError{message: "error"}} - assert_receive {:continued, ^instance} - refute_receive {:DOWN, ^ref, :process, ^pid, _} - end - end - - defp mock_event_store do - expect(MockEventStore, :subscribe_to, fn - _event_store, :all, name, pid, :origin, _opts -> - assert is_binary(name) - assert is_pid(pid) - - send(pid, {:subscribed, self()}) - - {:ok, self()} - end) - - stub(MockEventStore, :read_snapshot, fn _event_store, _snapshot_uuid -> - {:error, :snapshot_not_found} - end) - - stub(MockEventStore, :record_snapshot, fn _event_store, _snapshot -> :ok end) - stub(MockEventStore, :ack_event, fn _event_store, _pid, _event -> :ok end) - end - - defp send_events(pid, events, initial_event_number \\ 1, opts \\ []) do - recorded_events = EventFactory.map_to_recorded_events(events, initial_event_number, opts) - - send(pid, {:events, recorded_events}) - end - - defp wait_for_instance(pid, process_uuid) do - Wait.until(fn -> - assert {:ok, instance} = ProcessRouter.process_instance(pid, process_uuid) - - instance - end) - end -end diff --git a/test/process_managers/process_manager_subscription_test.exs b/test/process_managers/process_manager_subscription_test.exs deleted file mode 100644 index 3210692d..00000000 --- a/test/process_managers/process_manager_subscription_test.exs +++ /dev/null @@ -1,115 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerSubscriptionTest do - use Commanded.MockEventStoreCase - - alias Commanded.MockedApp - alias Commanded.ProcessManagers.ProcessRouter - - defmodule ExampleProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: MockedApp, - name: "ExampleProcessManager" - - @derive Jason.Encoder - defstruct [:data] - end - - describe "process manager subscription" do - test "should monitor subscription and terminate process manager on shutdown" do - {:ok, subscription} = start_subscription() - - expect_subscribe_to(subscription) - - {:ok, pm} = start_process_manager(subscription) - - Process.unlink(pm) - ref = Process.monitor(pm) - - shutdown_subscription(subscription) - - assert_receive {:DOWN, ^ref, :process, ^pm, _reason} - end - end - - test "should retry subscription on error" do - {:ok, subscription} = start_subscription() - - reply_to = self() - - # First and second subscription attempts fail - expect(MockEventStore, :subscribe_to, 2, fn - _event_store_meta, :all, "ExampleProcessManager", pm, :origin, _opts -> - send(reply_to, {:subscribe_to, pm}) - - {:error, :subscription_already_exists} - end) - - # Third subscription attempt succeeds - expect_subscribe_to(subscription) - - {:ok, pm} = ExampleProcessManager.start_link() - - assert_receive {:subscribe_to, ^pm} - assert_subscription_timer(pm, 1..3_000) - refute_receive {:subscribed, ^subscription} - - send(pm, :subscribe_to_events) - - assert_receive {:subscribe_to, ^pm} - assert_subscription_timer(pm, 1_000..9_000) - refute_receive {:subscribed, ^subscription} - - send(pm, :subscribe_to_events) - - assert_receive {:subscribed, ^subscription} - end - - defp assert_subscription_timer(pm, expected_timer_range) do - %ProcessRouter.State{subscribe_timer: subscribe_timer} = :sys.get_state(pm) - - assert is_reference(subscribe_timer) - - timer = Process.read_timer(subscribe_timer) - - assert is_integer(timer) - assert timer in expected_timer_range - end - - defp start_subscription do - pid = - spawn_link(fn -> - receive do - :shutdown -> :ok - end - end) - - {:ok, pid} - end - - defp expect_subscribe_to(subscription) do - reply_to = self() - - expect(MockEventStore, :subscribe_to, fn - _event_store_meta, :all, "ExampleProcessManager", pm, :origin, _opts -> - send(pm, {:subscribed, subscription}) - send(reply_to, {:subscribed, subscription}) - - {:ok, subscription} - end) - end - - defp start_process_manager(subscription) do - {:ok, pid} = ExampleProcessManager.start_link() - - assert_receive {:subscribed, ^subscription} - - {:ok, pid} - end - - defp shutdown_subscription(subscription) do - ref = Process.monitor(subscription) - - send(subscription, :shutdown) - - assert_receive {:DOWN, ^ref, :process, _, _} - end -end diff --git a/test/process_managers/process_manager_telemetry_test.exs b/test/process_managers/process_manager_telemetry_test.exs deleted file mode 100644 index 97b48d3c..00000000 --- a/test/process_managers/process_manager_telemetry_test.exs +++ /dev/null @@ -1,317 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerTelemetryTest do - use ExUnit.Case - - import Mox - - alias Commanded.ProcessManagers.ProcessManagerInstance - alias Commanded.UUID - - setup :set_mox_global - setup :verify_on_exit! - - defmodule Commands do - defmodule Ok do - defstruct [:message] - end - end - - defmodule Event do - @derive Jason.Encoder - defstruct [:message, :type] - end - - defmodule Agg do - defstruct [] - - def execute(_, _) do - [] - end - - def apply(_, _) do - %__MODULE__{} - end - end - - defmodule Router do - use Commanded.Commands.Router - - dispatch(Commands.Ok, to: Agg, identity: :message) - end - - defmodule App do - alias Commanded.EventStore.Adapters.InMemory - alias Commanded.Serialization.JsonSerializer - - use Commanded.Application, - otp_app: :app, - event_store: [ - adapter: InMemory, - serializer: JsonSerializer - ], - pubsub: :local, - registry: :local - - router Router - end - - defmodule ExamplePM do - use Commanded.ProcessManagers.ProcessManager, - application: App, - name: __MODULE__ - - alias Commands.Ok - - @derive Jason.Encoder - defstruct message: "init" - - def handle(%ExamplePM{}, %Event{type: type, message: message}) do - case type do - "ok" -> %Ok{message: message} - "error" -> {:error, message} - "retry" -> {:error, :retry} - "raise" -> raise message - end - end - - def apply(%ExamplePM{}, %Event{message: message}), - do: %ExamplePM{message: message} - - def error({:error, :retry}, %Event{}, failure_context) do - if failure_context.context[:retried?] do - :skip - else - {:retry, %{retried?: true}} - end - end - end - - alias Commands.Ok - - setup do - start_supervised!(App) - - attach_telemetry() - - :ok - end - - @handler "test-pm-handler" - - describe "process manager telemetry" do - test "emit `[:commanded, :process_manager, :handle, :start]` event" do - uuid = UUID.uuid4() - - {:ok, instance} = start_process_manager_instance(uuid) - - event = to_recorded_event(%Event{message: "start", type: "ok"}) - - :ok = ProcessManagerInstance.process_event(instance, event) - - assert_receive {[:commanded, :process_manager, :handle, :start], 1, measurements, metadata} - - assert match?(%{system_time: _system_time}, measurements) - - assert match?( - %{ - application: App, - process_manager_module: ExamplePM, - process_manager_name: "ExamplePM", - process_state: %ExamplePM{message: "init"}, - process_uuid: ^uuid, - recorded_event: ^event - }, - metadata - ) - - assert_receive {[:commanded, :process_manager, :handle, :stop], 2, _measurements, _metadata} - - refute_receive {[:commanded, :process_manager, :handle, :exception], _, _measurements, - _metadata} - end - - test "emit `[:commanded, :process_manager, :handle, :stop]` event" do - uuid = UUID.uuid4() - - {:ok, instance} = start_process_manager_instance(uuid) - - event = to_recorded_event(%Event{message: "start", type: "ok"}) - - :ok = ProcessManagerInstance.process_event(instance, event) - - assert_receive {[:commanded, :process_manager, :handle, :start], 1, _measurements, - _metadata} - - assert_receive {[:commanded, :process_manager, :handle, :stop], 2, measurements, metadata} - - assert match?(%{duration: _}, measurements) - assert is_integer(measurements.duration) - - assert match?( - %{ - application: App, - process_manager_module: ExamplePM, - process_manager_name: "ExamplePM", - process_state: %ExamplePM{message: "init"}, - process_uuid: ^uuid, - recorded_event: ^event, - commands: [%Ok{message: "start"}] - }, - metadata - ) - - refute_receive {[:commanded, :process_manager, :handle, :exception], _num, _measurements, - _metadata} - end - - test "emit `[:commanded, :process_manager, :handle, :stop]` with error event" do - uuid = UUID.uuid4() - - {:ok, instance} = start_process_manager_instance(uuid) - - event = to_recorded_event(%Event{message: "stop", type: "error"}) - - :ok = ProcessManagerInstance.process_event(instance, event) - - assert_receive {[:commanded, :process_manager, :handle, :start], 1, _measurements, - _metadata} - - assert_receive {[:commanded, :process_manager, :handle, :stop], 2, measurements, metadata} - - assert match?(%{duration: _}, measurements) - assert is_integer(measurements.duration) - - assert match?( - %{ - application: App, - process_manager_module: ExamplePM, - process_manager_name: "ExamplePM", - process_state: %ExamplePM{message: "init"}, - process_uuid: ^uuid, - recorded_event: ^event, - error: "stop" - }, - metadata - ) - - refute_receive {[:commanded, :process_manager, :handle, :exception], _num, _measurements, - _metadata} - end - - test "events are emitted with discrete start/stop on retries" do - uuid = UUID.uuid4() - - {:ok, instance} = start_process_manager_instance(uuid) - - event = to_recorded_event(%Event{message: "retry", type: "retry"}) - - :ok = ProcessManagerInstance.process_event(instance, event) - - assert_receive {[:commanded, :process_manager, :handle, :start], 1, _measurements, - _metadata} - - assert_receive {[:commanded, :process_manager, :handle, :stop], 2, measurements, metadata} - - assert match?(%{duration: _}, measurements) - assert is_integer(measurements.duration) - - assert match?( - %{ - application: App, - process_manager_module: ExamplePM, - process_manager_name: "ExamplePM", - process_state: %ExamplePM{message: "init"}, - process_uuid: ^uuid, - recorded_event: ^event, - error: :retry - }, - metadata - ) - - refute_receive {[:commanded, :process_manager, :handle, :exception], _num, _measurements, - _metadata} - - assert_receive {[:commanded, :process_manager, :handle, :start], 3, _measurements, - _metadata} - - assert_receive {[:commanded, :process_manager, :handle, :stop], 4, _measurements, _metadata} - end - - @tag capture_log: true - test "emit `[:commanded, :process_manager, :handle, :exception]` event" do - uuid = UUID.uuid4() - - {:ok, instance} = start_process_manager_instance(uuid) - - event = to_recorded_event(%Event{message: "exception", type: "raise"}) - - :ok = ProcessManagerInstance.process_event(instance, event) - - assert_receive {[:commanded, :process_manager, :handle, :start], 1, _measurements, - _metadata} - - refute_receive {[:commanded, :process_manager, :handle, :stop], _num, _measurements, - _metadata} - - assert_receive {[:commanded, :process_manager, :handle, :exception], 2, measurements, - metadata} - - assert match?(%{duration: _}, measurements) - assert is_integer(measurements.duration) - - assert match?( - %{ - application: App, - process_manager_module: ExamplePM, - process_manager_name: "ExamplePM", - process_state: %ExamplePM{message: "init"}, - process_uuid: ^uuid, - recorded_event: ^event, - kind: :error, - reason: %RuntimeError{message: "exception"}, - stacktrace: _ - }, - metadata - ) - end - end - - defp attach_telemetry do - agent = start_supervised!({Agent, fn -> 1 end}) - - :telemetry.attach_many( - @handler, - [ - [:commanded, :process_manager, :handle, :start], - [:commanded, :process_manager, :handle, :stop], - [:commanded, :process_manager, :handle, :exception] - ], - fn event_name, measurements, metadata, reply_to -> - num = Agent.get_and_update(agent, fn num -> {num, num + 1} end) - send(reply_to, {event_name, num, measurements, metadata}) - end, - self() - ) - - on_exit(fn -> - :telemetry.detach(@handler) - end) - end - - defp start_process_manager_instance(transfer_uuid) do - start_supervised( - {ProcessManagerInstance, - application: App, - idle_timeout: :infinity, - process_manager_name: "ExamplePM", - process_manager_module: ExamplePM, - process_router: self(), - process_uuid: transfer_uuid} - ) - end - - defp to_recorded_event(event) do - alias Commanded.EventStore.RecordedEvent - - %RecordedEvent{event_number: 1, stream_id: "stream-id", stream_version: 1, data: event} - end -end diff --git a/test/process_managers/process_manager_timeout_test.exs b/test/process_managers/process_manager_timeout_test.exs deleted file mode 100644 index 98516fb6..00000000 --- a/test/process_managers/process_manager_timeout_test.exs +++ /dev/null @@ -1,67 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessManagerTimeoutTest do - use ExUnit.Case - - alias Commanded.Helpers.Wait - alias Commanded.ProcessManagers.ExampleAggregate.Commands.{Pause, Start} - - alias Commanded.ProcessManagers.{ - ExampleApp, - ExampleProcessManager, - ExampleRouter, - ProcessRouter - } - - alias Commanded.UUID - - setup do - start_supervised!(ExampleApp) - - :ok - end - - test "should not timeout and shutdown process manager by default" do - aggregate_uuid = UUID.uuid4() - - {:ok, process_router} = ExampleProcessManager.start_link() - router_ref = Process.monitor(process_router) - - :ok = ExampleRouter.dispatch(%Start{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - process_instance = wait_for_process_instance(process_router, aggregate_uuid) - instance_ref = Process.monitor(process_instance) - - :ok = ExampleRouter.dispatch(%Pause{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - # Should not shutdown process manager or instance - refute_receive {:DOWN, ^router_ref, _, _, _} - refute_receive {:DOWN, ^instance_ref, _, _, _} - end - - test "should timeout and shutdown process manager when `event_timeout` configured" do - aggregate_uuid = UUID.uuid4() - - {:ok, process_router} = ExampleProcessManager.start_link(event_timeout: 100) - - router_ref = Process.monitor(process_router) - Process.unlink(process_router) - - :ok = ExampleRouter.dispatch(%Start{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - process_instance = wait_for_process_instance(process_router, aggregate_uuid) - instance_ref = Process.monitor(process_instance) - - :ok = ExampleRouter.dispatch(%Pause{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - # Should shutdown process manager and instance - assert_receive {:DOWN, ^router_ref, _, _, :event_timeout} - assert_receive {:DOWN, ^instance_ref, _, _, :shutdown} - end - - defp wait_for_process_instance(process_router, aggregate_uuid) do - Wait.until(fn -> - assert {:ok, instance} = ProcessRouter.process_instance(process_router, aggregate_uuid) - - instance - end) - end -end diff --git a/test/process_managers/process_router_process_pending_events_test.exs b/test/process_managers/process_router_process_pending_events_test.exs deleted file mode 100644 index da0a21ff..00000000 --- a/test/process_managers/process_router_process_pending_events_test.exs +++ /dev/null @@ -1,203 +0,0 @@ -defmodule Commanded.ProcessManagers.ProcessRouterProcessPendingEventsTest do - use ExUnit.Case - - import Commanded.Assertions.EventAssertions - import Commanded.Enumerable - - alias Commanded.EventStore - alias Commanded.Helpers.Wait - - alias Commanded.ProcessManagers.{ - ExampleApp, - ExampleProcessManager, - ExampleRouter, - ProcessManagerInstance, - ProcessRouter - } - - alias Commanded.ProcessManagers.ExampleAggregate.Commands.{Publish, Start} - - alias Commanded.ProcessManagers.ExampleAggregate.Events.{ - Interested, - Started, - Stopped, - Uninterested - } - - alias Commanded.UUID - - setup do - start_supervised!(ExampleApp) - - :ok - end - - test "should start process manager instance and successfully dispatch command" do - aggregate_uuid = UUID.uuid4() - - {:ok, process_router} = ExampleProcessManager.start_link() - - :ok = ExampleRouter.dispatch(%Start{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - # Dispatch command to publish multiple events and trigger dispatch of the stop command - :ok = - ExampleRouter.dispatch( - %Publish{ - aggregate_uuid: aggregate_uuid, - interesting: 10, - uninteresting: 1 - }, - application: ExampleApp - ) - - assert_receive_event(ExampleApp, Stopped, fn event -> - assert event.aggregate_uuid == aggregate_uuid - end) - - events = EventStore.stream_forward(ExampleApp, aggregate_uuid) |> Enum.to_list() - - assert pluck(events, :data) == [ - %Started{aggregate_uuid: aggregate_uuid}, - %Interested{aggregate_uuid: aggregate_uuid, index: 1}, - %Interested{aggregate_uuid: aggregate_uuid, index: 2}, - %Interested{aggregate_uuid: aggregate_uuid, index: 3}, - %Interested{aggregate_uuid: aggregate_uuid, index: 4}, - %Interested{aggregate_uuid: aggregate_uuid, index: 5}, - %Interested{aggregate_uuid: aggregate_uuid, index: 6}, - %Interested{aggregate_uuid: aggregate_uuid, index: 7}, - %Interested{aggregate_uuid: aggregate_uuid, index: 8}, - %Interested{aggregate_uuid: aggregate_uuid, index: 9}, - %Interested{aggregate_uuid: aggregate_uuid, index: 10}, - %Uninterested{aggregate_uuid: aggregate_uuid, index: 1}, - %Stopped{aggregate_uuid: aggregate_uuid} - ] - - Wait.until(fn -> - # Process instance should be stopped - assert ProcessRouter.process_instance(process_router, aggregate_uuid) == - {:error, :process_manager_not_found} - - # Process state snapshot should be deleted - assert EventStore.read_snapshot( - ExampleApp, - "\"ExampleProcessManager\"-\"#{aggregate_uuid}\"" - ) == - {:error, :snapshot_not_found} - end) - end - - test "should ignore uninteresting events" do - aggregate_uuid = UUID.uuid4() - - {:ok, process_router} = ExampleProcessManager.start_link() - - :ok = ExampleRouter.dispatch(%Start{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - # Dispatch commands to publish a mix of interesting and uninteresting events for the process router - :ok = - ExampleRouter.dispatch( - %Publish{ - aggregate_uuid: aggregate_uuid, - interesting: 0, - uninteresting: 2 - }, - application: ExampleApp - ) - - :ok = - ExampleRouter.dispatch( - %Publish{ - aggregate_uuid: aggregate_uuid, - interesting: 0, - uninteresting: 2 - }, - application: ExampleApp - ) - - :ok = - ExampleRouter.dispatch( - %Publish{ - aggregate_uuid: aggregate_uuid, - interesting: 10, - uninteresting: 0 - }, - application: ExampleApp - ) - - assert_receive_event(ExampleApp, Stopped, fn event -> - assert event.aggregate_uuid == aggregate_uuid - end) - - events = EventStore.stream_forward(ExampleApp, aggregate_uuid) |> Enum.to_list() - - assert pluck(events, :data) == [ - %Started{aggregate_uuid: aggregate_uuid}, - %Uninterested{aggregate_uuid: aggregate_uuid, index: 1}, - %Uninterested{aggregate_uuid: aggregate_uuid, index: 2}, - %Uninterested{aggregate_uuid: aggregate_uuid, index: 1}, - %Uninterested{aggregate_uuid: aggregate_uuid, index: 2}, - %Interested{aggregate_uuid: aggregate_uuid, index: 1}, - %Interested{aggregate_uuid: aggregate_uuid, index: 2}, - %Interested{aggregate_uuid: aggregate_uuid, index: 3}, - %Interested{aggregate_uuid: aggregate_uuid, index: 4}, - %Interested{aggregate_uuid: aggregate_uuid, index: 5}, - %Interested{aggregate_uuid: aggregate_uuid, index: 6}, - %Interested{aggregate_uuid: aggregate_uuid, index: 7}, - %Interested{aggregate_uuid: aggregate_uuid, index: 8}, - %Interested{aggregate_uuid: aggregate_uuid, index: 9}, - %Interested{aggregate_uuid: aggregate_uuid, index: 10}, - %Stopped{aggregate_uuid: aggregate_uuid} - ] - - Wait.until(fn -> - # Process instance should be stopped - assert ProcessRouter.process_instance(process_router, aggregate_uuid) == - {:error, :process_manager_not_found} - end) - end - - test "should ignore past events when starting subscription from current" do - aggregate_uuid = UUID.uuid4() - - :ok = ExampleRouter.dispatch(%Start{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - :ok = - ExampleRouter.dispatch( - %Publish{ - aggregate_uuid: aggregate_uuid, - interesting: 4, - uninteresting: 0 - }, - application: ExampleApp - ) - - wait_for_event(ExampleApp, Interested, fn event -> event.index == 4 end) - - {:ok, process_router} = ExampleProcessManager.start_link(start_from: :current) - - assert ProcessRouter.process_instances(process_router) == [] - - :ok = ExampleRouter.dispatch(%Start{aggregate_uuid: aggregate_uuid}, application: ExampleApp) - - :ok = - ExampleRouter.dispatch( - %Publish{ - aggregate_uuid: aggregate_uuid, - interesting: 6, - uninteresting: 0 - }, - application: ExampleApp - ) - - wait_for_event(ExampleApp, Interested, fn event -> event.index == 6 end) - - Wait.until(fn -> - assert {:ok, process_instance} = - ProcessRouter.process_instance(process_router, aggregate_uuid) - - %{items: items} = ProcessManagerInstance.process_state(process_instance) - - assert items == [1, 2, 3, 4, 5, 6] - end) - end -end diff --git a/test/process_managers/resume_process_manager_test.exs b/test/process_managers/resume_process_manager_test.exs deleted file mode 100644 index c4fc8beb..00000000 --- a/test/process_managers/resume_process_manager_test.exs +++ /dev/null @@ -1,68 +0,0 @@ -defmodule Commanded.ProcessManagers.ResumeProcessManagerTest do - use ExUnit.Case - - import Commanded.Assertions.EventAssertions - - alias Commanded.Helpers.{ProcessHelper, Wait} - - alias Commanded.ProcessManagers.{ - ProcessManagerInstance, - ProcessRouter, - ResumeApp, - ResumeProcessManager, - ResumeRouter - } - - alias Commanded.ProcessManagers.ResumeAggregate.Commands.{ResumeProcess, StartProcess} - alias Commanded.ProcessManagers.ResumeAggregate.Events.{ProcessResumed, ProcessStarted} - alias Commanded.UUID - - setup do - start_supervised!(ResumeApp) - - :ok - end - - test "should resume a process manager with same state when process restarts" do - {:ok, process_router} = ResumeProcessManager.start_link() - - process_uuid = UUID.uuid4() - command = %StartProcess{process_uuid: process_uuid, status: "start"} - - :ok = ResumeRouter.dispatch(command, application: ResumeApp) - - assert_receive_event(ResumeApp, ProcessStarted, fn event -> - assert event.process_uuid == process_uuid - assert event.status == "start" - end) - - # wait for process instance to receive event - Wait.until(fn -> - assert {:ok, process_instance} = - ProcessRouter.process_instance(process_router, process_uuid) - - assert %{status_history: ["start"]} = ProcessManagerInstance.process_state(process_instance) - end) - - ProcessHelper.shutdown(process_router) - - # wait for subscription to receive DOWN notification and remove subscription's PID - :timer.sleep(1_000) - - {:ok, process_router} = ResumeProcessManager.start_link() - - command = %ResumeProcess{process_uuid: process_uuid, status: "resume"} - - :ok = ResumeRouter.dispatch(command, application: ResumeApp) - - wait_for_event(ResumeApp, ProcessResumed, fn event -> event.process_uuid == process_uuid end) - - Wait.until(fn -> - assert {:ok, process_instance} = - ProcessRouter.process_instance(process_router, process_uuid) - - state = ProcessManagerInstance.process_state(process_instance) - assert state.status_history == ["start", "resume"] - end) - end -end diff --git a/test/process_managers/support/after_command_process_manager.ex b/test/process_managers/support/after_command_process_manager.ex deleted file mode 100644 index 72bf2462..00000000 --- a/test/process_managers/support/after_command_process_manager.ex +++ /dev/null @@ -1,57 +0,0 @@ -defmodule Commanded.ProcessManagers.AfterCommandProcessManager do - @moduledoc false - - alias Commanded.EventStore.EnrichedMetadata - alias Commanded.ProcessManagers.AfterCommandProcessManager - alias Commanded.ProcessManagers.ExampleAggregate.Commands.{Continue, Stop} - alias Commanded.ProcessManagers.ExampleAggregate.Events.{Interested, Started} - alias Commanded.ProcessManagers.ExampleApp - - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "AfterCommandProcessManager" - - @derive Jason.Encoder - defstruct [:status, items: []] - - def interested?(%Started{aggregate_uuid: aggregate_uuid}, _metadata), - do: {:start, aggregate_uuid} - - def interested?(%Interested{aggregate_uuid: aggregate_uuid}, _metadata), - do: {:continue, aggregate_uuid} - - def handle(%AfterCommandProcessManager{}, %Interested{index: 1, aggregate_uuid: aggregate_uuid}) do - %Continue{aggregate_uuid: aggregate_uuid} - end - - def handle(%AfterCommandProcessManager{}, %Interested{index: 10, aggregate_uuid: aggregate_uuid}) do - %Stop{aggregate_uuid: aggregate_uuid} - end - - # State mutators - - def apply(%AfterCommandProcessManager{} = process_manager, %Started{}) do - %AfterCommandProcessManager{process_manager | status: :started} - end - - def apply(%AfterCommandProcessManager{items: items} = process_manager, %Interested{index: index}) do - %AfterCommandProcessManager{process_manager | items: items ++ [index]} - end - - # after_command/2 callback - def after_command(%AfterCommandProcessManager{}, %Stop{}) do - :stop - end - - # after_command/3 callback - def after_command(%AfterCommandProcessManager{}, %Continue{}, %EnrichedMetadata{} = metadata) do - %EnrichedMetadata{metadata: user_metadata} = metadata - %{"notify_to" => notify_to} = user_metadata - - notify_to - |> :erlang.list_to_pid() - |> Process.send(:metadata_available, []) - - :continue - end -end diff --git a/test/process_managers/support/dynamic_process_manager.ex b/test/process_managers/support/dynamic_process_manager.ex deleted file mode 100644 index 2bccf035..00000000 --- a/test/process_managers/support/dynamic_process_manager.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule Commanded.ProcessManagers.DynamicProcessManager do - @moduledoc false - - alias Commanded.ExampleDomain.BankAccount.Commands.DepositMoney - alias Commanded.ExampleDomain.BankAccount.Events.{BankAccountOpened, MoneyDeposited} - alias Commanded.ExampleDomain.BankApp - alias Commanded.ProcessManagers.DynamicProcessManager - alias Commanded.UUID - - use Commanded.ProcessManagers.ProcessManager, - application: BankApp, - name: __MODULE__ - - @derive Jason.Encoder - defstruct [:account_number] - - def interested?(%BankAccountOpened{account_number: account_number}), - do: {:start, account_number} - - def interested?(%MoneyDeposited{account_number: account_number}), - do: {:continue!, account_number} - - def handle(%DynamicProcessManager{}, %BankAccountOpened{} = event) do - %BankAccountOpened{account_number: account_number} = event - - send_reply(event) - - # Deposit account opening welcome bonus - [ - %DepositMoney{account_number: account_number, transfer_uuid: UUID.uuid4(), amount: 100} - ] - end - - def handle(%DynamicProcessManager{}, %MoneyDeposited{} = event) do - send_reply(event) - - [] - end - - def apply(%DynamicProcessManager{} = pm, %BankAccountOpened{} = event) do - %BankAccountOpened{account_number: account_number} = event - - %DynamicProcessManager{pm | account_number: account_number} - end - - defp send_reply(event) do - reply_to = Agent.get(:reply_to, fn reply_to -> reply_to end) - - send(reply_to, {:event, self(), event}) - end -end diff --git a/test/process_managers/support/error/default_error_handling_process_manager.ex b/test/process_managers/support/error/default_error_handling_process_manager.ex deleted file mode 100644 index 9f33d439..00000000 --- a/test/process_managers/support/error/default_error_handling_process_manager.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Commanded.ProcessManagers.DefaultErrorHandlingProcessManager do - @moduledoc false - - alias Commanded.ProcessManagers.{DefaultErrorHandlingProcessManager, ErrorApp} - alias Commanded.ProcessManagers.ErrorAggregate.Commands.AttemptProcess - alias Commanded.ProcessManagers.ErrorAggregate.Events.ProcessStarted - - use Commanded.ProcessManagers.ProcessManager, - application: ErrorApp, - name: "DefaultErrorHandlingProcessManager" - - defstruct [:process_uuid] - - def interested?(%ProcessStarted{process_uuid: process_uuid}), do: {:start, process_uuid} - - def handle(%DefaultErrorHandlingProcessManager{}, %ProcessStarted{process_uuid: process_uuid}) do - %AttemptProcess{process_uuid: process_uuid} - end -end diff --git a/test/process_managers/support/error/error_aggregate.ex b/test/process_managers/support/error/error_aggregate.ex deleted file mode 100644 index 87fe4801..00000000 --- a/test/process_managers/support/error/error_aggregate.ex +++ /dev/null @@ -1,107 +0,0 @@ -defmodule Commanded.ProcessManagers.ErrorAggregate do - @moduledoc false - @derive Jason.Encoder - defstruct [:process_uuid] - - defmodule Commands do - defmodule StartProcess do - @derive Jason.Encoder - defstruct [:process_uuid, :strategy, :delay, :reply_to] - end - - defmodule RaiseError do - @derive Jason.Encoder - defstruct [:process_uuid, :message, :reply_to] - end - - defmodule RaiseException do - @derive Jason.Encoder - defstruct [:process_uuid, :message, :reply_to] - end - - defmodule AttemptProcess do - @derive Jason.Encoder - defstruct [:process_uuid, :strategy, :delay, :reply_to] - end - - defmodule ContinueProcess do - @derive Jason.Encoder - defstruct [:process_uuid, :reply_to] - end - end - - defmodule Events do - defmodule ProcessStarted do - @derive Jason.Encoder - defstruct [:process_uuid, :strategy, :delay, :reply_to] - end - - defmodule ProcessContinued do - @derive Jason.Encoder - defstruct [:process_uuid, :reply_to] - end - - defmodule ProcessError do - @derive Jason.Encoder - defstruct [:process_uuid, :message, :reply_to] - end - - defmodule ProcessException do - @derive Jason.Encoder - defstruct [:process_uuid, :message, :reply_to] - end - - defmodule ProcessApplyException do - @derive Jason.Encoder - defstruct [:process_uuid, :message, :reply_to] - end - - defmodule ProcessDispatchException do - @derive Jason.Encoder - defstruct [:process_uuid, :message, :reply_to] - end - end - - alias Commanded.ProcessManagers.ErrorAggregate - alias Commands.{AttemptProcess, ContinueProcess, RaiseError, RaiseException, StartProcess} - alias Events.{ProcessContinued, ProcessStarted} - - def execute(%ErrorAggregate{}, %StartProcess{} = command) do - %StartProcess{ - process_uuid: process_uuid, - strategy: strategy, - delay: delay, - reply_to: reply_to - } = command - - %ProcessStarted{ - process_uuid: process_uuid, - strategy: strategy, - delay: delay, - reply_to: reply_to - } - end - - def execute(%ErrorAggregate{}, %RaiseError{} = command) do - %RaiseError{message: message} = command - - {:error, message} - end - - def execute(%ErrorAggregate{}, %RaiseException{} = command) do - %RaiseException{message: message} = command - - raise message - end - - def execute(%ErrorAggregate{}, %AttemptProcess{}), - do: {:error, :failed} - - def execute(%ErrorAggregate{}, %ContinueProcess{process_uuid: process_uuid, reply_to: reply_to}), - do: %ProcessContinued{process_uuid: process_uuid, reply_to: reply_to} - - def apply(%ErrorAggregate{} = aggregate, %ProcessStarted{process_uuid: process_uuid}), - do: %ErrorAggregate{aggregate | process_uuid: process_uuid} - - def apply(%ErrorAggregate{} = aggregate, _event), do: aggregate -end diff --git a/test/process_managers/support/error/error_app.ex b/test/process_managers/support/error/error_app.ex deleted file mode 100644 index 2a039bff..00000000 --- a/test/process_managers/support/error/error_app.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Commanded.ProcessManagers.ErrorApp do - @moduledoc false - - use Commanded.Application, otp_app: :commanded - - alias Commanded.ProcessManagers.ErrorRouter - - router(ErrorRouter) -end diff --git a/test/process_managers/support/error/error_handling_process_manager.ex b/test/process_managers/support/error/error_handling_process_manager.ex deleted file mode 100644 index e592da9b..00000000 --- a/test/process_managers/support/error/error_handling_process_manager.ex +++ /dev/null @@ -1,232 +0,0 @@ -defmodule Commanded.ProcessManagers.ErrorHandlingProcessManager do - @moduledoc false - - alias Commanded.ProcessManagers.{ErrorHandlingProcessManager, FailureContext} - - alias Commanded.ProcessManagers.ErrorAggregate.Commands.{ - AttemptProcess, - ContinueProcess, - RaiseException - } - - alias Commanded.ProcessManagers.ErrorAggregate.Events.{ - ProcessApplyException, - ProcessContinued, - ProcessDispatchException, - ProcessError, - ProcessException, - ProcessStarted - } - - alias Commanded.ProcessManagers.ErrorApp - - use Commanded.ProcessManagers.ProcessManager, - application: ErrorApp, - name: "ErrorHandlingProcessManager" - - @derive Jason.Encoder - defstruct [:process_uuid] - - def interested?(%ProcessStarted{process_uuid: process_uuid}), do: {:start, process_uuid} - def interested?(%ProcessError{process_uuid: process_uuid}), do: {:start, process_uuid} - def interested?(%ProcessApplyException{process_uuid: process_uuid}), do: {:start, process_uuid} - def interested?(%ProcessException{process_uuid: process_uuid}), do: {:start, process_uuid} - def interested?(%ProcessContinued{process_uuid: process_uuid}), do: {:continue, process_uuid} - - def interested?(%ProcessDispatchException{process_uuid: process_uuid}), - do: {:start, process_uuid} - - def handle(%ErrorHandlingProcessManager{}, %ProcessStarted{} = event) do - %ProcessStarted{ - process_uuid: process_uuid, - reply_to: reply_to, - strategy: strategy, - delay: delay - } = event - - %AttemptProcess{ - process_uuid: process_uuid, - reply_to: reply_to, - strategy: strategy, - delay: delay - } - end - - def handle(%ErrorHandlingProcessManager{}, %ProcessDispatchException{} = event) do - %ProcessDispatchException{process_uuid: process_uuid, reply_to: reply_to, message: message} = - event - - %RaiseException{process_uuid: process_uuid, reply_to: reply_to, message: message} - end - - # Simulate an error handling an event. - def handle(%ErrorHandlingProcessManager{}, %ProcessError{} = event) do - %ProcessError{message: message} = event - - {:error, message} - end - - # Simulate an exception handling an event. - def handle(%ErrorHandlingProcessManager{}, %ProcessException{} = event) do - %ProcessException{message: message} = event - - raise message - end - - def handle(%ErrorHandlingProcessManager{}, %ProcessContinued{} = event) do - %ProcessContinued{reply_to: reply_to} = event - - reply(reply_to, :process_continued) - - [] - end - - # Simulate an exception applying an event. - def apply(%ErrorHandlingProcessManager{}, %ProcessApplyException{} = event) do - %ProcessApplyException{message: message} = event - - raise message - end - - # Skip events causing errors during event handling - def error({:error, error}, %ProcessError{} = event, %FailureContext{} = failure_context) do - %ProcessError{reply_to: reply_to} = event - - reply(reply_to, {:error, error, failure_context}) - - {:stop, error} - end - - # Stop on exceptions during event handling - def error({:error, error}, %ProcessException{} = event, %FailureContext{} = failure_context) do - %ProcessException{reply_to: reply_to} = event - - reply(reply_to, {:error, error, failure_context}) - - {:stop, error} - end - - # Stop on exceptions during event applying - def error( - {:error, error}, - %ProcessApplyException{} = event, - %FailureContext{} = failure_context - ) do - %ProcessApplyException{reply_to: reply_to} = event - - reply(reply_to, {:error, error, failure_context}) - - {:stop, error} - end - - # Stop on exceptions during command dispatch. - def error({:error, error}, %RaiseException{} = command, %FailureContext{} = failure_context) do - %RaiseException{reply_to: reply_to} = command - - reply(reply_to, {:error, error, failure_context}) - - {:stop, error} - end - - # Stop after three attempts. - def error( - {:error, :failed}, - %AttemptProcess{} = command, - %FailureContext{context: %{attempts: attempts}} = failure_context - ) - when attempts >= 2 do - %AttemptProcess{reply_to: reply_to} = command - - context = record_attempt(failure_context) - reply(reply_to, {:error, :too_many_attempts, context, failure_context}) - - {:stop, :too_many_attempts} - end - - def error( - {:error, :failed}, - %AttemptProcess{strategy: "retry"} = command, - %FailureContext{} = failure_context - ) do - %AttemptProcess{delay: delay, reply_to: reply_to} = command - - context = failure_context |> record_attempt() |> Map.put(:delay, delay) - - reply(reply_to, {:error, :failed, context, failure_context}) - - if is_number(delay) and delay > 0 do - # Retry command with delay - {:retry, delay, context} - else - # Retry command - {:retry, context} - end - end - - def error( - {:error, :failed}, - %AttemptProcess{strategy: "retry_failure_context"} = command, - %FailureContext{} = failure_context - ) do - %AttemptProcess{delay: delay, reply_to: reply_to} = command - - context = failure_context |> record_attempt() |> Map.put(:delay, delay) - failure_context = %FailureContext{failure_context | context: context} - - reply(reply_to, {:error, :failed, failure_context}) - - if is_number(delay) and delay > 0 do - # Retry command with delay - {:retry, delay, failure_context} - else - # Retry command - {:retry, failure_context} - end - end - - # Skip failed command - def error({:error, :failed}, %AttemptProcess{strategy: "skip"} = command, failure_context) do - %AttemptProcess{reply_to: reply_to} = command - - context = record_attempt(failure_context) - reply(reply_to, {:error, :failed, context, failure_context}) - - :skip - end - - # Skip failed command, continue pending - def error( - {:error, :failed}, - %AttemptProcess{strategy: "skip_continue_pending"} = command, - failure_context - ) do - %AttemptProcess{reply_to: reply_to} = command - - context = record_attempt(failure_context) - reply(reply_to, {:error, :failed, context, failure_context}) - - {:skip, :continue_pending} - end - - # Continue with modified command - def error({:error, :failed}, %AttemptProcess{strategy: "continue"} = command, failure_context) do - %AttemptProcess{process_uuid: process_uuid, reply_to: reply_to} = command - - context = record_attempt(failure_context) - reply(reply_to, {:error, :failed, context, failure_context}) - - continue = %ContinueProcess{process_uuid: process_uuid, reply_to: reply_to} - - {:continue, [continue | failure_context.pending_commands], context} - end - - defp record_attempt(%FailureContext{context: context}) do - Map.update(context, :attempts, 1, fn attempts -> attempts + 1 end) - end - - defp reply(reply_to, message) do - pid = :erlang.list_to_pid(reply_to) - - send(pid, message) - end -end diff --git a/test/process_managers/support/error/error_router.ex b/test/process_managers/support/error/error_router.ex deleted file mode 100644 index 51a77ad7..00000000 --- a/test/process_managers/support/error/error_router.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Commanded.ProcessManagers.ErrorRouter do - @moduledoc false - - use Commanded.Commands.Router - - alias Commanded.ProcessManagers.ErrorAggregate - - alias Commanded.ProcessManagers.ErrorAggregate.Commands.{ - AttemptProcess, - ContinueProcess, - RaiseError, - RaiseException, - StartProcess - } - - dispatch [AttemptProcess, ContinueProcess, RaiseError, RaiseException, StartProcess], - to: ErrorAggregate, - identity: :process_uuid -end diff --git a/test/process_managers/support/error/state_error_handling_process_manager.ex b/test/process_managers/support/error/state_error_handling_process_manager.ex deleted file mode 100644 index 130d697f..00000000 --- a/test/process_managers/support/error/state_error_handling_process_manager.ex +++ /dev/null @@ -1,36 +0,0 @@ -defmodule Commanded.ProcessManagers.StateErrorHandlingProcessManager do - @moduledoc false - - alias Commanded.ProcessManagers.ErrorAggregate.Commands.AttemptProcess - alias Commanded.ProcessManagers.ErrorAggregate.Events.ProcessStarted - alias Commanded.ProcessManagers.{ErrorApp, FailureContext} - alias Commanded.ProcessManagers.StateErrorHandlingProcessManager - - use Commanded.ProcessManagers.ProcessManager, - application: ErrorApp, - name: "StateErrorHandlingProcessManager" - - defstruct [:process_uuid, :reply_to] - - def interested?(%ProcessStarted{process_uuid: process_uuid}), do: {:start, process_uuid} - - def handle(%StateErrorHandlingProcessManager{}, %ProcessStarted{process_uuid: process_uuid}) do - %AttemptProcess{process_uuid: process_uuid} - end - - def apply(%StateErrorHandlingProcessManager{}, %ProcessStarted{} = event) do - %ProcessStarted{reply_to: reply_to, process_uuid: process_uuid} = event - - %StateErrorHandlingProcessManager{reply_to: reply_to, process_uuid: process_uuid} - end - - def error({:error, error}, event, %FailureContext{} = failure_context) do - %FailureContext{process_manager_state: %{reply_to: reply_to}} = failure_context - - pid = :erlang.list_to_pid(reply_to) - - send(pid, {:error, error, event, failure_context}) - - {:stop, :stopping} - end -end diff --git a/test/process_managers/support/example_aggregate.ex b/test/process_managers/support/example_aggregate.ex deleted file mode 100644 index efcfe465..00000000 --- a/test/process_managers/support/example_aggregate.ex +++ /dev/null @@ -1,123 +0,0 @@ -defmodule Commanded.ProcessManagers.ExampleAggregate do - @moduledoc false - alias Commanded.ProcessManagers.ExampleAggregate - - @derive Jason.Encoder - defstruct [:uuid, state: nil, items: []] - - defmodule Commands do - defmodule(Start, do: defstruct([:aggregate_uuid])) - defmodule(Publish, do: defstruct([:aggregate_uuid, :interesting, :uninteresting])) - defmodule(Pause, do: defstruct([:aggregate_uuid])) - defmodule(Continue, do: defstruct([:aggregate_uuid])) - defmodule(Stop, do: defstruct([:aggregate_uuid])) - defmodule(Error, do: defstruct([:aggregate_uuid])) - defmodule(Raise, do: defstruct([:aggregate_uuid])) - end - - defmodule Events do - defmodule Started do - @derive Jason.Encoder - defstruct([:aggregate_uuid]) - end - - defmodule Interested do - @derive Jason.Encoder - defstruct([:aggregate_uuid, :index]) - end - - defmodule Uninterested do - @derive Jason.Encoder - defstruct([:aggregate_uuid, :index]) - end - - defmodule Stopped do - @derive Jason.Encoder - defstruct([:aggregate_uuid]) - end - - defmodule Paused do - @derive Jason.Encoder - defstruct([:aggregate_uuid]) - end - - defmodule Errored do - @derive Jason.Encoder - defstruct([:aggregate_uuid]) - end - - defmodule Raised do - @derive Jason.Encoder - defstruct([:aggregate_uuid]) - end - end - - def start(%ExampleAggregate{}, aggregate_uuid) do - %Events.Started{aggregate_uuid: aggregate_uuid} - end - - def publish(%ExampleAggregate{uuid: aggregate_uuid}, interesting, uninteresting) do - Enum.concat( - publish_interesting(aggregate_uuid, interesting, 1), - publish_uninteresting(aggregate_uuid, uninteresting, 1) - ) - end - - def pause(%ExampleAggregate{uuid: aggregate_uuid}) do - %Events.Paused{aggregate_uuid: aggregate_uuid} - end - - def stop(%ExampleAggregate{uuid: aggregate_uuid}) do - %Events.Stopped{aggregate_uuid: aggregate_uuid} - end - - def continue(%ExampleAggregate{}) do - [] - end - - def error(%ExampleAggregate{uuid: aggregate_uuid}) do - %Events.Errored{aggregate_uuid: aggregate_uuid} - end - - def raise(%ExampleAggregate{uuid: aggregate_uuid}) do - %Events.Raised{aggregate_uuid: aggregate_uuid} - end - - defp publish_interesting(_aggregate_uuid, 0, _index), do: [] - - defp publish_interesting(aggregate_uuid, interesting, index) do - [ - %Events.Interested{aggregate_uuid: aggregate_uuid, index: index} - ] ++ publish_interesting(aggregate_uuid, interesting - 1, index + 1) - end - - defp publish_uninteresting(_aggregate_uuid, 0, _index), do: [] - - defp publish_uninteresting(aggregate_uuid, interesting, index) do - [ - %Events.Uninterested{aggregate_uuid: aggregate_uuid, index: index} - ] ++ publish_uninteresting(aggregate_uuid, interesting - 1, index + 1) - end - - # State mutators - - def apply(%ExampleAggregate{} = state, %Events.Started{aggregate_uuid: aggregate_uuid}), - do: %ExampleAggregate{state | uuid: aggregate_uuid, state: :started} - - def apply(%ExampleAggregate{items: items} = state, %Events.Interested{index: index}), - do: %ExampleAggregate{state | items: items ++ [index]} - - def apply(%ExampleAggregate{} = state, %Events.Paused{}), - do: %ExampleAggregate{state | state: :paused} - - def apply(%ExampleAggregate{} = state, %Events.Errored{}), - do: %ExampleAggregate{state | state: :errored} - - def apply(%ExampleAggregate{} = state, %Events.Raised{}), - do: %ExampleAggregate{state | state: :exception} - - def apply(%ExampleAggregate{} = state, %Events.Uninterested{}), do: state - - def apply(%ExampleAggregate{} = state, %Events.Stopped{}), - do: %ExampleAggregate{state | state: :stopped} -end diff --git a/test/process_managers/support/example_app.ex b/test/process_managers/support/example_app.ex deleted file mode 100644 index 3e503e44..00000000 --- a/test/process_managers/support/example_app.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Commanded.ProcessManagers.ExampleApp do - @moduledoc false - - use Commanded.Application, otp_app: :commanded - - alias Commanded.ProcessManagers.ExampleRouter - - router(ExampleRouter) -end diff --git a/test/process_managers/support/example_command_handler.ex b/test/process_managers/support/example_command_handler.ex deleted file mode 100644 index 05cb0c73..00000000 --- a/test/process_managers/support/example_command_handler.ex +++ /dev/null @@ -1,40 +0,0 @@ -defmodule Commanded.ProcessManagers.ExampleCommandHandler do - @moduledoc false - @behaviour Commanded.Commands.Handler - - alias Commanded.ProcessManagers.ExampleAggregate - - alias Commanded.ProcessManagers.ExampleAggregate.Commands.{ - Continue, - Error, - Pause, - Publish, - Raise, - Start, - Stop - } - - def handle(%ExampleAggregate{} = aggregate, %Start{aggregate_uuid: aggregate_uuid}), - do: ExampleAggregate.start(aggregate, aggregate_uuid) - - def handle(%ExampleAggregate{} = aggregate, %Publish{} = command) do - %Publish{interesting: interesting, uninteresting: uninteresting} = command - - ExampleAggregate.publish(aggregate, interesting, uninteresting) - end - - def handle(%ExampleAggregate{} = aggregate, %Pause{}), - do: ExampleAggregate.pause(aggregate) - - def handle(%ExampleAggregate{} = aggregate, %Stop{}), - do: ExampleAggregate.stop(aggregate) - - def handle(%ExampleAggregate{} = aggregate, %Continue{}), - do: ExampleAggregate.continue(aggregate) - - def handle(%ExampleAggregate{} = aggregate, %Error{}), - do: ExampleAggregate.error(aggregate) - - def handle(%ExampleAggregate{} = aggregate, %Raise{}), - do: ExampleAggregate.raise(aggregate) -end diff --git a/test/process_managers/support/example_process_manager.ex b/test/process_managers/support/example_process_manager.ex deleted file mode 100644 index b470e344..00000000 --- a/test/process_managers/support/example_process_manager.ex +++ /dev/null @@ -1,54 +0,0 @@ -defmodule Commanded.ProcessManagers.ExampleProcessManager do - @moduledoc false - - alias Commanded.ProcessManagers.ExampleAggregate.Commands.Stop - - alias Commanded.ProcessManagers.ExampleAggregate.Events.{ - Errored, - Interested, - Paused, - Raised, - Started, - Stopped - } - - alias Commanded.ProcessManagers.ExampleApp - alias Commanded.ProcessManagers.ExampleProcessManager - - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "ExampleProcessManager" - - @derive Jason.Encoder - defstruct [:status, items: []] - - def interested?(%Started{aggregate_uuid: aggregate_uuid}), do: {:start, aggregate_uuid} - def interested?(%Interested{aggregate_uuid: aggregate_uuid}), do: {:continue, aggregate_uuid} - def interested?(%Paused{aggregate_uuid: aggregate_uuid}), do: {:continue, aggregate_uuid} - def interested?(%Errored{aggregate_uuid: aggregate_uuid}), do: {:continue, aggregate_uuid} - def interested?(%Raised{aggregate_uuid: aggregate_uuid}), do: {:continue, aggregate_uuid} - def interested?(%Stopped{aggregate_uuid: aggregate_uuid}), do: {:stop, aggregate_uuid} - - def handle(%ExampleProcessManager{}, %Interested{index: 10, aggregate_uuid: aggregate_uuid}) do - %Stop{aggregate_uuid: aggregate_uuid} - end - - # Simulate a "stuck" process - def handle(%ExampleProcessManager{}, %Paused{}) do - :timer.sleep(:infinity) - end - - def handle(%ExampleProcessManager{}, %Errored{}), do: {:error, :failed} - - def handle(%ExampleProcessManager{}, %Raised{}), do: raise("failed") - - # State mutators - - def apply(%ExampleProcessManager{} = process_manager, %Started{}) do - %ExampleProcessManager{process_manager | status: :started} - end - - def apply(%ExampleProcessManager{items: items} = process_manager, %Interested{index: index}) do - %ExampleProcessManager{process_manager | items: items ++ [index]} - end -end diff --git a/test/process_managers/support/example_router.ex b/test/process_managers/support/example_router.ex deleted file mode 100644 index 286a7c9f..00000000 --- a/test/process_managers/support/example_router.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Commanded.ProcessManagers.ExampleRouter do - @moduledoc false - - use Commanded.Commands.Router - - alias Commanded.ProcessManagers.{ExampleAggregate, ExampleCommandHandler} - - alias Commanded.ProcessManagers.ExampleAggregate.Commands.{ - Continue, - Error, - Pause, - Publish, - Raise, - Start, - Stop - } - - dispatch [Error, Pause, Publish, Raise, Start, Stop, Continue], - to: ExampleCommandHandler, - aggregate: ExampleAggregate, - identity: :aggregate_uuid -end diff --git a/test/process_managers/support/identity/identity_process_manager.ex b/test/process_managers/support/identity/identity_process_manager.ex deleted file mode 100644 index 49549704..00000000 --- a/test/process_managers/support/identity/identity_process_manager.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Commanded.ProcessManagers.IdentityProcessManager do - @moduledoc false - - alias Commanded.DefaultApp - alias Commanded.ProcessManagers.IdentityProcessManager - alias Commanded.ProcessManagers.ProcessManager - - use Commanded.ProcessManagers.ProcessManager, - application: DefaultApp, - name: __MODULE__ - - @derive Jason.Encoder - defstruct [:uuid] - - defmodule AnEvent do - defstruct [:uuids, :reply_to] - end - - def interested?(%AnEvent{uuids: uuids}), do: {:start, uuids} - - def handle(%IdentityProcessManager{}, %AnEvent{} = event) do - %AnEvent{reply_to: reply_to} = event - - uuid = ProcessManager.identity() - - send(reply_to, {:identity, uuid, self()}) - - [] - end - - def apply(%IdentityProcessManager{} = state, _event), do: state -end diff --git a/test/process_managers/support/init/runtime_config_process_manager.ex b/test/process_managers/support/init/runtime_config_process_manager.ex deleted file mode 100644 index 601b360f..00000000 --- a/test/process_managers/support/init/runtime_config_process_manager.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Commanded.ProcessManagers.RuntimeConfigProcessManager do - @moduledoc false - - use Commanded.ProcessManagers.ProcessManager - - alias Commanded.DefaultApp - - @derive Jason.Encoder - defstruct [:process_uuid] - - def init(config) do - {reply_to, config} = Keyword.pop!(config, :reply_to) - {tenant, config} = Keyword.pop!(config, :tenant) - - config = - config - |> Keyword.put(:application, Module.concat([DefaultApp, tenant])) - |> Keyword.put(:name, Module.concat([__MODULE__, tenant])) - - send(reply_to, {:init, tenant}) - - {:ok, config} - end -end diff --git a/test/process_managers/support/multi/todo.ex b/test/process_managers/support/multi/todo.ex deleted file mode 100644 index 3922c028..00000000 --- a/test/process_managers/support/multi/todo.ex +++ /dev/null @@ -1,48 +0,0 @@ -defmodule Commanded.ProcessManagers.Todo do - @moduledoc false - - @derive Jason.Encoder - defstruct status: nil - - defmodule Commands do - defmodule CreateTodo do - @derive Jason.Encoder - defstruct([:todo_uuid]) - end - - defmodule MarkDone do - @derive Jason.Encoder - defstruct([:todo_uuid]) - end - end - - defmodule Events do - defmodule TodoCreated do - @derive Jason.Encoder - defstruct([:todo_uuid]) - end - - defmodule TodoDone do - @derive Jason.Encoder - defstruct([:todo_uuid]) - end - end - - alias Commanded.ProcessManagers.Todo - alias Commanded.ProcessManagers.Todo.Commands.{CreateTodo, MarkDone} - alias Commanded.ProcessManagers.Todo.Events.{TodoCreated, TodoDone} - - def execute(%Todo{}, %CreateTodo{todo_uuid: todo_uuid}) do - %TodoCreated{todo_uuid: todo_uuid} - end - - def execute(%Todo{}, %MarkDone{todo_uuid: todo_uuid}) do - %TodoDone{todo_uuid: todo_uuid} - end - - # State mutators - - def apply(%Todo{} = state, %TodoCreated{}), do: %Todo{state | status: :pending} - - def apply(%Todo{} = state, %TodoDone{}), do: %Todo{state | status: :done} -end diff --git a/test/process_managers/support/multi/todo_app.ex b/test/process_managers/support/multi/todo_app.ex deleted file mode 100644 index a4eca49f..00000000 --- a/test/process_managers/support/multi/todo_app.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Commanded.ProcessManagers.TodoApp do - use Commanded.Application, otp_app: :commanded - - alias Commanded.ProcessManagers.TodoRouter - - router(TodoRouter) -end diff --git a/test/process_managers/support/multi/todo_list.ex b/test/process_managers/support/multi/todo_list.ex deleted file mode 100644 index 99e47485..00000000 --- a/test/process_managers/support/multi/todo_list.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule Commanded.ProcessManagers.TodoList do - @moduledoc false - @derive Jason.Encoder - defstruct todo_uuids: [] - - defmodule Commands do - defmodule CreateList do - @derive Jason.Encoder - defstruct([:list_uuid, :todo_uuids]) - end - - defmodule MarkAllDone do - @derive Jason.Encoder - defstruct([:list_uuid]) - end - end - - defmodule Events do - defmodule TodoListCreated do - @derive Jason.Encoder - defstruct([:list_uuid, :todo_uuids]) - end - - defmodule ListAllDone do - @derive Jason.Encoder - defstruct([:list_uuid, :todo_uuids]) - end - end - - alias Commanded.ProcessManagers.TodoList - alias Commanded.ProcessManagers.TodoList.Commands.{CreateList, MarkAllDone} - alias Commanded.ProcessManagers.TodoList.Events.{ListAllDone, TodoListCreated} - - def execute(%TodoList{}, %CreateList{list_uuid: list_uuid, todo_uuids: todo_uuids}) do - %TodoListCreated{list_uuid: list_uuid, todo_uuids: todo_uuids} - end - - def execute(%TodoList{todo_uuids: todo_uuids}, %MarkAllDone{list_uuid: list_uuid}) do - %ListAllDone{list_uuid: list_uuid, todo_uuids: todo_uuids} - end - - def apply(%TodoList{} = state, %TodoListCreated{todo_uuids: todo_uuids}), - do: %TodoList{state | todo_uuids: todo_uuids} - - def apply(%TodoList{} = state, _event), do: state -end diff --git a/test/process_managers/support/multi/todo_process_manager.ex b/test/process_managers/support/multi/todo_process_manager.ex deleted file mode 100644 index 738e226e..00000000 --- a/test/process_managers/support/multi/todo_process_manager.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Commanded.ProcessManagers.TodoProcessManager do - @moduledoc false - - alias Commanded.ProcessManagers.{TodoApp, TodoProcessManager} - - use Commanded.ProcessManagers.ProcessManager, - application: TodoApp, - name: __MODULE__ - - @derive Jason.Encoder - defstruct [:todo_uuid] - - alias Commanded.ProcessManagers.Todo.Commands.MarkDone - alias Commanded.ProcessManagers.Todo.Events.TodoCreated - alias Commanded.ProcessManagers.TodoList.Events.ListAllDone - - def interested?(%TodoCreated{todo_uuid: todo_uuid}), do: {:start, todo_uuid} - def interested?(%ListAllDone{todo_uuids: todo_uuids}), do: {:continue, todo_uuids} - - def handle(%TodoProcessManager{}, %TodoCreated{}), do: [] - - def handle(%TodoProcessManager{todo_uuid: todo_uuid}, %ListAllDone{}) do - %MarkDone{todo_uuid: todo_uuid} - end - - def apply(%TodoProcessManager{} = state, %TodoCreated{todo_uuid: todo_uuid}) do - %TodoProcessManager{state | todo_uuid: todo_uuid} - end - - def apply(%TodoProcessManager{} = state, _event), do: state -end diff --git a/test/process_managers/support/multi/todo_router.ex b/test/process_managers/support/multi/todo_router.ex deleted file mode 100644 index f668dabf..00000000 --- a/test/process_managers/support/multi/todo_router.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Commanded.ProcessManagers.TodoRouter do - @moduledoc false - - use Commanded.Commands.Router - - alias Commanded.ProcessManagers.{Todo, TodoList} - alias Commanded.ProcessManagers.Todo.Commands.{CreateTodo, MarkDone} - alias Commanded.ProcessManagers.TodoList.Commands.{CreateList, MarkAllDone} - - identify Todo, by: :todo_uuid - identify TodoList, by: :list_uuid - - dispatch [CreateTodo, MarkDone], to: Todo - dispatch [CreateList, MarkAllDone], to: TodoList -end diff --git a/test/process_managers/support/null_handler.ex b/test/process_managers/support/null_handler.ex deleted file mode 100644 index a67ff542..00000000 --- a/test/process_managers/support/null_handler.ex +++ /dev/null @@ -1,6 +0,0 @@ -defmodule Commanded.ProcessManagers.NullHandler do - @moduledoc false - @behaviour Commanded.Commands.Handler - - def handle(_aggregate, _command), do: [] -end diff --git a/test/process_managers/support/null_router.ex b/test/process_managers/support/null_router.ex deleted file mode 100644 index 0f59289a..00000000 --- a/test/process_managers/support/null_router.ex +++ /dev/null @@ -1,10 +0,0 @@ -defmodule Commanded.ProcessManagers.NullRouter do - @moduledoc false - use Commanded.Commands.Router - - alias Commanded.ExampleDomain.BankAccount - alias Commanded.ExampleDomain.BankAccount.Commands.WithdrawMoney - alias Commanded.ProcessManagers.NullHandler - - dispatch WithdrawMoney, to: NullHandler, aggregate: BankAccount, identity: :account_number -end diff --git a/test/process_managers/support/resume/resume_aggregate.ex b/test/process_managers/support/resume/resume_aggregate.ex deleted file mode 100644 index 288371b0..00000000 --- a/test/process_managers/support/resume/resume_aggregate.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule Commanded.ProcessManagers.ResumeAggregate do - @moduledoc false - - defstruct [:status] - - defmodule Commands do - defmodule(StartProcess, do: defstruct([:process_uuid, :status])) - defmodule(ResumeProcess, do: defstruct([:process_uuid, :status])) - end - - defmodule Events do - defmodule ProcessStarted do - @derive Jason.Encoder - defstruct([:process_uuid, :status]) - end - - defmodule ProcessResumed do - @derive Jason.Encoder - defstruct([:process_uuid, :status]) - end - end - - alias Commanded.ProcessManagers.ResumeAggregate - alias Commanded.ProcessManagers.ResumeAggregate.Commands.{ResumeProcess, StartProcess} - alias Commanded.ProcessManagers.ResumeAggregate.Events.{ProcessResumed, ProcessStarted} - - def start_process(%ResumeAggregate{}, %StartProcess{process_uuid: process_uuid, status: status}) do - %ProcessStarted{process_uuid: process_uuid, status: status} - end - - def resume_process(%ResumeAggregate{}, %ResumeProcess{} = command) do - %ResumeProcess{process_uuid: process_uuid, status: status} = command - - %ProcessResumed{process_uuid: process_uuid, status: status} - end - - # State mutators - - def apply(%ResumeAggregate{} = state, %ProcessStarted{status: status}), - do: %ResumeAggregate{state | status: status} - - def apply(%ResumeAggregate{} = state, %ProcessResumed{status: status}), - do: %ResumeAggregate{state | status: status} -end diff --git a/test/process_managers/support/resume/resume_app.ex b/test/process_managers/support/resume/resume_app.ex deleted file mode 100644 index f0c6078d..00000000 --- a/test/process_managers/support/resume/resume_app.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Commanded.ProcessManagers.ResumeApp do - @moduledoc false - - use Commanded.Application, otp_app: :commanded - - alias Commanded.ProcessManagers.ResumeRouter - - router(ResumeRouter) -end diff --git a/test/process_managers/support/resume/resume_command_handler.ex b/test/process_managers/support/resume/resume_command_handler.ex deleted file mode 100644 index 38713297..00000000 --- a/test/process_managers/support/resume/resume_command_handler.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Commanded.ProcessManagers.ResumeCommandHandler do - @moduledoc false - @behaviour Commanded.Commands.Handler - - alias Commanded.ProcessManagers.ResumeAggregate - alias Commanded.ProcessManagers.ResumeAggregate.Commands.{ResumeProcess, StartProcess} - - def handle(%ResumeAggregate{} = aggregate, %StartProcess{} = start_process) do - ResumeAggregate.start_process(aggregate, start_process) - end - - def handle(%ResumeAggregate{} = aggregate, %ResumeProcess{} = resume_process) do - ResumeAggregate.resume_process(aggregate, resume_process) - end -end diff --git a/test/process_managers/support/resume/resume_process_manager.ex b/test/process_managers/support/resume/resume_process_manager.ex deleted file mode 100644 index d95585f3..00000000 --- a/test/process_managers/support/resume/resume_process_manager.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Commanded.ProcessManagers.ResumeProcessManager do - @moduledoc false - use Commanded.ProcessManagers.ProcessManager, - application: Commanded.ProcessManagers.ResumeApp, - name: "resume-process-manager" - - @derive Jason.Encoder - defstruct status_history: [] - - alias Commanded.ProcessManagers.ResumeAggregate.Events.{ProcessResumed, ProcessStarted} - alias Commanded.ProcessManagers.ResumeProcessManager - - def interested?(%ProcessStarted{process_uuid: process_uuid}), do: {:start, process_uuid} - def interested?(%ProcessResumed{process_uuid: process_uuid}), do: {:continue, process_uuid} - - def handle(%ResumeProcessManager{}, %ProcessStarted{}), do: [] - def handle(%ResumeProcessManager{}, %ProcessResumed{}), do: [] - - # State mutators - - def apply(%ResumeProcessManager{} = process, %ProcessStarted{} = event) do - %ResumeProcessManager{status_history: status_history} = process - %ProcessStarted{status: status} = event - - %ResumeProcessManager{process | status_history: status_history ++ [status]} - end - - def apply(%ResumeProcessManager{} = process, %ProcessResumed{} = event) do - %ResumeProcessManager{status_history: status_history} = process - %ProcessResumed{status: status} = event - - %ResumeProcessManager{process | status_history: status_history ++ [status]} - end -end diff --git a/test/process_managers/support/resume/resume_router.ex b/test/process_managers/support/resume/resume_router.ex deleted file mode 100644 index 639faa08..00000000 --- a/test/process_managers/support/resume/resume_router.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Commanded.ProcessManagers.ResumeRouter do - @moduledoc false - - use Commanded.Commands.Router - - alias Commanded.ProcessManagers.{ResumeAggregate, ResumeCommandHandler} - alias Commanded.ProcessManagers.ResumeAggregate.Commands.{ResumeProcess, StartProcess} - - dispatch StartProcess, - to: ResumeCommandHandler, - aggregate: ResumeAggregate, - identity: :process_uuid - - dispatch ResumeProcess, - to: ResumeCommandHandler, - aggregate: ResumeAggregate, - identity: :process_uuid -end diff --git a/test/process_managers/support/routing/routing_process_manager.ex b/test/process_managers/support/routing/routing_process_manager.ex deleted file mode 100644 index 67c2ab05..00000000 --- a/test/process_managers/support/routing/routing_process_manager.ex +++ /dev/null @@ -1,103 +0,0 @@ -defmodule Commanded.ProcessManagers.RoutingProcessManager do - use Commanded.ProcessManagers.ProcessManager, - application: Commanded.MockedApp, - name: __MODULE__ - - defmodule Started do - @enforce_keys [:process_uuid] - defstruct [:process_uuid, :reply_to, strict?: false] - end - - defmodule StartedFromMetadata do - defstruct [:process_uuid, :reply_to, strict?: false] - end - - defmodule Continued do - @enforce_keys [:process_uuid] - defstruct [:process_uuid, :reply_to, strict?: false] - end - - defmodule Stopped do - @enforce_keys [:process_uuid] - defstruct [:process_uuid] - end - - defmodule Errored do - @enforce_keys [:process_uuid] - defstruct [:process_uuid, :reply_to, :on_error] - end - - alias Commanded.EventStore.EnrichedMetadata - alias Commanded.ProcessManagers.FailureContext - alias Commanded.ProcessManagers.RoutingProcessManager - - defstruct [:processes] - - def interested?(%StartedFromMetadata{}, %EnrichedMetadata{ - metadata: %{"process_uuid" => process_uuid} - }), - do: {:start, process_uuid} - - def interested?(%Started{process_uuid: process_uuid, strict?: true}), - do: {:start!, process_uuid} - - def interested?(%Started{process_uuid: process_uuid}), - do: {:start, process_uuid} - - def interested?(%Continued{process_uuid: process_uuid, strict?: true}), - do: {:continue!, process_uuid} - - def interested?(%Continued{process_uuid: process_uuid}), do: {:continue, process_uuid} - - def interested?(%Stopped{process_uuid: process_uuid}), do: {:stop, process_uuid} - - def interested?(%Errored{}), do: raise("error") - - def handle(%RoutingProcessManager{}, %StartedFromMetadata{} = event) do - %StartedFromMetadata{reply_to: reply_to} = event - - send(reply_to, {:started, self()}) - - [] - end - - def handle(%RoutingProcessManager{}, %Started{} = event) do - %Started{reply_to: reply_to} = event - - send(reply_to, {:started, self()}) - - [] - end - - def handle(%RoutingProcessManager{}, %Continued{} = event) do - %Continued{reply_to: reply_to} = event - - send(reply_to, {:continued, self()}) - - [] - end - - def error({:error, error}, %Started{} = event, %FailureContext{}) do - %Started{reply_to: reply_to} = event - - send(reply_to, {:error, error}) - - {:stop, error} - end - - def error({:error, error}, %Continued{} = event, %FailureContext{}) do - %Continued{reply_to: reply_to} = event - - send(reply_to, {:error, error}) - - {:stop, error} - end - - def error({:error, error}, %Errored{} = event, %FailureContext{}) do - %Errored{reply_to: reply_to, on_error: on_error} = event - - send(reply_to, {:error, error}) - - on_error - end -end diff --git a/test/process_managers/support/timeout_process_manager.ex b/test/process_managers/support/timeout_process_manager.ex deleted file mode 100644 index de77a2e7..00000000 --- a/test/process_managers/support/timeout_process_manager.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Commanded.ProcessManagers.TimeoutProcessManager do - @moduledoc false - - alias Commanded.ProcessManagers.ExampleAggregate.Events.Started - alias Commanded.ProcessManagers.ExampleAggregate.Events.Stopped - alias Commanded.ProcessManagers.ExampleApp - alias Commanded.ProcessManagers.TimeoutProcessManager - - use Commanded.ProcessManagers.ProcessManager, - application: ExampleApp, - name: "TimeoutProcessManager", - idle_timeout: :timer.minutes(1) - - @derive Jason.Encoder - defstruct [:status] - - def interested?(%Started{aggregate_uuid: aggregate_uuid}), do: {:start, aggregate_uuid} - def interested?(%Stopped{aggregate_uuid: aggregate_uuid}), do: {:stop, aggregate_uuid} - - ## State mutators - - def apply(%TimeoutProcessManager{} = pm, %Started{}) do - %TimeoutProcessManager{pm | status: :started} - end -end diff --git a/test/subscriptions/support/distributed_subscribers.ex b/test/subscriptions/support/distributed_subscribers.ex index e50c7c81..57979318 100644 --- a/test/subscriptions/support/distributed_subscribers.ex +++ b/test/subscriptions/support/distributed_subscribers.ex @@ -5,7 +5,6 @@ defmodule Commanded.Subscriptions.DistributedSubscribers do alias Commanded.Subscriptions @event_handlers [EventHandler1, EventHandler2, EventHandler3] - @process_managers [ProcessManager1, ProcessManager2, ProcessManager3] for event_handler <- @event_handlers do defmodule event_handler do @@ -16,16 +15,7 @@ defmodule Commanded.Subscriptions.DistributedSubscribers do end end - for process_manager <- @process_managers do - defmodule process_manager do - use Commanded.ProcessManagers.ProcessManager, - application: DistributedApp, - name: __MODULE__, - consistency: :strong - end - end - - def all, do: @event_handlers ++ @process_managers + def all, do: @event_handlers def start_subscribers(nodes) do reply_to = self()