Skip to content

feat(reactor): hand the committed sequence to reactors through react_committed - #64

Open
JuaniRios wants to merge 1 commit into
masterfrom
reactor-event-sequence
Open

JuaniRios wants to merge 1 commit into
masterfrom
reactor-event-sequence

Conversation

@JuaniRios

@JuaniRios JuaniRios commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Part of RAI-3105.

Reactors can now read the committed event's sequence, through a new default method Reactor::react_committed. Nothing changes for existing reactors.

Why

st0x.liquidity wants structured log lines (liq_trade, liq_transfer, liq_event) with a stable event_id, so the log pipeline can drop duplicates. The natural id is <aggregate type>:<aggregate id>:<sequence>.

ReactorBridge::dispatch already gets each committed EventEnvelope with its sequence, but it only passes (id, event) to the reactor. So a reactor can't build that id today. Writing the lines from evolve() isn't an option either, cause it also runs on every replay.

API

#[async_trait]
pub trait Reactor: Dependent {
    type Error: std::error::Error + Send + Sync;

    async fn react(&self, event: <Self::Dependencies as EntityList>::Event) -> Result<(), Self::Error>;

    // new, with a default
    async fn react_committed(
        &self,
        event: <Self::Dependencies as EntityList>::Event,
        committed: Committed,
    ) -> Result<(), Self::Error> {
        self.react(event).await
    }
}

#[non_exhaustive]
pub struct Committed {
    pub sequence: usize,
}
  • ReactorBridge::dispatch now calls react_committed with each envelope's sequence. The default forwards to react, so a reactor that doesn't override it behaves exactly as before.
  • Committed is #[non_exhaustive] (build it with Committed::new), so we can add more commit facts later without a breaking change.
  • Arc<R> and RetryOnBusy<R> forward react_committed. RetryOnBusy retries it with the same committed. The IdempotentReactor contract now covers a react_committed override too.
  • ReactorHarness::receive_committed(id, event, sequence) to test an override. receive still calls react.
  • react_committed only runs on commit dispatch. Loading an entity, rebuilding snapshots, and projection catch-up or rebuild never reach a reactor, same as before. send_command() still bypasses reactors.

I picked a default method over a new trait or an adapter type: it's the smallest change, it needs no new wiring in StoreBuilder, and a reactor opts in by overriding one method.

Compatibility

  • Additive only. Existing impl Reactor blocks compile unchanged. I checked st0x.liquidity against this branch with no code changes: cargo check --workspace --all-targets --all-features is clean.
  • One thing to keep in mind: a hand-written wrapper that implements Reactor by forwarding react to an inner reactor must also forward react_committed. If not, the default calls the inner react and the inner override never runs. Nothing breaks for wrappers of reactors that don't override. It's in the rustdoc and docs/cqrs.md.

Tests

  • dispatch_hands_each_envelope_sequence_to_react_committed: a multi-event commit through the bridge gives each event its own sequence.
  • store_hands_each_commit_to_react_committed_with_its_stored_sequence: through a real StoreBuilder and SQLite, the reactor gets the same sequences as the events table, per aggregate.
  • restart_replay_and_hydration_do_not_reach_react_committed: 11 events (past the snapshot size), snapshot deleted, then a new store over the same DB. Building it rebuilds the snapshot, then rebuild_all, catch_up, and load run. The reactor gets nothing. The next command arrives with sequence 12.
  • default_react_committed_forwards_to_react, arc_forwards_react_committed_to_the_override, retry_on_busy_retries_react_committed_with_the_same_sequence, retry_on_busy_react_still_calls_inner_react, and reactor_harness_receive_committed_reaches_the_override.
  • CI-equivalent checks pass locally: check, nextest (198 tests), clippy, fmt, doc, examples, and pre-commit.
  • Multi-model review (sol, Cursor Grok, composer, flash, opus): 2 passes. Pass 1 found 3 lows (the restart test did not reach a snapshot rebuild, the IdempotentReactor contract did not mention react_committed, and a misplaced heading in docs/domain.md). All fixed. Pass 2 was clean on every lane.

Release

