Skip to content

core: Bridge::switchBackend and installReconnectHandler still call the blocking registration verbs, so a backend that says kCallerMustNotBlock is blocked there anyway #615

Description

@Yaraslaut

Found while fixing #593. Not folded into that PR (#585): #593 is about the one call site that acquires a model for a new handler; this is about the two that re-acquire one for handlers that already exist, and they are a different change.

The finding

Bridge has four places that register a model on a backend. After #585 and #593, two of them go through the structural surface and honour IBackend::bindWaitPolicy(); the other two still call the blocking legacy verbs directly, with no policy check and no structural path at all:

  • Bridge::switchBackend, phase 1 (include/morph/core/bridge.hpp, the staging loop):

    auto newId = binding->shared
                     ? newShared->registerModelShared(
                           binding->typeId, binding->modelFactory,
                           {.contextKey = binding->contextKey, .primary = binding->primary})
                     : newShared->registerModelWithContext(binding->typeId, binding->modelFactory,
                                                           binding->contextKey);
  • Bridge::installReconnectHandler's reconnect callback, which runs the same two verbs on the backend's transport thread.

Both run under std::scoped_lock const lock{_mtx, _attachMtx}.

For a QtWebSocketBackend with Config::asyncRegistrationEnabled set — the WASM configuration — those verbs are sendSync, which pumps a nested QEventLoop on the Qt thread. That is precisely the block #568 exists to remove, and #593 has just given the interface a way to say "do not do this to me" that these two sites do not consult. So a WASM client that never blocks on its first registration can still block on a reconnect or a backend swap.

docs/spec/concurrency_and_lifetimes.md already names the reconnect half as an open hazard, from the lifetime angle rather than the WASM one:

installReconnectHandler's reconnect callback. Left as a liveness() check […] The handler runs on the backend's transport thread and calls registerModelWithContext/registerModelShared, which blocks on a nested QEventLoop for QtWebSocketBackend. […] No safe mechanical fix is known for this site; it remains open, tracked as the residual scope of issue #489.

A second, smaller finding inside the first

The two sources disagree about who owns the reconnect half. docs/spec/core/backend.md's migration table assigns it to #570:

| morph#570 | The example GUIs and the WASM spike; Bridge::installReconnectHandler onto bindModel. | Open |

…but #570's own body scopes itself to examples/ only, listing eleven example files and three docs, and never mentions bridge.hpp. Whichever is right, one of them is wrong, and the work is currently owned by nobody in writing. switchBackend is named by neither.

Why it matters beyond WASM

switchBackend is documented as atomic — phase 1 stages every re-registration and rolls back on a throw. Moving it to bindModel is not a one-line substitution, because the structural surface reports failure through a Completion rather than a throw and the rollback is built on the throw. That is the substance of this ticket, and it is why it should not be done inside a PR about something else.

Verification status

Inferred from reading the code on 48195d0e (#585's branch with #593's fix). Not reproduced. Stated plainly because the distinction is load-bearing here:

  • What I did: read both call sites and confirmed by grep that neither names bindModel, promoteModel or bindWaitPolicy; confirmed QtWebSocketBackend::registerModelWithContext/registerModelShared both reach sendSync and that sendSync parks a nested QEventLoop; confirmed the spec/issue mismatch above by reading both.
  • What I did not do: build or run a WASM client, force a reconnect against a QtWebSocketBackend with asyncRegistrationEnabled set, or observe a deadlock. Every claim about runtime behaviour here is a reading of the dispatch chain.
  • Not measured: whether any shipped example currently sets asyncRegistrationEnabled and relies on reconnect or switchBackend. If none does, this is latent rather than active — which changes priority, not validity.
  • The full suite (1836 cases, MORPH_BUILD_QT=ON MORPH_BUILD_NET=ON, GCC 16.2.1 Debug) is green on that revision, so nothing in the tree exercises this today.

What would close it

Both sites reaching bindModel/promoteModel, with switchBackend's atomicity preserved — its rollback must key on a rejected Completion rather than on a thrown exception — and a test that a reconnect on a kCallerMustNotBlock backend does not park the calling thread. Per AGENTS.md's "would this still pass if the feature did nothing": that test must fail, by hanging, if the call is put back on the blocking verb, which is how #585 proved the equivalent claim for the first-registration path (examples/common/testkit/test_wasm_registration_path_native.cpp).

Also: decide whether the reconnect half is #570's or this ticket's, and make docs/spec/core/backend.md and #570 agree.

What would change the verdict

  • Close this if Bridge is deliberately allowed to block on a re-registration that it may not block on for a first registration — but then docs/spec/core/backend.md's migration row for ladder: move the example GUIs and the WASM spike onto the structural registration surface #570 needs deleting, and the reason wants writing down, because the two paths currently differ for no stated reason.
  • Close this if a reproduction shows a reconnect on an asyncRegistrationEnabled backend does not park the Qt thread, in which case my reading of sendSync is wrong and I would want to know where.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk

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

    area: coreSubsystem: corearea: qtSubsystem: qtbugSomething isn't workingtriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions