Skip to content

fix(sdks/go): recover durable waits after reconnect - #4448

Merged
igor-kupczynski merged 3 commits into
mainfrom
go-durable-wait-reconnect
Jul 20, 2026
Merged

fix(sdks/go): recover durable waits after reconnect#4448
igor-kupczynski merged 3 commits into
mainfrom
go-durable-wait-reconnect

Conversation

@igor-kupczynski

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

Copy link
Copy Markdown
Contributor

Description

Go durable waits now report pending callbacks after reconnect and periodically so persisted completions can be replayed.

Why:

  • Stream interruptions previously lost callback routing, leaving completed durable waits stuck.
  • Periodic status reconciliation allows the engine to rediscover and redeliver completed entries.

How:

  • Track outstanding durable callback waits and report them through WorkerStatus after connecting.
  • Resend current waiting entries whenever callback state changes and every second.
  • Preserve pending callbacks across reconnects and reconcile them with engine responses.
  • Atomically deliver or briefly buffer early completions, with race-safe shutdown handling.

similarity to other SDKs

Python

  • async def _poll_worker_status(self) -> None:
    if self._request_queue is None or self._worker_id is None:
    return
    if not self._pending_callbacks:
    return
    waiting = [
    DurableTaskAwaitedCompletedEntry(
    durable_task_external_id=task_ext_id,
    invocation_count=inv_count,
    node_id=node_id,
    branch_id=branch_id,
    )
    for (task_ext_id, inv_count, branch_id, node_id) in self._pending_callbacks
    ]
    request = DurableTaskRequest(
    worker_status=DurableTaskWorkerStatusRequest(
    worker_id=self._worker_id,
    waiting_entries=waiting,
    )
    )
    await self._request_queue.put(request)

TypeScript

  • private _pollWorkerStatus(): void {
    if (!this._workerId || this._pendingCallbacks.size === 0) return;
    const waitingEntries: DurableTaskAwaitedCompletedEntry[] = [];
    for (const key of this._pendingCallbacks.keys()) {
    const parts = key.split(':');
    waitingEntries.push({
    durableTaskExternalId: parts[0],
    invocationCount: parseInt(parts[1], 10),
    branchId: parseInt(parts[2], 10),
    nodeId: parseInt(parts[3], 10),
    });
    }
    this._enqueueRequest({
    workerStatus: {
    workerId: this._workerId,
    waitingEntries,
    } as DurableTaskWorkerStatusRequest,
    });
    }

Fixes #4445

Type of change

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

What's Changed

  • Report pending callback waiting entries after reconnect and while waits remain pending.
  • Add one deterministic reconnect regression test.

Checklist

Changes have been:

  • Tested (unit, integration, or manually with steps specified)
  • Linted and formatted
  • Documented (where applicable)
  • Added to CHANGELOG (where applicable) -- see Keep a Changelog

Testing

  • go test -race ./pkg/client -count=1
  • go test ./sdks/go/... -count=1
  • gofmt -d pkg/client/durable_task_listener.go pkg/client/durable_task_listener_test.go
  • git diff --check -- pkg/client/durable_task_listener.go pkg/client/durable_task_listener_test.go

🤖 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: Cursor with GPT-5.6 Sol was used to develop and test the implementation and draft this pull request.

@vercel

vercel Bot commented Jul 16, 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, Comment Jul 20, 2026 1:42pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-deadlock job failed on this PR (optimistic-scheduling=false). This check is non-mandatory and does not block merging, but may be worth investigating. View logs

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-deadlock job failed on this PR (optimistic-scheduling=true). This check is non-mandatory and does not block merging, but may be worth investigating. View logs

@igor-kupczynski igor-kupczynski added sdk-go Related to the Go SDK and removed engine Related to the core Hatchet engine labels Jul 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-deadlock job failed on this PR (optimistic-scheduling=true). This check is non-mandatory and does not block merging, but may be worth investigating. View logs

@igor-kupczynski
igor-kupczynski marked this pull request as ready for review July 16, 2026 15:28
@github-actions github-actions Bot added engine Related to the core Hatchet engine and removed sdk-go Related to the Go SDK labels Jul 16, 2026
@igor-kupczynski
igor-kupczynski requested a review from grutt July 20, 2026 07:50
Keep queued durable requests paired with their waiters until a stream has actually connected, preventing reconnect retries from creating orphaned requests.
@igor-kupczynski
igor-kupczynski force-pushed the go-durable-wait-reconnect branch from 91d5b0b to d05cb4c Compare July 20, 2026 13:39
@igor-kupczynski
igor-kupczynski merged commit dea544c into main Jul 20, 2026
62 of 66 checks passed
@igor-kupczynski
igor-kupczynski deleted the go-durable-wait-reconnect branch July 20, 2026 14:49
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.

[BUG] Go SDK durable waits can hang after stream reconnect without worker_status polling

2 participants