Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include application and handler_name as additional event handler and upcaster metadata #396

Merged
merged 2 commits into from Aug 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@
### Enhancements

- Add `init/1` callback function to event handlers and process managers ([#393](https://github.com/commanded/commanded/pull/393)).
- Include `application` and `handler_name` as additional event handler metadata ([#396](https://github.com/commanded/commanded/pull/396)).

## v1.1.1

Expand Down
75 changes: 6 additions & 69 deletions guides/Events.md
Expand Up @@ -85,76 +85,11 @@ end

This will ensure the handler only receives events appended to that stream.

### `init/0` callback
### Event handler callbacks

You can define an `init/0` function in your handler to be called once it has started and successfully subscribed to the event store.

This callback function must return `:ok`, any other return value will terminate the event handler with an error.

```elixir
defmodule ExampleHandler do
use Commanded.Event.Handler,
application: ExampleApp,
name: "ExampleHandler"

def init do
# optional initialisation
:ok
end

def handle(%AnEvent{..}, _metadata) do
# ... process the event
:ok
end
end
```

### `error/3` callback

You can define an `error/3` callback function to handle any exceptions or errors returned from your event handler's `handle/2` functions. The `error/3` function is passed the actual error (e.g. `{:error, :failure}`), the failed event, and a failure context.

Use pattern matching on the error and/or failed event to explicitly handle certain errors or events. You can choose to retry, skip, or stop the event handler after an error.

The default behaviour if you don't provide an `error/3` callback is to stop the event handler using the exact error reason returned from the `handle/2` function. You should supervise event handlers to ensure they are correctly restarted on error.

#### Example error handling

```elixir
defmodule ExampleHandler do
use Commanded.Event.Handler,
application: ExampleApp,
name: __MODULE__

require Logger

alias Commanded.Event.FailureContext

def handle(%AnEvent{}, _metadata) do
# Simulate event handling failure
{:error, :failed}
end

def error({:error, :failed}, %AnEvent{} = event, %FailureContext{context: context}) do
context = record_failure(context)

case Map.get(context, :failures) do
too_many when too_many >= 3 ->
# Skip bad event after third failure
Logger.warn(fn -> "Skipping bad event, too many failures: " <> inspect(event) end)

:skip

_ ->
# Retry event, failure count is included in context map
{:retry, context}
end
end

defp record_failure(context) do
Map.update(context, :failures, 1, fn failures -> failures + 1 end)
end
end
```
- `c:Commanded.Event.Handler.init/0` - (optional) initialisation callback function called when the handler starts.
- `c:Commanded.Event.Handler.init/1` - (optional) used to configure the handler before it starts.
- `c:Commanded.Event.Handler.error/3` - (optional) called when an event handle/2 callback returns an error.

### Metadata

Expand All @@ -166,6 +101,8 @@ The `handle/2` function in your handler receives the domain event and a map of m

In addition to the metadata key/values you provide, the following system values will be included in the metadata passed to an event handler:

- `application` - the `Commanded.Application` associated with the event handler.
- `handler_name` - the name of the event handler.
- `event_id` - a globally unique UUID to identify the event.
- `event_number` - a globally unique, monotonically incrementing and gapless integer used to order the event amongst all events.
- `stream_id` - the stream identity for the event.
Expand Down
4 changes: 2 additions & 2 deletions lib/commanded/aggregates/aggregate.ex
Expand Up @@ -276,14 +276,14 @@ defmodule Commanded.Aggregates.Aggregate do
@doc false
@impl GenServer
def handle_info({:events, events}, %Aggregate{} = state) do
%Aggregate{lifespan_timeout: lifespan_timeout} = state
%Aggregate{application: application, lifespan_timeout: lifespan_timeout} = state

Logger.debug(fn -> describe(state) <> " received events: #{inspect(events)}" end)

try do
state =
events
|> Upcast.upcast_event_stream()
|> Upcast.upcast_event_stream(additional_metadata: %{application: application})
|> Enum.reduce(state, &handle_event/2)

state = Enum.reduce(events, state, &handle_event/2)
Expand Down
6 changes: 6 additions & 0 deletions lib/commanded/event/failure_context.ex
Expand Up @@ -4,6 +4,8 @@ defmodule Commanded.Event.FailureContext do

The available fields are:

- `application` - the associated `Commanded.Application`.
- `handler_name` - the name of the event handler.
- `context` - a map that is passed between each failure. Use it to store any
transient state between failures. As an example it could be used to count
error failures and stop or skip the problematic event after too many.
Expand All @@ -13,12 +15,16 @@ defmodule Commanded.Event.FailureContext do
"""

@type t :: %__MODULE__{
application: Commanded.Application.t(),
handler_name: String.t(),
context: map(),
metadata: map(),
stacktrace: Exception.stacktrace() | nil
}

defstruct [
:application,
:handler_name,
:context,
:metadata,
:stacktrace
Expand Down