fix(jobcontroller): give each job reconnect its own deadline - #3485
fix(jobcontroller): give each job reconnect its own deadline#3485kuangren777 wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. WalkthroughThe job controller adds a 15-second per-job reconnect timeout and a concurrency limit of eight. Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The PR gives each job its own reconnect deadline and limits concurrent reconnects, improving recovery for high-latency connections; no actionable merge-blocking risk remains beyond normal checks and review. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
onConnectionUp created a single 5s context and reused it for every ReconnectJob call in a serial loop. The deadline covers the whole batch, so on a high-latency connection only the first few jobs complete before it elapses; every remaining job then fails simultaneously with "context deadline exceeded" and its block stays stalled until the user tears the connection down by hand. Observed on a Tailscale DERP-relayed link (~85ms RTT), where a single job reconnect costs 0.55-1.31s: [conn:...] connection became connected, reconnecting jobs ... successfully reconnected to job manager (x3, over ~5s) [job:...] error reconnecting: failed to get job: context deadline exceeded (x12, same ms) [conn:...] finished reconnecting jobs: 3/15 successful On a LAN each reconnect costs ~30-50ms, so the shared budget fits 100+ jobs and the bug is invisible. Give each reconnect its own timeout and run them with bounded concurrency, so recovery no longer depends on RTT or job count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 23b715d95b4307a61fae4190689fde2358fbaffe)
544914a to
3cadccb
Compare
Problem
onConnectionUpcreates a single 5-second context and reuses it for everyReconnectJobcall in a serial loop:The deadline covers the whole batch rather than each job. On a high-latency connection only the first few jobs finish before it elapses; every remaining job then fails at the same instant with
context deadline exceededand its block stays stalled. Those jobs are not retried — the only recovery is switching the block's connection to local and back, which tears down the conncontroller and starts a fresh job.Reproduction
macOS client to a Linux host over a Tailscale DERP-relayed link, ~85ms RTT (relay-only, no direct path). 15 durable jobs on the connection. Laptop sleeps, sshd reaps the session, laptop wakes:
All 12 failures land in the same millisecond, which is the shared deadline expiring rather than 12 independent timeouts. Measured cost of one successful reconnect on this link: 0.55s and 1.31s. 5s divided by ~1.5s is about 3 jobs, matching
3/15.On a LAN a reconnect costs roughly 30-50ms, so the same 5s budget covers 100+ jobs and the bug is invisible. It only shows up over WAN/relayed links, where it makes durable blocks look permanently hung after every sleep/wake.
Fix
Give each reconnect its own deadline and run them with bounded concurrency. Recovery then depends on the slowest single job rather than on job count multiplied by RTT.
Notes
syncandpanichandlerwere already imported; no new dependencies.ReconnectJobalready dedups per job id viasingleflight.Group, so concurrent calls for distinct job ids are safe.ReconnectJobTimeout(15s) andReconnectJobConcurrency(8) are exported consts. Happy to tune these or make them configurable.go vet ./pkg/jobcontroller/...andgo build -race ./pkg/jobcontroller/are clean.pkg/jobcontrollercurrently has no test files, andonConnectionUpreaches directly into thewstoreglobal DB and the package-levelReconnectJob. If you want coverage here I am happy to follow up with a small refactor that makes the reconnect function injectable, so the batching semantics can be tested without a live DB or connection.Verification
Built and run on macOS arm64 against the same setup. Forced a disconnect by killing the sshd session, with 9 durable jobs attached:
209ms for 9 jobs, averaging 23ms each, which is below the cost of a single reconnect round trip on this link -- the reconnects overlapped as intended. Before the change the same connection reported
3/15 successfulafter 5.6s.Re-tested under injected latency to reproduce the original failure condition. Added
tc qdisc add dev tailscale0 root netem delay 100mson the remote host, bringing RTT to 107ms (measuredrtt min/avg/max/mdev = 106.945/107.466/108.317/0.512 ms), with 13 durable jobs attached:13/13 in 1.34s. 13 jobs over a concurrency of 8 is two waves, ~0.67s each, which matches the per-job cost at this RTT. The pre-change serial loop would need ~8.7s for the same set and would have abandoned the tail at the shared 5s deadline.