This needs a release before st0x.liquidity can drop its temporary git-rev pin (ST0x-Technology/st0x.liquidity#1696). I did not tag one.

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@JuaniRios
JuaniRios force-pushed the reactor-event-sequence branch from bdfb7a2 to 915875f Compare October 9, 2026 01:43
@JuaniRios
JuaniRios requested a review from agryaznov October 9, 2026 02:03
@JuaniRios JuaniRios self-assigned this Oct 9, 2026
@JuaniRios

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@linear-code

linear-code Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

RAI-2998

RAI-3063

RAI-3105

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Team
  • Run ID: 69a55112-9261-47c4-80cb-8e456ce13e52

📥 Commits

Reviewing files that changed from the base of the PR and between c04c0fa and 915875f.


📒 Files selected for processing (7)
  • SPEC.md
  • crates/event-sorcery/src/lib.rs
  • crates/event-sorcery/src/lifecycle.rs
  • crates/event-sorcery/src/reactor.rs
  • crates/event-sorcery/src/testing.rs
  • docs/cqrs.md
  • docs/domain.md

Included review availability: This review used your included allowance. 3 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 4 reviews per hour.



Walkthrough

The Reactor API adds Committed metadata and a react_committed method. ReactorBridge passes each persisted event’s aggregate-stream sequence through this method. Arc and RetryOnBusy forward committed dispatch, and ReactorHarness adds a committed-event test path. Tests cover sequence delivery, retries, and replay or hydration paths that do not dispatch reactors. The documentation describes the method and its idempotency contract.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 91587

No confirmed issue blocks merging after the reported checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 36 functions across 4 files. (3 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check Passed The title clearly and concisely describes the main change: passing the committed event sequence to reactors through the new react_committed method.
Description check Passed The description directly explains the API change, motivation, compatibility behavior, affected components, replay semantics, and test coverage.

Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 36 functions across 4 files. (3 skipped: 3 unsupported.)



  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR


  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@JuaniRios

Copy link
Copy Markdown
Collaborator Author

@rain-marvin review

@rain-marvin

rain-marvin Bot commented Oct 9, 2026

Copy link
Copy Markdown

🔎 Reviewing 915875f, started by @JuaniRios. The review will appear here when it's done.

@rain-marvin rain-marvin 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.

This PR adds a default method, Reactor::react_committed(event, committed), that gives a reactor the stored sequence of each committed event. With the sequence, a consumer such as st0x.liquidity can build a stable event id <aggregate type>:<aggregate id>:<sequence> for log deduplication. The default forwards to react, so existing reactors keep working. ReactorBridge::dispatch calls the new method. Arc<R> and RetryOnBusy<R> forward it, and ReactorHarness::receive_committed lets tests reach an override.

Overall read: correct and additive. The only path that sends committed events to reactors is the cqrs-es query dispatch after commit, which goes through ReactorBridge. Loading an entity, snapshot rebuild, projection catch-up and projection rebuild never reach a reactor, so the new method runs once per commit and never on replay. envelope.sequence is the value that goes into the events table, and numbering continues correctly after snapshots and compaction. Committed is Copy, so a RetryOnBusy retry gets the same sequence. The crate has no other forwarding wrapper that could drop an override. The tests cover the bridge, the full store commit, the restart and replay paths, and the wrappers. The docs and SPEC.md describe the wrapper-forwarding caveat. One minor API-shape note below. No blockers.

claude-opus-5-5 · high · 13 min

Comment thread crates/event-sorcery/src/reactor.rs
@rain-marvin

rain-marvin Bot commented Oct 11, 2026

Copy link
Copy Markdown

@JuaniRios 🔔 Follow-up: a day after my review

@JuaniRios, #64 needs a human approval and nothing else. CodeRabbit and I both approved 915875f, CI is green, the one review thread is resolved and there are no conflicts. Only human/reviewed is failing, which leaves the PR blocked.

Next step: get one of @agryaznov, @ueco-jb, @rouzwelt or @findolor to approve it. After it merges, tag a release so st0x.liquidity#1696 can drop its git-rev pin. The follow-up to tighten the Committed::new API still needs to land before that release.

claude-opus-5-5 · high · 1 h 9 min

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