Skip to content

fix: Stop the worker thread promptly when the client is closed during a reconnect wait - #93

Merged
jsonbailey merged 2 commits into
mainfrom
jb/sdk-3206/close-during-reconnect-wait
Sep 28, 2026
Merged

jsonbailey merged 2 commits into
mainfrom
jb/sdk-3206/close-during-reconnect-wait

Conversation

@jsonbailey

@jsonbailey jsonbailey commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

connect checks the stop state only before sleep(interval), and Kernel#sleep cannot be interrupted. When close is called during a reconnect wait, the worker thread stays alive until the interval ends (up to 30 seconds). Then it sends one more request, and the connection from that request is not closed.

This PR:

  • Replaces the sleep with a wait on a Concurrent::Event that close sets, so the wait ends when the client is closed.
  • Checks the stop state again after the wait, so no request is sent after close.
  • Closes a connection that opens while close runs, before the worker thread returns.
  • Uses the event as the only stop state. The separate AtomicBoolean is removed.

This problem was reported externally in ruby-eventsource PR 54. That PR proposed Thread#kill, which can stop the thread in the middle of a user callback. This PR takes a different approach.

Repro

A local TCP server closes the stream, and the client is closed during the reconnect wait:

  • Before: the thread lived 0.71s (reconnect_time: 1) and 2.85s (reconnect_time: 3) after close, and each run sent 1 request after close.
  • After: the thread exits in 0.00s, and 0 requests are sent after close.

Testing

New specs in spec/client_spec.rb:

  • close during a reconnect wait stops the worker thread promptly: with reconnect_time: 5 and a server that returns 500, the worker thread ends within 1s of close.
  • close during a reconnect wait does not send another request: the server gets only the first request.
  • closes a connection that opens while the client is closed: a query_params callback calls close, so the request always opens a connection after close. A raw TCP server checks that the client closes that connection.

Mutation check: when I reverted each fix line by itself, its spec failed and did not hang. The reverted lines were: event wait back to sleep, no check after the wait, no connection close in run_stream, and no event set in close.

bundle exec rspec spec: 174 examples, 0 failures (run twice). bundle exec rubocop --parallel: no offenses.

Known, not fixed here

With http 6, @http_client is a non-persistent HTTP::Session, so @http_client.close does nothing. Because of this, close cannot interrupt a request that is still connecting or waiting for response headers. The thread stops when that request returns or times out, and the new check then closes the connection.

Jira

SDK-3206


Note

Overview
Fixes a shutdown bug where close during a reconnect backoff could leave the LD/SSEClient worker alive until sleep finished (up to ~30s), then issue one more HTTP request and sometimes leave that connection open.

Stop signaling is consolidated on a Concurrent::Event: close sets it, loops use set?, and reconnect backoff uses @stop_event.wait(interval) instead of Kernel#sleep, so the wait ends immediately on close. connect re-checks stop after the wait so no retry request runs once closed. run_stream adds a post-connect check that reset_http and returns if close raced after a successful connect (e.g. during query_params).

New specs cover prompt worker exit, no extra request after close during wait, and closing a connection opened after the client is already closed.

Reviewed by Cursor Bugbot for commit 2433eef. Bugbot is set up for automated code reviews on this repo. Configure here.

@jsonbailey
jsonbailey marked this pull request as ready for review September 25, 2026 20:59
@jsonbailey
jsonbailey requested a review from a team as a code owner September 25, 2026 20:59
@jsonbailey
jsonbailey merged commit 054b11b into main Sep 28, 2026
12 checks passed
@jsonbailey
jsonbailey deleted the jb/sdk-3206/close-during-reconnect-wait branch September 28, 2026 13:56
jsonbailey pushed a commit that referenced this pull request Sep 28, 2026
🤖 I have created a release *beep* *boop*
---


##
[3.0.0](2.6.0...3.0.0)
(2026-09-28)


### ⚠ BREAKING CHANGES

* Report a server-initiated stream close to the error handler
([#91](#91))

### Features

* Report a server-initiated stream close to the error handler
([#91](#91))
([7e4dc1b](7e4dc1b))


### Bug Fixes

* Declare logger as a runtime dependency
([#94](#94))
([f7809c2](f7809c2))
* Stop the worker thread promptly when the client is closed during a
reconnect wait
([#93](#93))
([054b11b](054b11b))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> **Release 3.0.0** — bumps the package version from `2.6.0` to `3.0.0`
in `.release-please-manifest.json`, `lib/ld-eventsource/version.rb`, and
documents the release in `CHANGELOG.md`.
> 
> This is a Release Please cut, not new library logic in the diff. The
changelog records what ships in **3.0.0**: a **breaking** behavior
change where a **server-initiated stream close** is reported through the
client **error handler** (e.g. `StreamClosedByServerError`), plus fixes
to declare **`logger` as a runtime dependency** and to **stop the worker
thread promptly** when the client is closed during a reconnect backoff
wait.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d5431c5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

2 participants