Skip to content

No declarative way to run work AFTER the transactional commit — After methods insert at the front, so they run before it #3975

Description

@jeremydmiller

Add a declarative way to say "run this after the transactional commit" — a convention or attribute that appends to Postprocessors rather than inserting at the front.

The problem

After reads like a post-handler hook that runs at the end. It does not run after the commit. Chain.cs:360 and MiddlewarePolicy.cs:94 both do:

chain.Postprocessors.Insert(i, frame);

— front of the list. The transactional commit is itself a postprocessor (DocumentSessionSaveChanges, added by each store's PersistenceFrameProvider.ApplyTransactionSupport), so an After method runs before the write is durable.

That is a reasonable default for "after the handler," but it means there is currently no supported way to express "after the commit" — even though the position exists and Wolverine itself uses it: ApplyTransactionSupport does

chain.Postprocessors.Add(new DocumentSessionSaveChanges());
chain.Postprocessors.Add(new FlushOutgoingMessages());

so FlushOutgoingMessages is a first-party post-commit postprocessor.

What applications do instead, and why it's fragile

They hand-write a Frame and Add it from a policy sequenced after the persistence policy. That works — but the ordering is positional and unstated. Nothing in the source declares the intent, nothing validates it, and a change in policy application order silently moves the frame to the wrong side of the commit with every test still green.

The failure mode is the bad one: side effects that are supposed to follow a durable write instead run before it, and only diverge when a commit fails.

Where this has cost real time

Two cases in CritterWatch, both of which reached for After first because the name implies it:

  1. An alert raise/resolve latch that had to be set only after the append committed. Using After recorded "alert raised" for a transaction that could still fail. The workaround was a hand-written early SaveChangesAsync inside the handler.
  2. An ingest cache write-back that must not publish state for a batch that was never persisted — same shape, same workaround. Its code comment calls the resulting explicit save "the most load-bearing one in the codebase."

In both cases the correct mechanism (a frame appended after the commit) existed but had to be discovered by reading Wolverine's own frame-provider source.

Ask

Something like:

public static class SomeHandler
{
    public static void Handle(...) { }

    [WolverineAfterCommit]                       // or: an `AfterCommit` conventional method name
    public static void AfterCommit(...) { }
}

that appends to Postprocessors after the persistence provider's commit frame, with the same variable-binding rules After already has.

Two properties matter more than the spelling:

  • It must land after the commit frame regardless of policy ordering — that is the whole point; getting it by luck of sequencing is what applications already do.
  • It must not run when the commit throws. Frames are concatenated without a try/finally (HandlerChain.cs:709-717), so unwinding already gives this — worth pinning with a test so it stays true.

Nice-to-have

If After's pre-commit position is kept (and it probably should be, for compatibility), consider mentioning the distinction in the middleware docs. The name is doing a lot of work and currently points the wrong way for anyone thinking about durability.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions