Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/commanded/commands/dispatcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ defmodule Commanded.Commands.Dispatcher do
defp to_execution_context(%Pipeline{} = pipeline, %Payload{} = payload) do
%Pipeline{
command: command,
command_uuid: command_uuid,
causation_id: causation_id,
metadata: metadata
} = pipeline
Expand All @@ -175,7 +174,8 @@ defmodule Commanded.Commands.Dispatcher do

%ExecutionContext{
command: command,
causation_id: causation_id || command_uuid,
# `causation_id` is resolved at the router boundary (#624)
causation_id: causation_id,
correlation_id: correlation_id,
metadata: metadata,
handler: handler_module,
Expand Down
21 changes: 19 additions & 2 deletions lib/commanded/commands/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,12 @@
Options:

- `causation_id` - an optional UUID used to identify the cause of the
command being dispatched.
command being dispatched. When the option is omitted, the command's
own `command_uuid` is used (preserving prior behaviour). Pass
`causation_id: nil` explicitly to mark the resulting events as chain
roots — they will be persisted with `causation_id` set to `NULL`,
letting `WHERE causation_id IS NULL` queries identify roots without
a self-join.

- `command_uuid` - an optional UUID used to identify the command being
dispatched. When omitted, one will be generated automatically.
Expand Down Expand Up @@ -524,7 +529,7 @@
) :: dispatch_resp()

defmacro __before_compile__(_env) do
quote generated: true do

Check warning on line 532 in lib/commanded/commands/router.ex

View workflow job for this annotation

GitHub Actions / Quality Assurance (1.19.x, 27)

Avoid long quote blocks.
@doc false
def __registered_commands__ do
Enum.map(@registered_commands, fn {command_module, _command_opts} -> command_module end)
Expand Down Expand Up @@ -568,10 +573,22 @@
opts = Keyword.merge(@command_opts, opts)

application = Keyword.fetch!(opts, :application)
causation_id = Keyword.get(opts, :causation_id)
command_uuid = Router.fetch_command_uuid(opts)
consistency = Keyword.fetch!(opts, :consistency)
correlation_id = Keyword.get_lazy(opts, :correlation_id, &UUID.uuid7/0)

# Resolve `:causation_id` at the router boundary so the fallback
# decision happens once and downstream middleware/handlers see the
# final value. Three cases:
# - opt absent -> fall back to command_uuid (legacy)
# - `causation_id: <uuid>` -> use the uuid
# - `causation_id: nil` -> use nil (chain root, #624)
causation_id =
case Keyword.fetch(opts, :causation_id) do
{:ok, value} -> value
:error -> command_uuid
end

metadata = Keyword.fetch!(opts, :metadata) |> validate_metadata()

retry_attempts = Keyword.get(opts, :retry_attempts)
Expand Down
10 changes: 10 additions & 0 deletions test/commands/correlation_causation_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ defmodule Commanded.Commands.CorrelationCasuationTest do

assert event.causation_id == causation_id
end

test "should be `nil` on created event when `causation_id: nil` is passed explicitly" do
command = %OpenAccount{account_number: "ACC123", initial_balance: 500}

:ok = BankRouter.dispatch(command, application: BankApp, causation_id: nil)

[event] = EventStore.stream_forward(BankApp, "ACC123") |> Enum.to_list()

assert is_nil(event.causation_id)
end
end

describe "`correlation_id`" do
Expand Down
Loading