You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
core: Bridge::switchBackend and installReconnectHandler still call the blocking registration verbs, so a backend that says kCallerMustNotBlock is blocked there anyway #615
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 asyncRegistrationEnabledand 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.
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
Bridgehas four places that register a model on a backend. After #585 and #593, two of them go through the structural surface and honourIBackend::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):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
QtWebSocketBackendwithConfig::asyncRegistrationEnabledset — the WASM configuration — those verbs aresendSync, which pumps a nestedQEventLoopon 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.mdalready names the reconnect half as an open hazard, from the lifetime angle rather than the WASM one: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:…but #570's own body scopes itself to
examples/only, listing eleven example files and three docs, and never mentionsbridge.hpp. Whichever is right, one of them is wrong, and the work is currently owned by nobody in writing.switchBackendis named by neither.Why it matters beyond WASM
switchBackendis documented as atomic — phase 1 stages every re-registration and rolls back on a throw. Moving it tobindModelis not a one-line substitution, because the structural surface reports failure through aCompletionrather 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:grepthat neither namesbindModel,promoteModelorbindWaitPolicy; confirmedQtWebSocketBackend::registerModelWithContext/registerModelSharedboth reachsendSyncand thatsendSyncparks a nestedQEventLoop; confirmed the spec/issue mismatch above by reading both.QtWebSocketBackendwithasyncRegistrationEnabledset, or observe a deadlock. Every claim about runtime behaviour here is a reading of the dispatch chain.asyncRegistrationEnabledand relies on reconnect orswitchBackend. If none does, this is latent rather than active — which changes priority, not validity.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, withswitchBackend's atomicity preserved — its rollback must key on a rejectedCompletionrather than on a thrown exception — and a test that a reconnect on akCallerMustNotBlockbackend 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.mdand #570 agree.What would change the verdict
Bridgeis deliberately allowed to block on a re-registration that it may not block on for a first registration — but thendocs/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.asyncRegistrationEnabledbackend does not park the Qt thread, in which case my reading ofsendSyncis wrong and I would want to know where.🤖 Generated with Claude Code
https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk