Skip to content

refactor(dispatcher): align aggregate failure boundary - #98

Merged
yordis merged 2 commits into
mainfrom
yordis/refactor-dispatcher-exit-boundary
Apr 29, 2026
Merged

yordis merged 2 commits into
mainfrom
yordis/refactor-dispatcher-exit-boundary

Conversation

@yordis

@yordis yordis commented Apr 29, 2026

Copy link
Copy Markdown
Member
  • keep dispatch failure guarantees attached to the aggregate execution boundary instead of a separate wrapper process
  • make timeout reporting deterministic so retries and failure handling are easier to reason about
  • remove infrastructure that no longer contributes to caller safety once exit normalization lives in one place

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@cursor

cursor Bot commented Apr 29, 2026

Copy link
Copy Markdown

PR Summary

Cursor Bugbot is generating a summary for commit f7b3adf. Configure here.

@coderabbitai

coderabbitai Bot commented Apr 29, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@yordis has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 37 minutes and 12 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de10b90e-3f26-4ad0-988d-a21802aa6ce3

📥 Commits

Reviewing files that changed from the base of the PR and between f7b3adf and 5c4979e.

📒 Files selected for processing (4)
  • guides/explanations/fork-differences.md
  • lib/commanded/aggregates/aggregate.ex
  • test/aggregates/execute_command_test.exs
  • test/middleware/middleware_test.exs

Walkthrough

The pull request refactors command execution error handling by removing asynchronous Task.Supervisor-based dispatch and consolidating error normalization into the Aggregate.execute/5 function. This shifts responsibility for translating timeouts, remote node failures, and aggregate stops from the dispatcher to the aggregate executor, simplifying the dispatcher implementation.

Changes

Cohort / File(s) Summary
Error Handling Refactoring
lib/commanded/aggregates/aggregate.ex, lib/commanded/commands/dispatcher.ex
Moved timeout, remote node-down, and failure error normalization from dispatcher (via Task.yield result mapping) into Aggregate.execute/5, which now returns normalized error tuples. Dispatcher simplified to direct function calls.
Infrastructure Cleanup
lib/commanded/application/supervisor.ex
Removed Task.Supervisor child spec and naming convention; supervisor now omits async dispatch infrastructure.
Documentation Update
guides/explanations/commands.md
Specified single observable timeout error outcome: {:error, :aggregate_execution_timeout}, removing mention of alternate :aggregate_execution_failed for timeouts.
Test Coverage
test/aggregates/execute_command_test.exs
Added three new integration tests: normal aggregate stop, command timeout, and abnormal termination; includes supporting test modules and command structs.
Test Assertion Tightening
test/application/telemetry_test.exs, test/commands/command_timeout_test.exs, test/middleware/middleware_test.exs, test/application/application_supervisor_test.exs
Updated timeout-related assertions to expect deterministic {:error, :aggregate_execution_timeout} instead of accepting either :aggregate_execution_timeout or :aggregate_execution_failed; removed TaskDispatcher from hibernation checks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 No more async tasks awaiting fate,
Errors now return in simpler state,
Direct aggregate calls, no Task.Supervisor wait,
Timeouts caught early, oh how great!
One true path for aggregate_execution_timeout 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main refactoring objective: moving failure handling from the dispatcher wrapper to the aggregate execution boundary.
Description check ✅ Passed The description is directly related to the changeset, providing three specific rationales for the refactoring that align with the code changes across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yordis/refactor-dispatcher-exit-boundary

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 37 minutes and 12 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
lib/commanded/aggregates/aggregate.ex (2)

303-304: Keep the original abnormal exit reason observable.

Normalizing every remaining exit to :aggregate_execution_failed is fine for the public API, but it also makes cases like Process.exit(self(), :boom) opaque in production. Please log or emit telemetry with the raw reason before returning the normalized tuple.

Suggested change
-      :exit, _reason ->
+      :exit, reason ->
+        Logger.error("aggregate execution failed: " <> inspect(reason))
         {:error, :aggregate_execution_failed}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/commanded/aggregates/aggregate.ex` around lines 303 - 304, The pattern
match that collapses every abnormal exit (:exit, _reason -> {:error,
:aggregate_execution_failed}) should preserve visibility of the original reason:
before returning the normalized {:error, :aggregate_execution_failed} tuple, log
or emit telemetry with the raw reason. Update the clause that matches :exit (the
":exit, _reason ->" branch in aggregate.ex) to call Logger.error/1 or
:telemetry.execute/3 (with event name like [:commanded, :aggregate, :exit] and
include the reason in the metadata) and then return the same normalized tuple so
the public API is unchanged.

295-299: Consider adding a regression test for the :remote_node_down exit case.

The :nodedown clause correctly matches GenServer.call/3 exits when the target node becomes unavailable. While this pattern is stable across OTP 28 and all addressing methods (PID, {name, Node}, {:via, module, term}), a dedicated test would prevent accidental changes to this error path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/commanded/aggregates/aggregate.ex` around lines 295 - 299, Add a
regression test that exercises the :nodedown exit clause and asserts the module
returns {:error, :remote_node_down} when a GenServer.call to the aggregate
process fails due to a remote node going down; specifically, write a test that
invokes the code path that sends GenServer.call(..., {:execute_command,
context}, timeout) (the same call pattern matched in aggregate.ex) and
simulates/forces a {:nodedown, _} exit from the remote node so the clause
matching {{:nodedown, _node_name}, {GenServer, :call, [^name, {:execute_command,
^context}, ^timeout]}} is hit and the function returns {:error,
:remote_node_down}. Ensure the test isolates the behavior (mock or spawn a
remote node/process as needed) and asserts the exact returned value to prevent
regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/commanded/aggregates/aggregate.ex`:
- Around line 303-304: The pattern match that collapses every abnormal exit
(:exit, _reason -> {:error, :aggregate_execution_failed}) should preserve
visibility of the original reason: before returning the normalized {:error,
:aggregate_execution_failed} tuple, log or emit telemetry with the raw reason.
Update the clause that matches :exit (the ":exit, _reason ->" branch in
aggregate.ex) to call Logger.error/1 or :telemetry.execute/3 (with event name
like [:commanded, :aggregate, :exit] and include the reason in the metadata) and
then return the same normalized tuple so the public API is unchanged.
- Around line 295-299: Add a regression test that exercises the :nodedown exit
clause and asserts the module returns {:error, :remote_node_down} when a
GenServer.call to the aggregate process fails due to a remote node going down;
specifically, write a test that invokes the code path that sends
GenServer.call(..., {:execute_command, context}, timeout) (the same call pattern
matched in aggregate.ex) and simulates/forces a {:nodedown, _} exit from the
remote node so the clause matching {{:nodedown, _node_name}, {GenServer, :call,
[^name, {:execute_command, ^context}, ^timeout]}} is hit and the function
returns {:error, :remote_node_down}. Ensure the test isolates the behavior (mock
or spawn a remote node/process as needed) and asserts the exact returned value
to prevent regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b1102fa-f37d-4a1c-81c7-918f2f7d36c9

📥 Commits

Reviewing files that changed from the base of the PR and between 7a07792 and f7b3adf.

📒 Files selected for processing (9)
  • guides/explanations/commands.md
  • lib/commanded/aggregates/aggregate.ex
  • lib/commanded/application/supervisor.ex
  • lib/commanded/commands/dispatcher.ex
  • test/aggregates/execute_command_test.exs
  • test/application/application_supervisor_test.exs
  • test/application/telemetry_test.exs
  • test/commands/command_timeout_test.exs
  • test/middleware/middleware_test.exs
💤 Files with no reviewable changes (2)
  • test/application/application_supervisor_test.exs
  • lib/commanded/application/supervisor.ex

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis
yordis merged commit 353e939 into main Apr 29, 2026
3 checks passed
@yordis
yordis deleted the yordis/refactor-dispatcher-exit-boundary branch April 29, 2026 03:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant