Skip to content

A ClickedEvent whose circuit is gone is DROPPED silently — while the action it triggers is fire-and-forget and outlives the circuit #3566

Closed

Description

@rbuergi

What happened

A ClickedEvent whose originating circuit has been disposed is dropped with a warning and nothing
else
— while the action it would have triggered is deliberately fire-and-forget and outlives the
circuit
. So a user who clicks a button and immediately navigates away gets a silent nothing, and
there is no way for them or for anyone reading the UI to tell the difference between "it ran" and "it
was discarded".

Measured on MeshWeaver.Education run
34042620439, job
101512796719 — the Store Install button.

Playwright trace, attempt 1:

15:44:38.03  Is visible … getByRole('button', {name: /^(Install$|Update to)/})  → true
15:44:38.03  Click … locator resolved to <button …>, performing click action, click action done   (no error)
15:44:38.06  Navigate to "/Store"      ← 20 ms later

Portal log, the next five seconds:

15:44:38.205  Circuit connection DOWN: id=_XUK2rPty6NdzA3VHePOWQcrxate5THzbdSGQ2rZKeo
15:44:38.205  Circuit closed … / Disposing per-circuit portal hub portal/_XUK2… on circuit close
15:44:38.362  Orleans: delivering RawJson to e2e-admin/Packages, sender=portal/_XUK2…
15:44:38.580  [ROUTE] Directed delivery to pod hub 'portal/_XUK2…' was refused: its OWNER RELEASED the address
15:44:43.410  warn: Dropping ClickedEvent for stream JRGdthy_Aka7g8BnYLXctQ on hub e2e-admin/Packages:
              no synchronization hub found on this hub or any parent — the target stream is gone
              (disposed circuit, released read stream, or never-created sync hub).

Drop site: src/MeshWeaver.Data/DataExtensions.cs:488-494 (LogStreamMessageDrop), reached from
:440. WithClickAction's body — here CatalogLayoutAreas.InstallPackage — is never invoked.

Why this is not simply "the circuit was gone, so of course"

🚨 The action does not need the circuit, and the framework already knows that. InstallPackage
ends in RunAsSystem(...).Subscribe(...) — fire-and-forget. In the very same run, the retry's install
ran to completion 850 ms AFTER its own circuit closed. So the handler is perfectly capable of
outliving the circuit; the only thing the disposal removes is the path the event takes to reach it.

🚨 And it is not decided by how fast you navigate. The retry had a tighter window — 4 ms
click→navigate, circuit closed 90 ms after the click — and it won. What differed was load: the
failing attempt's card locator took 2 939 ms to settle versus 905 ms on the retry, i.e. the
page was still streaming. So the outcome turns on server load at the moment of the click, which is
precisely the shape that makes it rare in testing and reachable in production.

The denominator

Dropping ClickedEvent appears exactly once in run 34042620439 — that click — and zero times
in run
34080328179, where the
same install succeeded. One occurrence, one failure; zero occurrences, zero failures.

Why it belongs in the "half-lands and nothing notices" family

The 5-second sync-hub grace means the drop is logged after the event arrived, at Warning, on the
server. The clicking user sees an unchanged page. Nothing retries, nothing surfaces, and the next
thing that reads the expected outcome — a probe, another view, a person — sees an absence that looks
exactly like "you never clicked". In the run above that cost a 300-second probe of a package that was
never going to exist.

What is NOT being proposed

  • Not a retry timer or a resubscribe. If the event is genuinely undeliverable, the fix is not to
    poll for a stream that is gone.
  • Not widening the sync-hub grace. That is a bound, and moving it moves the cliff.

The scope call, which is the maintainer's

Two coherent answers, and this issue does not pick one:

  1. Deliver it anyway. An action bound with WithClickAction that does not read back through the
    originating stream has no dependency on that stream; routing the event to the target hub without
    requiring a live sync hub would make the click land. The risk is actions that DO write back to the
    caller's stream and would then complete into nothing.
  2. Refuse it visibly. Keep the drop, but stop it being silent — surface it to whatever is still
    attached, and/or raise it above Warning so an operator sees a lost user action rather than a
    routine line.

Whichever is taken, the current state — the action is capable of running, the event is thrown away,
and only a Warning five seconds later records it — is the one that should not stand.

Provenance

Measured 2026-09-07 from the full job log, the portal log and both retained Playwright traces of run
34042620439, plus run 34080328179 and today's green run 34088440149 as controls. Filed out of
MeshWeaver.Education#275, whose own text says bundling would give one issue two root causes — the
harness half (a Store install click with no outcome check and no wait, unlike its sibling install()
helper which retries in rounds) stays there.

Activity

  1. rbuergi commented on Sep 7, 2026

    @rbuergi
    ContributorAuthor

    Fixed and merged — closing by hand (the merge queue does not fire Closes)

    PR #3601 merged. A ClickedEvent whose Blazor circuit has gone is now REFUSED to its sender — a
    DeliveryFailure the portal surfaces as the standard error modal, plus one Error line naming the
    action and the area — instead of being dropped while the fire-and-forget action it triggers outlives
    the circuit.

    The scope call, recorded because it was a judgement and not a measurement. The issue named two
    options: deliver the action anyway, or refuse visibly. This took refuse visibly, on the grounds
    that a dropped action the user believes succeeded is the worse failure. The maintainer confirmed that
    direction before it landed. If the intent was ever that the action should still run, this is the
    wrong half and should be re-cut rather than extended.

    Data-sync traffic deliberately keeps the historical silent drop — a data frame for a subscriber
    that has gone away wants no audience, and NACKing every one would be pure teardown noise. That
    control is asserted as its own test, and it is what makes this a measurement of the user-action
    class rather than of "everything now NACKs".

    Verified before merge: DroppedUserActionIsRefusedTest 3/3 (including the control and the marker
    test that a fourth user-action event added later cannot silently rejoin the churn class);
    MeshWeaver.Documentation.Test 305/305; -warnaserror clean on all three touched projects.

    Closed 2026-09-07 during a fleet-wide sweep.

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