Stop publishing Failed for a drop that is about to be retried - #20
Open
adventures92 wants to merge 1 commit into
Open
adventures92 wants to merge 1 commit into
adventures92 wants to merge 1 commit into
Conversation
Two contradictions of the documented ConnectionState contract, one cause. With reconnection on, a ping timeout produced: [Disconnected, Connecting, Connected, Failed(PingTimeout), Reconnecting(1)] Failed is documented as terminal — "nothing recovers a Failed namespace automatically" — and the guide's own example reacts to it by refreshing a token or showing a retry button. A consumer following that prompted the user over a transient drop the library then recovered from unaided. With reconnection off, the same timeout settled on a bare Disconnected rather than Failed(PingTimeout): the error was discarded, leaving a deliberate close() and a connection that died indistinguishable. ConnectionManager.onEngineError wrote Failed straight to the client state, bypassing updateAggregateState. An engine error always arrives before the close it triggers, and that close recomputes the aggregate — overwriting it. Meanwhile NamespaceSocketImpl.onEngineError never touched state at all, so onEngineClose fell through to Disconnected and dropped the cause. The error is now recorded and handed to the namespaces at close time, and only when the close ends the attempt. updateAggregateState already derives Failed from namespace state, so the client level follows without a second write. While reconnecting the namespaces go to Reconnecting and the error stays on the errors flow, where a retried drop belongs. terminalError is cleared unconditionally so an error from a connection since retried cannot resurface on a later clean close. State is still mutated only on the WorkQueue worker: the engine emits on the shared queue and these handlers run inline. PingTimeoutConnectionStateTest pins both halves at the SocketClient level, which EngineConnectionHeartbeatTest does not — it drives EngineConnection directly and asserts only the errors flow. It needed startEmbedded to forward sendServerPings, which it accepted but silently dropped. Verified across 8 consecutive full jvmTest runs. apiCheck clean: the change is confined to internal classes and moves no public API. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two contradictions of the documented
ConnectionStatecontract, one cause. Found by testswritten during the CI investigation and deliberately held back until they could be explained.
What the library actually did
Failedpublished for a drop about to be retried.guide/connection-state.mdstates"Nothing recovers a
Failednamespace automatically", and the guide's own example does this:A consumer following that prompted the user over a transient drop the library recovered from
moments later, unaided.
Failedlost when it was genuinely terminal. With reconnection off the same timeout settledon a bare
Disconnected, discarding the error — so a deliberateclose()and a connection thatdied produced identical state, with nothing in
connectionStateto tell them apart.Cause
ConnectionManager.onEngineErrorwroteFailedstraight to the client state, bypassingupdateAggregateState(). An engine error always arrives before the close it triggers, and thatclose recomputes the aggregate — overwriting it. Meanwhile
NamespaceSocketImpl.onEngineErrornever touched state at all, so
onEngineClosefell through toDisconnectedand dropped the cause.Both symptoms, one root: the error was written to the wrong level, at the wrong time.
Fix
Record the error; hand it to the namespaces at close time, and only when the close ends the
attempt.
updateAggregateState()already derivesFailedfrom namespace state, so the clientlevel follows without a second write.
Reconnectingand the error stays on theerrorsflow, where a retried drop belongs. No
Failedis published at all.Failed(cause)and the aggregate follows.terminalErroris cleared unconditionally, not only when consumed — otherwise an error froma connection since retried could resurface on a later clean
close().Invariants checked
WorkQueueworker. The engine emits on the shared queueand these handlers run inline, so the new field is only ever touched there.
apiCheckpasses; the change is confined tointernalclasses andConnectionState.Failedalready existed.Tests
PingTimeoutConnectionStateTestpins both halves at theSocketClientlevel, whichEngineConnectionHeartbeatTestdoes not — that drivesEngineConnectiondirectly and asserts onlythe
errorsflow. It neededstartEmbeddedto forwardsendServerPings, a parameter it acceptedand silently dropped, which is why it could not compile before.
Verified across 8 consecutive full
jvmTestruns, plusapiCheck,spotlessCheck,detekt,check-docs.sh, and anmdbook build.Documentation
Consumer-visible, so
CHANGELOG.mdgets a### Fixedentry under## [Unreleased].Two guide corrections — the code was wrong and the prose was right, but the prose was imprecise:
reconnection.mdconflated the retried and terminal cases. A ping timeout underreconnection = trueis not terminal, and now says so.connection-state.mdnow states thatDisconnectedversusFailedis how a consumer tells adeliberate shutdown from a dropped connection, without watching the
errorsflow alongside.Worth releasing promptly
Sockit is now listed on klibs.io, showing
0.0.2—the affected version. Indexed projects pick up new versions the day after they reach Central.
🤖 Generated with Claude Code