Repository navigation
client: retire an idle session instead of reusing one the server ended - #13
Merged
Merged
Conversation
An idle client sends nothing, so the server ends its session after its
2-minute idle timeout without the client knowing. The next Dial went out
on that session and failed as soon as the signed unknown-session answer
came back ("soar: server lost the session"). In Lantern, where Soar is
used minutes apart, this cost the first Soar attempt after every pause.
Dial now handshakes afresh when the session has had no streams and no
authenticated answer for Config.SessionIdle (default 90 s, under the
server's 2 minutes). SPEC 7.4 notes the behavior.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQFpfCXs3JqrfyDuiGabF5
A concurrent Dial could open a stream between the busy check and the close, and lose it. retireIfIdle now checks and closes under the session's lock, and a Dial whose session was retired between getting it and opening its stream handshakes afresh instead of failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQFpfCXs3JqrfyDuiGabF5
There was a problem hiding this comment.
🟢 Approval recommended
The synchronized retirement and retry logic have targeted regression coverage, with no blocking issues identified.
0 open findings
What changed in this PR
Prevents failed dials after long idle periods by replacing inactive client sessions before reuse.
Changes:
- Adds configurable session retirement with a 90-second default.
- Synchronizes retirement with stream creation and retries concurrent retirement.
- Documents the behavior and adds regression coverage.
| File | Description |
|---|---|
| SPEC.md | Documents idle-session reconnection guidance. |
| e2e_test.go | Tests successful dialing after the server’s idle timeout. |
| client.go | Implements idle retirement and safe dial retries. |
| client_gaps_test.go | Tests retirement eligibility and active-session preservation. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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.
Summary
The first Soar dial after a pause of more than 2 minutes fails within about one round trip with
soar: server lost the session. That is the usual pattern in Lantern, where Soar requests are minutes apart. This PR makes the client handshake again in that case instead of reusing a session the server has already ended.Why it fails:
client.go,fillLocked: no polls when idle).IdleTimeout(2 minutes by default;server.go,sweep).Client.session()still returned the old session, because nothing on the client side had closed it.sequenceDiagram autonumber participant K as kindling<br/>race participant C as Client<br/>client.go participant S as Server<br/>server.go K->>C: DialContext, request 1 C->>S: data queries on sid N S-->>C: answers, lastAuth = t0 Note over C: no streams left, so no polls are sent Note over S: server.go sweep<br/>lastSeen older than IdleTimeout of 2m<br/>session N ended ⚠️ K->>C: DialContext, request 2, minutes later rect rgba(255, 200, 200, 0.3) Note over C: client.go session()<br/>sid N not closed locally, so it is reused 🐛 C->>S: data query on sid N S-->>C: signed NXDOMAIN, unknown session C-->>K: soar: server lost the session endSeen in production. In SigNoz client traces from 10.0.3, Soar attempts failed about 0.1–0.2 s after starting with
connect_failed/ "server lost the session". This happened on 2026-10-14 and twice on 2026-10-15 at 10:46 and 15:03 UTC; those two were the first Soar attempts to join the race on kindling's new 8 s start delay. Soar never got a usable attempt in those requests.Change
Config.SessionIdle, default 90 s.session()closes the current session and starts a new handshake when that session meets all three conditions:busytestfillLockeduses, moved intoclientSession.busy().SessionIdle.retireIfIdle), so another Dial can't open a stream between them.errSessionIdle).lastAuthstays fresh, and the 15 s liveness check already covers it if it goes dead.The fix costs one handshake round trip after a long pause, instead of a failed request. A server restart, or any other loss the client can't foresee, still ends the in-flight stream as before.
Tests
TestIdleSessionRetiredBeforeServerForgetsIt(e2e): the server hasIdleTimeout= 1 s and the client hasSessionIdle= 500 ms. After a 2 s pause, the first dial must work. Without the fix it fails with exactly the production error,soar: server lost the session.TestDialRetiresOnlyAnIdleSession: a session that isn't idle yet is kept, a session with a stream open is kept, and an idle session is closed and replaced. Both guards were mutation-checked: removing either one fails the test.session()returning andopenStreamcan't be forced without a test hook. A stress test with concurrent dials hit it in only about 10–15% of bursts, so it was dropped. The check and close are atomic undercs.mu, which the stream-open case above covers.go test -race ./...passes.TestIdleSessionSurvivesResumealso still passes: a 17 s pause stays under 90 s, so the session is kept.The Rust client has the same bug. The matching fix is getlantern/soar-rs#4.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QQFpfCXs3JqrfyDuiGabF5
Summary by CodeRabbit