Skip to content

fix(go): preserve result subscriptions across reconnect - #4571

Merged
igor-kupczynski merged 6 commits into
mainfrom
atomic-result-listener-handoff
Jul 31, 2026
Merged

fix(go): preserve result subscriptions across reconnect#4571
igor-kupczynski merged 6 commits into
mainfrom
atomic-result-listener-handoff

Conversation

@igor-kupczynski

@igor-kupczynski igor-kupczynski commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Description

Context: On the client, during reconnect, we have basically two stream: the old (now broken), the new (the one being established). Let's call this process a "handoff" from the old stream to the new stream. Concretely (in connectOnce) the following steps happen during the handoff:

  1. Replay. Re-send every subscription currently in the registry onto the new stream, so the server knows about them on the new connection .
  2. Publish. Swap the client field to the new stream and bump generation, so every future snapshot() returns the new stream. [until this completes, the snapshot() returns the old stream].
  3. Retire. CloseSend the old stream.

The bug is that 1 and 2 are not on the same critical section: if a new registration happened after 1, but before 2, it would be registered on the old broken stream (so therefore wouldn't happen).

This is a minimal fix that brings all 3 steps under a single sendMu mutex. (The side effect is that the registrations are stalled during the handoff, but it's the right tradeoff here).

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Test changes (add, refactor, improve or change a test)

What's Changed

  • Serialize replay and stream installation with workflow-result registration sends
  • Add a deterministic regression for registration during reconnect handoff

Testing

I've iterated a bit on this, and settled on pkg/client/listener_reconnect_invariant_test.go. It tests the invariant not the mechanism so even if it +300 it's worth adding. The invariant under test: every AddWorkflowRun that returns no error has its run ID sent on the valid stream.

Two tests:

  1. TestAddWorkflowRunDuringReconnectLandsOnReplacement targets the bug.
  2. TestWorkflowRunsListenerReconnectChaosPreservesRegistrations chaos testing.

Both tests were run against the parent commit (red) and fix (green).

Checklist

Changes have been:

  • Tested (unit, integration, or manually with steps specified)
  • Linted and formatted
  • Documented (not applicable; no public API change)
  • Added to CHANGELOG (not applicable)

🤖 AI Disclosure
  • I acknowledge that an LLM was used in the creation of this Pull Request, in accordance with Hatchet's AI_POLICY.md.

  • Details: Used Cursor agents to investigate the concurrency interleaving, implement the focused fix and regression test, and review the resulting diff. All listed tests and linters were run locally.

Serialize replay and stream publication with new registrations so successful sends cannot be stranded on a retiring client.
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hatchet-docs Ready Ready Preview Jul 31, 2026 1:53pm

Request Review

@github-actions github-actions Bot added the engine Related to the core Hatchet engine label Jul 30, 2026
Replace the glassbox handoff regression (which observed sendMu contention
via a production-code hook) with two tests driven entirely through the
public listener API and the constructor seam:

- a targeted regression that holds a reconnect open mid-replay and
  asserts a registration made inside the window reaches the replacement
- a seeded chaos test: concurrent registrations, per-send latency, and
  breaks at fixed registration progress, asserting the current stream at
  quiescence saw every successful registration (replayable via
  HATCHET_CHAOS_SEED)

Both fail on the parent commit (5/5 and 11/20 iterations) and pass with
the fix, so the TryLock/onSendMuContention scaffolding is removed from
reconnecting_stream.go; the production diff is now the pure critical
section change.
Drop references to the pre-fix behavior and trim wording; the comments now
describe only what the tests do and why, for readers of the merged code.
Comment thread pkg/client/reconnecting_stream.go Outdated
connectOnce installs under its own sendMu hold via installClientLocked;
the self-locking wrapper had no production callers left. The one test
that installs out-of-band now takes sendMu at the call site.
@igor-kupczynski
igor-kupczynski merged commit 6b365b0 into main Jul 31, 2026
59 of 61 checks passed
@igor-kupczynski
igor-kupczynski deleted the atomic-result-listener-handoff branch July 31, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine Related to the core Hatchet engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants