Skip to content

feat: End a stream with an error when neither an event nor a heartbeat arrives for 30 seconds. - #268

Merged
goloroden merged 1 commit into
mainfrom
feat/heartbeat-timeout
Oct 3, 2026
Merged

goloroden merged 1 commit into
mainfrom
feat/heartbeat-timeout

Conversation

@goloroden

Copy link
Copy Markdown
Member

The server sends a heartbeat every second on the streams of observe_events and run_eventql_query. If the connection stalls without being closed (for example behind a proxy that keeps it open but passes nothing anymore), the client waited until some other timeout, and an application observing events kept believing it was live. The same change is made in all seven client SDKs.

  • observe_events and run_eventql_query now raise the new HeartbeatTimeoutError (a subclass of ServerError, exported from eventsourcingdb and eventsourcingdb.errors) when no line, heartbeat included, arrives for 30 seconds. The error propagates through async with response:, which closes the response and the connection.
  • Every line restarts the 30 seconds (an asyncio.timeout per readline()); the timeout never spans a yield, so the time the caller spends on an item does not count.
  • Only an expiry of that timeout becomes HeartbeatTimeoutError. Any other TimeoutError, and a caller's own cancellation or timeout, pass on unchanged.
  • read_events, read_subjects and read_event_types have no heartbeats and stay unchanged.
  • 30 seconds is a constant in an internal module, which the tests patch, so the public API only grows by the error class.
  • README: a paragraph under "Running EventQL Queries" and the subsection "Detecting a Stalled Connection" under "Observing Events".

Tests (tests/test_heartbeat_timeout.py, local aiohttp server, both calls): stall → HeartbeatTimeoutError and the client disconnects; heartbeats keep it alive; items in time are delivered; task.cancel() and the caller's own asyncio.timeout are not reported as the new error; a foreign TimeoutError passes unchanged. Red first: the stall tests ran into their 5 s guard. make qa green (uv lock check, ruff, pyright, bandit, 89 tests with Docker); the new tests also pass under Python 3.14.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TCdPte6nY6ToUVd2prb9GM

…t arrives for 30 seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCdPte6nY6ToUVd2prb9GM
@goloroden
goloroden requested a review from a team as a code owner October 3, 2026 21:33
@goloroden goloroden self-assigned this Oct 3, 2026
@goloroden
goloroden merged commit 0565aa6 into main Oct 3, 2026
4 checks passed
@goloroden
goloroden deleted the feat/heartbeat-timeout branch October 3, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant