Skip to content

Make StatusInfoReader re-openable after close() - #3

Merged
thorwhalen merged 1 commit into
masterfrom
fix/reopenable-status-info-reader
Sep 22, 2026
Merged

thorwhalen merged 1 commit into
masterfrom
fix/reopenable-status-info-reader

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

Summary

Fixes i2mint/pchealthstream2py#1 — StatusInfoReader.open() called self.start() unconditionally, so a second open() after close() always raised RuntimeError: threads can only be started once, violating the reuse contract SourceReader's own class docstring demonstrates.

  • open() now stops and joins the previous run's worker thread before starting a new one, then reuses self for the very first run (a threading.Thread can only be started once) and a fresh threading.Thread(target=self.run, ...) for every subsequent reopen.
  • New keyword-only reopen_timeout_s (default None = wait indefinitely) bounds how long open() waits for the previous worker to stop; if it doesn't stop in time, raises a clear RuntimeError explaining why (rather than silently ending up with two producers feeding the same queue) instead of hanging forever with no None default in place of an unbounded wait — additive, keyword-only, fully backward compatible.
  • Two new regression tests: test_reader_can_be_reopened (drives 3 open/close cycles, asserts item counts don't drift — a leaked worker would double/triple the count each cycle) and test_reopen_does_not_leak_worker_threads (inspects the actual worker threads directly, asserting only the newest is alive after each reopen).

Verification

Branch already sat directly on current master (no rebase needed; the unrelated already-merged PR #2, a packaging/CI modernization, is already in this history). fleet_dependents.json lists no importers of pchealthstream2py. wads ci-local: PASSED — ruff format + lint, pytest py3.10/3.12 (5 passed, including both new reopen regression tests), build.

Closes #1

🤖 Generated with Claude Code

StatusInfoReader subclasses both SourceReader and threading.Thread, and
open() ended with self.start(). A Thread can only be started once, so the
second open() always raised "threads can only be started once" -- breaking
the reuse contract SourceReader's own class docstring demonstrates, and
breaking StreamBuffer, which calls source_reader.open() on every start.

open() now stops and joins the previous run's worker, then starts a fresh
daemon thread. The join is required rather than cosmetic: open() clears the
stop flag, and a previous worker still inside its time.sleep would never
observe the stop, leaving two producers appending to one queue (counts drift
9 / 18 / 27 instead of holding at 9).

The first open() is unchanged -- it still starts self -- so is_alive(),
join() and the rest of the inherited Thread API behave exactly as before for
single-use readers, isinstance(reader, threading.Thread) stays True, and
close() stays non-blocking. Only the second open(), which until now raised
unconditionally, behaves differently.

How long open() waits is exposed as a keyword-only reopen_timeout_s,
defaulting to None (wait for as long as it takes). A caller that sets a
finite timeout and hits it gets an explanatory RuntimeError instead of two
silent producers.

Adds test_reader_can_be_reopened (three open/close cycles produce a steady
sample count) and test_reopen_does_not_leak_worker_threads (a timing-free
check that only the newest worker is ever alive). Both fail on the
unmodified code and both also fail against a join-less implementation.

Claude-Session: https://claude.ai/code/session_01L1aQPB34n7PU7jmbztSjBe
@thorwhalen
thorwhalen merged commit c572dba into master Sep 22, 2026
7 checks passed
@thorwhalen
thorwhalen deleted the fix/reopenable-status-info-reader branch September 22, 2026 12:57
thorwhalen added a commit that referenced this pull request Sep 22, 2026
#3 ran the first open() in self and later ones in fresh threads, so after a
reopen reader.is_alive() was False while the reader was reading and
reader.join() returned at once. Every open() now gets its own worker thread
and is_alive/join follow it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.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.

StatusInfoReader cannot be re-opened after close() (SourceReader contract violation)

1 participant