Skip to content

Make the router's routing state authoritative instead of sniffed - #630

Draft
GoodForOneFare wants to merge 1 commit into
mainfrom
gordo-stdout-router-reenable
Draft

Make the router's routing state authoritative instead of sniffed#630
GoodForOneFare wants to merge 1 commit into
mainfrom
gordo-stdout-router-reenable

Conversation

@GoodForOneFare

@GoodForOneFare GoodForOneFare commented Aug 14, 2026

Copy link
Copy Markdown
Member

Bug

StdoutRouter.disable restored the original write method but left the write_without_cli_ui compatibility method installed. Because enabled? inferred routing from that method, the process could reach a contradictory state:

  • routing was inactive,
  • enabled? still returned true,
  • enable refused to route the stream again,
  • Capture#run accepted the false-positive state and silently captured nothing.

Resolving enable/disable only through the current $stdout and $stderr also orphaned routes when either global was replaced. with_enabled compounded the problem by unconditionally disabling on exit, including routes that existed before entering its scope.

Fix

Routing state now lives in a weak identity registry rather than being inferred from methods installed on a stream.

  • Routing membership is explicit. routed?(stream) checks the registry, enabled? remains as a compatibility alias, and routed_streams returns a read-only snapshot for inspection.
  • enable is additive and per-stream. It routes the current $stdout and $stderr without discarding routes temporarily displaced from those globals. disable(stream) removes one route; no-argument disable drains every live registered route.
  • Scoped activation unwinds only its own work. with_enabled remembers only streams it newly routed and removes only those on exit. If a later stream fails to activate, activation rolls back only streams added by that attempt and preserves pre-existing routes.
  • Capture establishes the routing it needs. Capture#run executes inside with_enabled, so disabled, half-routed, or partially replaced stdout/stderr globals are routed for the capture and restored afterward instead of raising or letting output escape.
  • The registry does not retain abandoned caller-owned streams. Active routes are held in an ObjectSpace::WeakMap; callers should still pair explicit enable/disable operations, but dropping a transient stream no longer pins it for the process lifetime.
  • The compatibility bypass remains a real bypass. The router-installed write_without_cli_ui closes over the stream's pre-routing write. write_without_routing uses the same method when available, so a later write wrapper can call either bypass without re-entering itself.
  • Independent cli-ui copies do not stack routers. A stream-owned ownership record distinguishes this router's permanent compatibility method from a foreign one. Activation preflights all current streams and benignly declines before changing anything if another copy already owns one.
  • Writer requires the pre-routing method explicitly. This prevents accidentally constructing a Writer around an already-routed write, preserves pre-existing singleton wrappers during normal disable, and lets in-flight writes finish after deactivation.
  • Temporary test streams use scoped routing. Tests that replace stdio with capture_io use with_enabled, avoiding accidental long-lived routes.

The earlier role-retagging and exact routing-state restoration layer was removed. Routing is additive and idempotent; scopes need only remember the streams they added, not rewrite the roles or wrappers of existing routes.

Tests

Coverage includes:

  • enable → disable → GC → re-enable,
  • transient and permanent $stdout/$stderr replacement,
  • aliased stdout/stderr,
  • targeted and global disable,
  • nested/scoped activation and failure rollback,
  • capture while disabled or partially routed,
  • compatibility and public bypasses called by later write wrappers,
  • foreign compatibility markers and duplicate-router hook prevention,
  • weak-registry collection of abandoned streams,
  • public routing inspection without mutable registry access,
  • pre-existing singleton write restoration,
  • in-flight writes and direct bypass behavior.

Validation:

  • 156 tests and 509 assertions across seeds 630, 20260817, and 8675309, with one existing skip,
  • RuboCop clean,
  • Sorbet clean.

🤖 Generated with LLM assistance

(posted by an LLM bot on behalf of Gord)

@GoodForOneFare
GoodForOneFare force-pushed the gordo-stdout-router-reenable branch from bb136f6 to d9e06ba Compare August 14, 2026 19:33
@GoodForOneFare GoodForOneFare changed the title Make StdoutRouter.disable reversible and with_enabled state-preserving Make the router's routing state authoritative instead of sniffed Aug 14, 2026
@GoodForOneFare
GoodForOneFare force-pushed the gordo-stdout-router-reenable branch 5 times, most recently from 812ee3f to cce3eac Compare August 17, 2026 14:09
Disabling restored write while leaving the compatibility marker behind, so marker sniffing could report a routed stream after routing had stopped. That made re-enabling unreliable and let captures silently miss output.

Track active streams in a weak identity registry. Enable is additive, disable can target one stream or drain every live route, and with_enabled unwinds only streams activated by its scope. Roll back only newly activated streams if a later activation fails.

Have Capture establish that scope itself so partially replaced stdout/stderr globals are routed for the capture without disturbing existing routes. Expose routed? and a read-only routed_streams snapshot while retaining enabled? compatibility.

Pin write_without_cli_ui and write_without_routing to the pre-routing write so later wrappers cannot re-enter themselves. Keep ownership with the stream, preflight foreign compatibility markers, and decline rather than stack a second router from another cli-ui copy.

Require Writer's original write explicitly, preserve pre-existing singleton writes, and let abandoned caller-owned streams fall out of the weak registry without an explicit disable.

Co-authored-by: River <river@shopify.com>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Assisted-By: devx/c0685ec8-2be2-4abc-9131-691e185f92ae
@GoodForOneFare
GoodForOneFare force-pushed the gordo-stdout-router-reenable branch from cce3eac to 63edccc Compare August 27, 2026 20:25
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