Skip to content

feat(batching): CoalesceBy de-duplication + IBatchContext (GH-3289 Phase 1)#3300

Merged
jeremydmiller merged 1 commit into
mainfrom
feat-batching-phase1-context-coalesce-3289
Jul 5, 2026
Merged

feat(batching): CoalesceBy de-duplication + IBatchContext (GH-3289 Phase 1)#3300
jeremydmiller merged 1 commit into
mainfrom
feat-batching-phase1-context-coalesce-3289

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Phase 1 of the #3289 batch-processing plan — the two safe additions that only add behavior and never change what gets acknowledged. (Phase 0 docs are #3299.)

Item 1 — CoalesceBy (de-duplicate a batch by key)

The "trigger storm" case: 1,000 RecalculateScores events about ~40 aggregates reach the handler as 1,000 items and each aggregate is recomputed ~25×. CoalesceBy collapses that to one message per key, last-wins:

opts.BatchMessagesOf<RecalculateScores>(b =>
{
    b.BatchSize = 500;
    b.TriggerTime = 10.Seconds();
    b.CoalesceBy((RecalculateScores x) => x.AggregateId); // handler sees distinct keys, last wins
});
  • New internal CoalescingMessageBatcher<T,TKey>: groups by tenant id (exactly like DefaultMessageBatcher<T>), then de-duplicates within each tenant group.
  • Settlement is unchanged. Only what the handler sees shrinks — every member envelope still rides on the batch (new Envelope(coalesced, allMembers)), so inbox/outbox tracking and dead-lettering behave identically to a normal batch. Dropping 1,000 messages → 40 distinct keys still settles all 1,000.
  • BatchingOptions.CoalesceBy<T,TKey>(Func<T,TKey>) is sugar over the existing Batcher seam. It validates that the selector's element type matches the batched element type and throws a clear message otherwise. The lambda parameter must be explicitly typed ((RecalculateScores x) => ...) so both type args infer without restructuring the existing BatchMessagesOf<T>(Action<BatchingOptions>) API — no breaking change to existing call sites.

Item 5 — IBatchContext (read-only batch identity)

public static void Handle(Item[] items, IBatchContext batch)
{
    // batch.BatchId  -> stable id to correlate this batch's logs
    // batch.Members  -> per original member: MessageId, Attempts, SentAt
}
  • Injected via a new BatchContextVariableSource, built from the active Envelope the same way TenantId and the "now" clock are supplied (registered in WolverineOptions alongside them).
  • BatchContext is public (private ctor, public static For(Envelope) factory) because the generated handler code has no access to Wolverine internals and must call the factory to read the internal Envelope.Batch.
  • Purely informational — reading it never changes acknowledgement/settlement. Under CoalesceBy, Members still lists every original member.

Tests

src/Testing/CoreTests/Acceptance/batch_coalescing_and_context.cs:

  • CoalescingMessageBatcher unit tests: last-wins de-dup, all members retained on the batch, tenant grouping, element-type guard, CoalesceBy installs the coalescing batcher.
  • End-to-end: CoalesceBy (handler sees the deduped set) and IBatchContext injection (BatchId non-empty, Members counts all published).

All 6 new tests green; the existing 34 batch acceptance tests still pass. dotnet build wolverine.slnx -c Release clean.

Docs

docs/guide/handlers/batching.md: new "De-duplicating a batch with CoalesceBy" and "Batch identity with IBatchContext" sections.

🤖 Generated with Claude Code

 Phase 1)

Phase 1 of the #3289 batch-processing plan — the two "safe additions" that
only add behavior and never change what gets acknowledged.

Item 1 — CoalesceBy:
- New CoalescingMessageBatcher<T,TKey> groups by tenant id (like the default
  batcher) then de-duplicates by a user key, last-wins for the message the
  handler sees. EVERY member envelope still rides on the batch, so settlement
  (inbox/outbox tracking + dead-lettering) is identical to a normal batch.
- BatchingOptions.CoalesceBy<T,TKey>(Func<T,TKey>) installs it; validates the
  selector's element type matches the batched element type.

Item 5 — IBatchContext:
- Read-only batch identity (BatchId + Members: MessageId/Attempts/SentAt)
  injectable into any handler via a new BatchContextVariableSource, built from
  the active Envelope exactly like TenantId/now are supplied. BatchContext is
  public so the generated handler code can call its For(Envelope) factory.

Tests: CoalescingMessageBatcher unit tests (last-wins, all-members-retained,
tenant grouping, element-type guard) + end-to-end CoalesceBy and IBatchContext
injection acceptance tests. Docs updated with both features.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit c8962d6 into main Jul 5, 2026
26 checks passed
This was referenced Jul 9, 2026
This was referenced Jul 13, 2026
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