Procedure and one-off-query callbacks are silently lost when the socket dies #41
Labels
No labels
area:defold
area:docs
area:infra
area:protocol
area:testing
kind:bug
kind:chore
phase:breadth
phase:core
phase:hardening
phase:slice
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
yuris/spacetimedb-defold#41
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while actioning the review on #29 (PR #38). Out of scope there, and left deliberately.
The problem
PR #38 fixed this for reducer calls: when the socket dies,
set_sendwalks the in-flightCallhandles and settles each witherror = "connection lost", so a caller learns their request will never complete.Procedures and one-off queries got no such treatment, because they have no handle to settle. Their
c.pendingentries are dropped when the socket is replaced, and the callback is never invoked. A consumer that wrote:waits forever for a callback that cannot arrive. That is exactly the shape of the bug #38 closed for
call_reducer, still open on two other paths.Why it matters more than a stuck table entry
The leaked
c.pendingentry is minor — it is one table per lost request, bounded by how many requests were in flight. The real cost is the silent non-completion: a game that disables a button until a procedure returns leaves it disabled forever, and nothing anywhere reports why. This SDK's whole documented posture is that failures should be loud or absent.Two ways to fix it
call_procedurereturns something withon_ok/on_error, andset_sendsettles it like aCall. Consistent, and it makes the whole request surface uniformly observable. Larger, and it adds public API that #24 freezes.set_send: walk every remainingc.pendingentry regardless ofkindand invoke its callback with an error. Much smaller, no new API, and it fits the existing(err, ...)contract. The catch is thatcall_reducer's legacy callback deliberately does not fire on a dead socket (PR #38 asserts that, because it never used to and the shape is frozen), so doing it for procedures would make the two paths inconsistent in the opposite direction.Option 2 is probably right for 1.0 — smaller, no frozen surface added — but the inconsistency with
call_reducer's legacy callback has to be a deliberate, documented choice rather than an accident. Whichever is chosen, say why the other was not.Related asymmetry, same area
M.subscribe(the legacy whole-set form) parks pending entries inc.subscriptionsandresubscribe_allre-issues them with a throwawayfunction() endcallback. So the legacy subscribe form has no handle and no way to observe a rejection — strictly less observable thansubscribe_query. That is deliberate, not a bug, but it belongs in the same conversation if anyone proposes handles forM.subscribe.Acceptance
A test proving a
call_procedureand aone_off_queryin flight when the socket dies both reach their callback with an error, and thatc.pendingis empty afterwards. Mutation-check it: removing the settle must make the test hang or fail, not pass quietly.