Skip to content

Hypothesis fault-injection for the Rust pump's FD lifecycle (#74, #286) - #244

Open
leynos wants to merge 48 commits into
mainfrom
python-pipeline-streams-tests
Open

Hypothesis fault-injection for the Rust pump's FD lifecycle (#74, #286)#244
leynos wants to merge 48 commits into
mainfrom
python-pipeline-streams-tests

Conversation

@leynos

@leynos leynos commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

The Rust inter-stage pump takes over the raw pipe descriptors from asyncio for the duration of a transfer, across several partial-failure paths — FD extraction, reader-transport pause/resume, and blocking-mode switch/restore — with no isolated seam for fault injection (#74).

Seams

New module cuprum/_pipeline_stream_fds.py (extracting the FD lifecycle also lifts _pipeline_streams.py back under the 400-line health cap: 300 + 166):

  • _BlockingModeGuard — the FD-state object. engage switches the descriptor pair to blocking mode capturing prior state (rolling back a partial change on failure); restore returns them to that state.
  • _paused_reader — a context manager wrapping _pause_reader_transport so the resume cannot be skipped on any exit path (normal return, exception, or cancellation).

_run_rust_pump is refactored (via _pump_over_raw_fds) to drive these. Behaviour is preserved — the existing test_pipeline_stream_backend_selection.py suite (repointed to the new module) still passes, including the pause→drain→restore→resume ordering and the writer-toggle rollback tests.

Fault-injection tests

cuprum/unittests/test_pipeline_streams_fd_lifecycle.py covers the four hazards #74 names:

Hazard Test
Leaked blocking state round-trip property over initial modes, plus an injected toggle failure asserting no descriptor is left switched
Missing resume _paused_reader resumes exactly once on normal and exception exit; skips resume when the transport can't pause or pausing raises
Wrong fallback a blocking-toggle failure returns the Python-fallback signal (False) and still resumes the reader
Swallowed unexpected errors _surface_unexpected_pipe_failures raises the first non-pipe exception and suppresses BrokenPipeError/ConnectionResetError

Validation

Full gates green: make check-fmt, make lint (ruff, interrogate 100%, pylint 10.00/10 — both modules under the line cap), make test (762 passed / 47 skipped; Rust nextest 57/57). Wheel-manifest snapshot regenerated for the two new files.

Closes #74

This branch also carries the Rust-pump observation channel — the PumpEvent
type and public RustPumpDeclineReason, the observe_pump hook registry on
its own ContextVar, the PumpMetricsHook metrics adapter, the
cuprum_rust_pump_declined_total and
cuprum_rust_pump_failed_after_cancel_total counters, and ADR 008 recording
the decision. That work is required by #286, which records its acceptance
criteria; it is not incidental scope.

Closes #286

🤖 Generated with Claude Code

Summary by Sourcery

Isolate the Rust pump’s raw file-descriptor lifecycle behind dedicated helpers and add targeted fault-injection tests for pause/resume and blocking-mode behaviour.

Enhancements:

  • Extract the FD extraction, pause/resume, and blocking-mode management logic from the pipeline streams module into a new _pipeline_stream_fds module with a _BlockingModeGuard and _paused_reader seam.
  • Refactor the Rust pump path to route FD handling through _pump_over_raw_fds, keeping existing behaviour while simplifying _run_rust_pump.

Tests:

  • Add hypothesis-based and unit tests validating the FD blocking-mode guard, reader pause/resume context manager, Rust pump fallback behaviour, and error-surfacing semantics for pipe-related failures.
  • Update backend-selection tests to use the new FD lifecycle module directly and to manage OS-level pipes without depending on internals of _pipeline_streams.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Summary

  • Add raw file-descriptor lifecycle helpers in cuprum/_pipeline_stream_fds.py.
  • Refactor Rust pump routing for rollback, reader pause/resume, cancellation-safe cleanup, fallback, and teardown diagnostics.
  • Add pipe-task lifecycle helpers and shared ContextVar registration support.
  • Add structured DEBUG records for fallback and teardown failures.
  • Add pump observation APIs with PumpEvent, observe_pump(), PumpHookRegistration, and PumpMetricsHook.
  • Export the observation APIs from cuprum.
  • Add Hypothesis and unit tests for lifecycle failures, cancellation, fallback, metrics, observability, descriptor cleanup, and pipe-error handling.
  • Regenerate the wheel-manifest snapshot.
  • Close issues #74 and #286.

Documentation

  • Document descriptor ownership, lifecycle guarantees, fallback behaviour, cancellation, and diagnostics.
  • Document pump observation metrics, hook registration, failure handling, and module boundaries.
  • Add ADR 008 for the Rust-pump observation channel and index it in docs/contents.md.
  • Add screen-reader descriptions for Figures 3, 6, and 7.
  • Update docs/execplans/4-3-1-parametrize-existing-stream-unit-tests.md with the supersession note for relocated pipeline modules.
  • Document the new pump observation APIs and metrics in CHANGELOG.md.

Walkthrough

Split pipe-task orchestration from stream handling. Add safe descriptor extraction, reader pause and resume, blocking-mode rollback, cancellation-safe Rust pumping, fallback reporting, descriptor cleanup checks, and a dedicated pump observation channel with hooks and metrics.

Changes

Rust pump lifecycle

Layer / File(s) Summary
Pipe-task orchestration
cuprum/_pipeline_pipe_tasks.py, cuprum/_pipeline_internals.py, cuprum/_pipeline_wait.py, cuprum/_process_lifecycle.py, cuprum/unittests/test_pipeline_pipe_tasks.py
Move pipe-task creation, capture gathering, result collection, and failure filtering into a dedicated module. Update callers and cancellation teardown tests.
Descriptor lifecycle and Rust pump hand-off
cuprum/_pipeline_stream_fds.py, cuprum/_pipeline_streams.py, cuprum/unittests/test_pipeline_streams_fd_lifecycle.py, cuprum/unittests/test_pipeline_streams_cancellation.py, cuprum/unittests/test_pipeline_stream_backend_selection.py, cuprum/unittests/test_pipeline_streams_blocking_mode.py, cuprum/unittests/test_pipeline_fd_cleanup.py, cuprum/unittests/_rust_pump_test_helpers.py
Manage raw descriptor extraction, reader pausing, blocking-mode changes, rollback, restoration, fallback, cancellation, and teardown diagnostics.
Pump events and metrics
cuprum/pump_events.py, cuprum/pump_observation.py, cuprum/adapters/pump_metrics.py, cuprum/__init__.py, cuprum/unittests/test_pump_observation.py, cuprum/unittests/test_pump_metrics_adapter.py, cuprum/unittests/test_pipeline_streams_observability.py
Add typed pump events, scoped hooks, bounded decline reasons, cancellation-failure events, metrics counters, and channel-isolation tests.
Shared registration lifecycle
cuprum/_token_registration.py, cuprum/context/registration.py, cuprum/context/state.py
Share ContextVar token installation, restoration, detachment, and context-manager behaviour between context and pump-hook registrations.
Validation and documentation
cuprum/unittests/__snapshots__/test_maturin_build.ambr, docs/adr-008-rust-pump-observation-channel.md, docs/contents.md, docs/cuprum-design.md, docs/developers-guide.md, docs/execplans/4-3-1-parametrize-existing-stream-unit-tests.md, docs/users-guide.md, CHANGELOG.md, typos.local.toml
Update wheel snapshots, ADR records, guides, design documentation, execution-plan notes, changelog entries, and typo-check configuration.

Sequence Diagram(s)

sequenceDiagram
  participant Pipeline
  participant ReaderTransport
  participant BlockingModeGuard
  participant RustPump
  participant PumpObservation
  participant MetricsCollector
  Pipeline->>ReaderTransport: pause_reading()
  Pipeline->>BlockingModeGuard: engage(reader_fd, writer_fd)
  BlockingModeGuard->>RustPump: run transfer
  RustPump-->>BlockingModeGuard: complete or fail
  BlockingModeGuard->>BlockingModeGuard: restore descriptor modes
  Pipeline->>ReaderTransport: resume_reading()
  Pipeline->>PumpObservation: emit decline or cancellation-failure event
  PumpObservation->>MetricsCollector: increment mapped counter
Loading

Possibly related PRs

  • leynos/cuprum#136 — Shares Rust pump descriptor extraction and hand-off logic.
  • leynos/cuprum#156 — Shares the ContextVar token-registration lifecycle used by pump-hook registration.
  • leynos/cuprum#223 — Shares pipeline stream, pipe-task, and cancellation-safe teardown changes.

Suggested labels: Issue

Poem

Pause the reader. Guard the flow.
Set descriptor modes, then restore them.
If Rust declines, Python proceeds.
Cancellation waits for the worker.
Hooks record each decision.

🚥 Pre-merge checks | ✅ 16 | ❌ 4

❌ Failed checks (4 inconclusive)

Check name Status Explanation Resolution
Developer Documentation ❓ Inconclusive Evidence collection is still in progress. Inspect the roadmap, changed APIs, and documentation references before deciding.
Testing (Unit And Behavioural) ❓ Inconclusive Placeholder only. Await code and test inspection.
Performance And Resource Use ❓ Inconclusive Investigation is still in progress; no verdict submitted yet. Gather implementation and historical diff evidence before deciding.
Architectural Complexity And Maintainability ❓ Inconclusive Assessment pending repository inspection. Inspect the new lifecycle and observation abstractions, their dependency edges, and reuse evidence before deciding.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the Rust pump FD lifecycle work and links both related issues, #74 and #286.
Description check ✅ Passed The description clearly covers the FD lifecycle seams, fault-injection tests, observation channel, and linked issue objectives.
Linked Issues check ✅ Passed The changes address the linked issues through FD lifecycle safeguards, fallback tests, pump observability, metrics, API exports, and documentation.
Out of Scope Changes check ✅ Passed The implementation, tests, public API changes, telemetry, and documentation support the objectives of issues #74 and #286.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Testing (Overall) ✅ Passed Pass: real Rust-pump decline paths, FD rollback, pause/resume cancellation, worker draining, observability, metrics, task teardown, and public FD cleanup have substantive assertions and non-vacuous...
User-Facing Documentation ✅ Passed The user guide documents Rust-pump fallback reasons, cancellation and teardown diagnostics, observer registration, both counters, label bounds, hook failure behaviour, and practical examples.
Module-Level Documentation ✅ Passed All 22 changed Python modules have module-level docstrings; new modules describe their purpose, role, utility, and relationships to pipeline, ContextVar, pump, or test components.
Testing (Property / Proof) ✅ Passed Mark PASS: substantive Hypothesis tests cover all FD mode pairs, injected toggle targets and error classes, normal/exception exits, cancellation, and bounded pipe-outcome sequences.
Testing (Compile-Time / Ui) ✅ Passed The PR changes Python only, with no new Rust/TypeScript compile-time surface. It adds a normalised wheel snapshot and focused assertions for structured logs, events, and metrics.
Unit Architecture ✅ Passed Keep the separation: FD mutations stay in _BlockingModeGuard, metrics use an injected MetricsCollector, and hooks use a scoped ContextVar; tests verify rollback, fallback, restoration, and is...
Domain Architecture ✅ Passed Raw FD, asyncio transport, and Rust handling stay in private pipeline modules; metrics translation stays in adapters, and pump_events/pump_observation have no runtime adapter imports.
Observability ✅ Passed Structured logs cover declines, cancellation-masked failures, observer errors, and teardown failures; bounded counters cover routing and cancellation failures, while native spans record pump operat...
Security And Privacy ✅ Passed Pass this check: no secrets or credentials were added; events and metrics expose only bounded reasons, while logs contain fixed metadata and Rust pump diagnostics without payloads or command argume...
Concurrency And State ✅ Passed Accept: _await_rust_pump drains cancelled workers before restore; immutable ContextVar hooks isolate tasks; guard rollback is tested; cancellation, interleaving, ordering, and cleanup tests cover...
Rust Compiler Lint Integrity ✅ Passed The PR diff has zero Rust or Cargo paths; the Rust tree has no broad dead-code/import suppressions, and its only clone is an intentional captured-event snapshot.
📋 Issue Planner

Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).

View plan for ticket: #74

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch python-pipeline-streams-tests

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors the Rust pump FD lifecycle management into a dedicated module with reusable blocking/pause guards, updates the Rust-pump dispatch path to use these abstractions, and adds focused property- and fault-injection tests around FD blocking state, reader pause/resume, and error surfacing behavior.

Sequence diagram for _pump_over_raw_fds FD lifecycle and fallback

sequenceDiagram
    participant Pump as _pump_over_raw_fds
    participant Reader as asyncio_StreamReader
    participant Writer as asyncio_StreamWriter
    participant Guard as _BlockingModeGuard
    participant Rust as rust_pump_stream

    Pump->>Reader: _paused_reader(reader)
    activate Reader
    Pump->>Pump: _drain_reader_buffer(reader, writer)

    Pump->>Guard: _BlockingModeGuard.engage(reader_fd, writer_fd)
    alt [OSError from engage]
        Guard-->>Pump: OSError
        Pump-->>Pump: return False
    else [engage ok]
        Pump->>Rust: loop.run_in_executor(None, rust_pump_stream, reader_fd, writer_fd)
        Pump->>Guard: restore()
        Pump-->>Pump: return True
    end
    deactivate Reader
Loading

File-Level Changes

Change Details Files
Extract FD lifecycle helpers into a dedicated module and wire them into the Rust pump path.
  • Introduce _pipeline_stream_fds.py with helpers for extracting FDs from asyncio transports and pausing reader transports
  • Add _BlockingModeGuard to encapsulate switching pipe FDs into blocking mode and restoring prior state, including rollback on partial failure
  • Add _paused_reader context manager to ensure reader transports are always resumed when pausable
  • Refactor _pump_over_raw_fds to use the new abstractions when handing control to the Rust pump
  • Update _pipeline_streams to import and use the new FD lifecycle utilities instead of local implementations
cuprum/_pipeline_stream_fds.py
cuprum/_pipeline_streams.py
Adjust existing backend-selection tests to the new FD lifecycle seams and direct OS interactions.
  • Switch tests to import os directly instead of accessing it through _pipeline_streams
  • Update mocks and monkeypatches to target the new FD lifecycle module for pause, blocking, and restore behavior
  • Ensure ordering and rollback expectations (pause→drain→restore→resume and writer-toggle failure) are preserved under the refactor
cuprum/unittests/test_pipeline_stream_backend_selection.py
Add focused Hypothesis-based fault-injection tests around FD blocking and pause/resume behavior.
  • Add property tests ensuring _BlockingModeGuard round-trips arbitrary initial blocking modes and never leaks transient blocking state on toggle failure
  • Add tests validating _paused_reader’s behavior with normal, exceptional, and non-pausable transports, including skip-resume semantics on pause failure
  • Add tests verifying that a blocking-toggle failure in the Rust-pump path causes a Python fallback and still resumes the reader
  • Add tests for _surface_unexpected_pipe_failures to raise the first unexpected exception while suppressing BrokenPipeError/ConnectionResetError
cuprum/unittests/test_pipeline_streams_fd_lifecycle.py

Assessment against linked issues

Issue Objective Addressed Explanation
#74 Introduce an isolated FD-state object and reader-transport context manager to manage the Rust pump FD pause/blocking lifecycle for easier fault injection and verification.
#74 Add fault-injection tests (primarily Hypothesis-based) that verify FD blocking mode is restored correctly and no blocking state is leaked, including partial failures during blocking-mode toggling.
#74 Add tests (primarily Hypothesis-based) around the Rust dispatch path and pipe-error suppression logic to catch missing reader-resume calls, incorrect fallback behaviour, and swallowed unexpected errors.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-access[bot]

This comment was marked as outdated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 904477a19b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cuprum/_pipeline_stream_fds.py
Comment thread cuprum/_pipeline_streams.py
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added the Issue label Jul 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cuprum/_pipeline_stream_fds.py`:
- Around line 53-74: Update _pause_reader_transport to return an explicit
success indicator alongside the resume callback, distinguishing a completed
pause from unsupported transport or pause errors. Update _paused_reader and
_pump_over_raw_fds to consume that indicator and return False before handing the
raw FD to Rust when pausing fails, while preserving the existing resume cleanup
for successful pauses.

In `@cuprum/_pipeline_streams.py`:
- Around line 164-168: Update the executor-based pump flow around
rust_pump_stream so cancellation does not immediately run guard.restore():
retain the executor future, await it to completion when the awaiting task is
cancelled, then restore the descriptors and resume _paused_reader only after the
worker thread returns. Add a regression test that cancels the pumping task
mid-transfer and verifies descriptor restoration occurs only after the worker
completes.

In `@cuprum/unittests/test_pipeline_streams_fd_lifecycle.py`:
- Around line 65-73: Update every lifecycle assertion in this test, including
the assertions at the referenced ranges and the shown guard.restore checks, to
include a concise diagnostic message using the assert message form. Make each
message identify the specific invariant being validated, including current
versus expected blocking state where applicable.

In `@docs/cuprum-design.md`:
- Around line 2106-2108: Update the `_paused_reader` documentation to state that
the reader transport resumes only after a successful pause; when pause hooks are
unavailable or `pause_reading()` fails, no resume callback is invoked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8fae609d-48da-47a8-87ad-fcaf4b1c2ae3

📥 Commits

Reviewing files that changed from the base of the PR and between 302858c and a3f900b.

📒 Files selected for processing (6)
  • cuprum/_pipeline_stream_fds.py
  • cuprum/_pipeline_streams.py
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/test_pipeline_stream_backend_selection.py
  • cuprum/unittests/test_pipeline_streams_fd_lifecycle.py
  • docs/cuprum-design.md
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)

Comment thread cuprum/_pipeline_stream_fds.py Outdated
Comment thread cuprum/_pipeline_streams.py Outdated
Comment thread cuprum/unittests/test_pipeline_streams_fd_lifecycle.py Outdated
Comment thread docs/cuprum-design.md Outdated
@pandalump

Copy link
Copy Markdown
Collaborator

All four findings verified against current code and fixed in 93cea31. None were stale.

1. _pause_reader_transport success indicator

Valid, with a nuance worth stating: the function returned None for two different situations — a transport with no pause/resume hooks, and a pause_reading() that raised — and those need opposite handling.

  • No hooks → there are no callbacks to race, so the hand-off is safe. test_dispatch_uses_rust_when_reader_transport_cannot_pause already pins this deliberately ("Missing pause/resume hooks should not force a Python fallback"), so falling back here would have broken an existing contract.
  • Pause raised → asyncio may still be consuming the descriptor, so handing it to Rust races that reader. This is the case that must fall back.

_pause_reader_transport now returns a _ReaderPause carrying may_hand_off and an optional resume; _paused_reader yields that indicator; and _pump_over_raw_fds returns False before engaging blocking mode when the pause failed. Resume cleanup for a successful pause is unchanged.

Two new tests cover the outcomes, plus one asserting the failed-pause path never reaches _BlockingModeGuard.engage.

2. Cancellation racing the worker thread

Valid, and the most serious of the four. run_in_executor cannot interrupt the worker thread, so on cancellation the old code ran guard.restore() while rust_pump_stream was still mid-transfer and owned both descriptors — and _paused_reader then resumed the transport on the way out. Both handed the descriptors back to asyncio while native code was still using them.

_await_rust_pump now retains the executor future, shields it so cancelling the task does not mark the future cancelled under a live thread, and drains it before propagating CancelledError. Restore and resume therefore happen only after the worker returns.

The regression test cancels mid-transfer (worker blocked on an Event) and asserts the observed ordering. I verified it is non-vacuous rather than assuming: reverting _await_rust_pump to the previous try/finally form makes it fail with

AssertionError: restore must happen only after the worker thread returns; observed order [...]

3. Assertion messages

Done — every lifecycle assertion in the module now carries a diagnostic naming the invariant, and the blocking-state assertions report current versus expected, e.g.

assert os.get_blocking(reader_fd) == reader_blocking, (
    "restore must return the reader FD to its prior mode; now "
    f"{os.get_blocking(reader_fd)}, expected {reader_blocking}"
)

grep for bare asserts in the file now returns zero.

4. _paused_reader documentation

Corrected. The design doc claimed it "always resumes", which overstated it. It now states that only a pause that actually took effect is resumed — no resume is invoked when the hooks are absent or pause_reading() raises — and documents the may_hand_off result. I also added a short paragraph on the cancellation ordering from finding 2, since that is now part of the module's contract.

Validation

Command Outcome
uv run pytest cuprum/unittests/test_pipeline_streams_fd_lifecycle.py 11 passed
uv run pytest cuprum/unittests/test_pipeline_stream_backend_selection.py 7 passed (existing contracts intact)
make check-fmt pass
make lint pass (ruff, interrogate 100%, pylint 10.00/10, clippy)
make typecheck pass (ty clean)
make test pass — Rust nextest 57/57, full Python suite green
make markdownlint / make nixie pass

The two fakes that patched _pause_reader_transport were updated for the new return type; both modules' existing assertions are otherwise unchanged.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai The Observability warning is addressed in 41281c2.

The finding was correct: the raw-descriptor hand-off has three partial-failure paths and all three were silent. That silence is not incidental — each one ends with the hop falling back to the Python pump and completing correctly, so nothing surfaces to the caller by design. The consequence is that a deployment which has quietly stopped taking the fast path looks identical to one that never had it, which is exactly the question an operator asks.

Each decline now emits a record against the cuprum._pipeline_streams logger, using the same field convention as the pipeline fail-fast records (cuprum_action, plus a reason):

cuprum_reason Seam that declined
raw_fd_unavailable _extract_stream_fd found no descriptor on either transport
reader_pause_failed pause_reading() raised, so asyncio may still be consuming
blocking_mode_unavailable _BlockingModeGuard.engage could not switch both descriptors

Level is DEBUG deliberately. A fall-back is a per-hop routing decision, not a fault, so a warning would make a correctly-working pipeline noisy on every platform where the fast path does not apply — the guide documents raising that single logger to diagnose fast-path coverage.

Two notes on what I did not do. I did not instrument the successful hand-off or _BlockingModeGuard.restore: those run once per hop on the hot path and would emit a record per inter-stage transfer, which is telemetry's job rather than logging's. And the new tests live in cuprum/unittests/test_pipeline_streams_observability.py rather than extending test_pipeline_streams_fd_lifecycle.py, which is already at 440 lines and over the repository's 400-line guidance.

The tests drive each reason through the real code path that emits it rather than calling the log helper, and I mutation-checked them rather than assuming: dropping any one of the three log calls fails that reason's case, and changing _LOGGER.debug to _LOGGER.warning fails the level assertion.

All six gates are green (check-fmt, lint, typecheck, test, markdownlint, nixie); the suite is 770 passed / 47 skipped with pylint at 10.00/10.

codescene-access[bot]

This comment was marked as outdated.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline 41281c2
printf '\n== candidate files ==\n'
fd -a '^(test_pipeline_streams_observability\.py|_pipeline_streams\.py)$' .

printf '\n== implementation outline ==\n'
ast-grep outline cuprum/_pipeline_streams.py --items all --match 'pump|raw|fd|LOGGER' || true

printf '\n== observability records and fallback context ==\n'
rg -n -C 5 'raw_fd_unavailable|reader_pause_failed|blocking_mode_unavailable|_LOGGER\.(debug|warning|info)' \
  cuprum/_pipeline_streams.py

printf '\n== focused tests ==\n'
cat -n cuprum/unittests/test_pipeline_streams_observability.py

Length of output: 27828


@pandalump Confirm the finding as addressed.

All three Python-fallback seams now emit one structured DEBUG record through cuprum._pipeline_streams, with the expected cuprum_action="rust_pump_declined" and distinct reasons. The focused parametrized test drives each real path and verifies the reason; the level test pins the intentional non-warning behaviour.

🐇 ✅


✏️ Learnings added
Learnt from: pandalump
URL: https://github.com/leynos/cuprum/pull/244

Timestamp: 2026-07-29T13:40:55.592Z
Learning: In `cuprum/_pipeline_streams.py`, Rust-pump fallback is an expected per-hop routing decision and must be recorded at DEBUG level, not warning level. Emit structured logger fields `cuprum_action="rust_pump_declined"` and a `cuprum_reason` identifying the declined seam (`raw_fd_unavailable`, `reader_pause_failed`, or `blocking_mode_unavailable`); avoid logging successful raw-FD hand-offs and blocking-mode restoration on the hot path.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]

This comment was marked as resolved.

codescene-access[bot]

This comment was marked as outdated.

Comment thread cuprum/_pipeline_streams.py
@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai Both pre-merge warnings addressed.

User-Facing Documentation — taken, in 5ba99e7. docs/users-guide.md gains a "Why a hop fell back to Python" section under Rust stream observability: the three cuprum_reason values with what each means, why they sit at DEBUG rather than WARNING, and how to raise that single logger when investigating throughput. You were right that this belongs in the users guide — selecting the rust backend does not guarantee every hop takes it, and that is a user-visible performance property even though the logs are diagnostic.

Out of Scope Changes — declining, because both items were added at this PR's own request.

The docs and the observability are not drive-by additions. Each was raised as a failing pre-merge check on this pull request in an earlier round, and each cites AGENTS.md:

  • Developer Documentation: "This introduces _BlockingModeGuard and _paused_reader as reusable lifecycle seams, but a repository-wide search found no mention of _pipeline_stream_fds in docs/ … Add the decision to the relevant project document and index it through docs/contents.md." (AGENTS.md:L118-L124)
  • Observability: the FD extraction, pause, and blocking-mode boundaries were flagged as silent, which they were.

So the diff contains them because this PR was told to add them. Recommending they be split out now would leave the check that required them failing, and the two checks cannot both be satisfied by any single diff.

They are also in scope on the criterion the guidance itself sets: documentation and observability for a change made in this pull request stay with it. _pipeline_stream_fds.py is created here; the seams documented are the ones added here; the three decline reasons logged are the three partial-failure paths this PR extracted. None of it describes pre-existing behaviour.

The snapshot update is not a change at all in the discretionary sense: cuprum/unittests/__snapshots__/test_maturin_build.ambr records the wheel's file manifest, so it drifts mechanically whenever a file is added under cuprum/. Leaving it stale fails test_maturin_wheel_build_snapshot.

The general rule I would like applied in future: when a check on this PR has required a change, a later out-of-scope finding against that same change should reconcile with the earlier requirement rather than reverse it.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@pandalump Acknowledge the documentation update in 5ba99e7.

Withdraw the out-of-scope finding. Keep the lifecycle documentation, fallback observability, and wheel-manifest snapshot in this PR: they directly support functionality introduced here, satisfy earlier PR-specific requirements, and the snapshot update is mechanically necessary for the package-manifest test.

Apply the stated reconciliation rule in future reviews: do not classify a change as out of scope when an earlier finding on the same PR required that change and it remains directly coupled to the PR’s implementation.

🐇


✏️ Learnings added
Learnt from: pandalump
URL: https://github.com/leynos/cuprum/pull/244

Timestamp: 2026-07-30T13:34:31.589Z
Learning: For `leynos/cuprum` pull requests, retain documentation and observability changes in the same PR when they directly describe or diagnose functionality introduced by that PR, especially when earlier PR review findings required them. Do not later flag those required, directly coupled changes as out of scope. Treat `cuprum/unittests/__snapshots__/test_maturin_build.ambr` updates as mechanically required whenever newly packaged files alter the wheel manifest.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

codescene-access[bot]

This comment was marked as outdated.

`_pump_over_raw_fds` catches `(OSError, ValueError)` from the blocking-mode
toggle, because `os.set_blocking` reports a closed descriptor as `ValueError`
and a bad one as `OSError`. Only the `OSError` half was injected here, so
narrowing that catch would still have passed this suite while crashing a hop
the Python fallback could have carried.

Parametrize over both classes, keeping the fallback and reader-resumption
assertions for each. No production change: the seam already handles both.
codescene-access[bot]

This comment was marked as outdated.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cuprum/unittests/test_pipeline_streams_fd_lifecycle.py`:
- Around line 339-350: Update both test call sites, including
test_pump_over_raw_fds_falls_back_when_pause_fails and the shown _run_rust_pump
test, to obtain reader and writer descriptors from owned_fds() instead of
literals 1 and 2. Wrap each invocation in the helper’s required with context so
the pump uses only test-owned pipe descriptors.

In `@cuprum/unittests/test_pump_metrics_adapter.py`:
- Around line 293-301: Update
test_a_decline_without_a_reason_falls_back_to_a_bounded_label to compare the
reason label against the existing UNKNOWN_DECLINE_REASON constant instead of the
duplicated "unknown" literal, matching the established usage elsewhere in the
module.

In `@docs/adr-008-rust-pump-observation-channel.md`:
- Around line 15-17: Update the ADR sentence near the inter-stage pipe hand-off
to state that a hop may decline that hand-off for one of four reasons,
preserving the following cancellation and worker-failure statement.

In `@docs/cuprum-design.md`:
- Around line 780-781: Update the statement describing the seven `ExecPhase`
values to limit completeness to the declared type, and acknowledge that
consumers need a policy for malformed or future runtime phase values; do not
claim hooks can never receive other values.

In `@docs/developers-guide.md`:
- Around line 497-500: The documentation wording in docs/developers-guide.md
lines 497-500 must describe cuprum_reason as naming the reason for the decline,
not the refusing seam. Apply the corresponding wording change in CHANGELOG.md
lines 21-22 so it says the field is labelled with the decline reason; no code
changes are required.

In `@docs/users-guide.md`:
- Around line 1359-1364: Update the raw_fd_unavailable description in the
cuprum_reason table to state that at least one asyncio transport may lack a raw
descriptor, matching the decline behavior of _try_rust_pump().
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b4dae902-d32c-4590-9799-b4f73c20ab1a

📥 Commits

Reviewing files that changed from the base of the PR and between 7f6ec92 and bccaf99.

📒 Files selected for processing (31)
  • CHANGELOG.md
  • cuprum/__init__.py
  • cuprum/_pipeline_internals.py
  • cuprum/_pipeline_pipe_tasks.py
  • cuprum/_pipeline_stream_fds.py
  • cuprum/_pipeline_streams.py
  • cuprum/_pipeline_wait.py
  • cuprum/_process_lifecycle.py
  • cuprum/_token_registration.py
  • cuprum/adapters/pump_metrics.py
  • cuprum/context/registration.py
  • cuprum/context/state.py
  • cuprum/pump_events.py
  • cuprum/pump_observation.py
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/_rust_pump_test_helpers.py
  • cuprum/unittests/test_pipeline_fd_cleanup.py
  • cuprum/unittests/test_pipeline_pipe_tasks.py
  • cuprum/unittests/test_pipeline_stream_backend_selection.py
  • cuprum/unittests/test_pipeline_streams_cancellation.py
  • cuprum/unittests/test_pipeline_streams_fd_lifecycle.py
  • cuprum/unittests/test_pipeline_streams_observability.py
  • cuprum/unittests/test_public_api.py
  • cuprum/unittests/test_pump_metrics_adapter.py
  • cuprum/unittests/test_pump_observation.py
  • docs/adr-008-rust-pump-observation-channel.md
  • docs/contents.md
  • docs/cuprum-design.md
  • docs/developers-guide.md
  • docs/execplans/4-3-1-parametrize-existing-stream-unit-tests.md
  • docs/users-guide.md
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)

Comment thread cuprum/unittests/test_pipeline_streams_fd_lifecycle.py
Comment thread cuprum/unittests/test_pump_metrics_adapter.py
Comment thread docs/adr-008-rust-pump-observation-channel.md Outdated
Comment thread docs/cuprum-design.md Outdated
Comment thread docs/developers-guide.md Outdated
Comment thread docs/users-guide.md Outdated
`make spelling` regenerates `typos.toml` from the shared base plus
`typos.local.toml`, but the inline-code-span exemption lived only in the
generated file — hand-edited in, so every regeneration deleted it and
three backtick-quoted identifiers (`artifact`, `color`) failed the gate
on a clean checkout. The pattern now lives in the overlay's
`[patterns] ignore`, which the generator merges and preserves. Verified
non-vacuous: a seeded violation outside a code span still fails the
gate.

Closes #294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

leynos added 2 commits August 7, 2026 15:23
Two fault-injection tests handed `reader_fd=1, writer_fd=2` to the pump.
Those are the test runner's own stdout and stderr. Nothing reaches a
syscall today because every descriptor-touching seam is monkeypatched,
but that is convention rather than construction: `_pause_reader_transport`
permits the hand-off for a reader with no transport, so a future test that
drops one patch would `fcntl` the runner's streams and the damage would
surface far from here. This repository has already fixed real fd-1/2
corruption once.

Route both call sites through `owned_fds()`, the helper written for
exactly this hazard, so the descriptors under test are ones the test owns.

Also assert the reasonless-decline label against `UNKNOWN_DECLINE_REASON`
rather than a second copy of the literal `"unknown"`; the constant is
already imported and used further down the same module, and a duplicated
operator-visible value drifts.
Four documentation claims had drifted from the code they describe.

ADR-008 said a hop may decline "for one of three reasons"; the enum has
four since `READER_UNRESUMABLE` joined it. Point at
`RustPumpDeclineReason` instead of restating a count, so a fifth reason
does not need an edit here.

The design document claimed there is no value beyond the seven declared
phases that a hook can receive. The same document, some seven hundred
lines later, describes a fail-closed reducer and a fail-open logging
adapter both handling unrecognized phases. Limit the claim to the declared
type and point at the run-time policy.

The developers' guide and the changelog both described `cuprum_reason` as
naming "the seam that refused". It carries the decline reason; the seam is
the actor, the reason is why it refused, and the guide's own Table 2 gives
both columns.

The users' guide implied `raw_fd_unavailable` needs both transports to
lack a descriptor. `_pump_over_raw_fds` declines on `reader_fd is None or
writer_fd is None`, so one is enough.
codescene-access[bot]

This comment was marked as outdated.

The dispatch-order test passed literal `reader_fd=1, writer_fd=2` to
`_run_rust_pump` — the test runner's own stdout and stderr. CodeRabbit
flagged the same pattern in the FD lifecycle tests; this site was not
flagged, but it is the identical trap.

It is inert today only because `_set_stream_fds_blocking` and
`_restore_stream_fd_blocking` are both monkeypatched here. That is
convention, not construction: `_pause_reader_transport` permits the
hand-off for a reader with no transport, so dropping either patch reaches
`fcntl` on descriptors 1 and 2 for real. This repository has already fixed
that corruption once, which is why "inert today by monkeypatch" is not a
good enough guarantee to leave in place.

Route it through `owned_fds()` like the others, so the descriptors under
test are ones the test owns. No literal descriptor arguments to the pump
helpers remain in the repository.
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos leynos changed the title Hypothesis fault-injection for the Rust pump's FD lifecycle (#74) Hypothesis fault-injection for the Rust pump's FD lifecycle (#74, #286) Aug 7, 2026
`_paused_reader` resumes in a bare `finally:`, so a `CancelledError` — a
`BaseException` — still undoes the pause. Nothing proved it. The only
test driving both exits parametrized `body_raises` over a synchronously
raised `ValueError`, which an `except Exception:` guard catches just as
well, so narrowing the guard would skip the resume on cancellation and
leave the whole suite green.

Add a test that parks a task on an await inside the block and cancels it
from outside, asserting both that the `CancelledError` reaches the
awaiter and that the transport is resumed exactly once. Under the
narrowed guard it fails on `resume_calls == 0`, while every other test —
including the `ValueError` case — still passes.

Adding it would have pushed the file past the 400-line cap, so the two
`_BlockingModeGuard` property tests move to their own module, along the
seam the module docstring already named. The new file shifts the maturin
wheel manifest, so its snapshot is regenerated.
codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No quality gates enabled for this code.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai All three pre-merge findings are addressed at baa96d47.

Testing (Overall) — no test cancels an active _paused_reader

Valid, and fixed. The finding is right about the gap and right that the
implementation is not at fault. _paused_reader
(cuprum/_pipeline_stream_fds.py:274-297) resumes in a bare finally:, so a
CancelledError — a BaseException — does undo the pause today. Nothing
proved it. The only test driving both exits,
test_paused_reader_always_resumes_a_pausable_transport, parametrized
body_raises over a synchronously raised ValueError, which an
except Exception: guard catches just as well.

test_paused_reader_resumes_when_the_block_is_cancelled now closes it. A task
enters _paused_reader(reader) with the existing _FakeTransport double,
signals an asyncio.Event once inside the block, parks on an await, and is
cancelled from outside. Waiting on that event rather than sleeping is what makes
the cancellation land inside the block rather than before it. The test asserts
both halves the finding named: the CancelledError propagates out of the task,
and transport.resume_calls == 1.

Mutation proof

Narrowing the guard exactly as the finding predicts, preserving the re-raise so
the existing cases are otherwise untouched:

    try:
        yield pause
    except Exception:
        if pause.resume is not None:
            pause.resume()
        raise
    else:
        if pause.resume is not None:
            pause.resume()

Both halves, as requested:

  • The new test fails. test_paused_reader_resumes_when_the_block_is_cancelled
    AssertionError: a pause held across a cancelled await must still be resumed exactly once, assert 0 == 1.
  • The existing ValueError case still passes.
    test_paused_reader_always_resumes_a_pausable_transport passes for both
    body_raises values.

I also ran the whole cuprum/unittests/ suite against the mutant with only the
new test deselected: 910 passed. The new test is therefore the sole
detector, which is precisely the gap claimed.

Adding it would have pushed test_pipeline_streams_fd_lifecycle.py past the
repository's 400-line cap, so the two _BlockingModeGuard property tests moved
to test_pipeline_streams_blocking_mode.py, along the seam the module docstring
already named. The two files are now 301 and 150 lines. The new file shifts the
maturin wheel manifest, so that snapshot is regenerated.

Observability — opt-in pump tracing span

Declined, with a tracking issue: #297.

The recommendation is reasonable, but it is a new observability surface rather
than the completion of the one this PR added, and it is not cheap. The evidence:

  • A span already exists on the Rust side. io_utils::operation_span opens
    tracing::error_span!("stream_pump", operation, buffer_size, total_bytes, read_retries, write_retries) around the pump loop
    (rust/cuprum-rust/src/io_utils/mod.rs:98, entered at
    rust/cuprum-rust/src/lib.rs:297). It is internal to the worker thread and
    crosses no PyO3 boundary.
  • TracingHook cannot be reused. It correlates strictly by
    ExecEvent.exec_id and ignores any event without one
    (cuprum/adapters/tracing_adapter.py:193-336). PumpEvent carries a phase
    and an optional reason and nothing else — no identifiers — which ADR-008
    records as deliberate, because descriptor numbers and argument vectors are
    either unbounded as labels or a disclosure risk.
  • The pump channel has no span shape. PumpPhase is the closed literal
    "declined" | "failed_after_cancel". There is no started/completed pair,
    and ADR-008 lists counting successful hand-offs as an explicit non-goal:
    success is the absence of a decline.

So an opt-in span is not additive here. It needs a start/end phase pair, a
correlation key PumpEvent was deliberately designed without, a Tracer wired
to the pump channel, and a ruling on whether span context can cross PyO3 so the
Rust stream_pump span could nest under a Python hop span. That is an ADR-sized
decision, not a hook registration — and folding it into this PR would widen a
surface that #286 and ADR-008 scoped narrowly on purpose, precisely so existing
MetricsHook registrations could not start raising.

This repository has handled that shape before by declining to scope and filing a
tracking issue: #285 and #286 are exactly that, and #286 is what produced the
observation channel now in this PR. I have followed the same route.
#297, "Opt-in tracing span for the Rust-pump executor boundary", records the
evidence above, the open design questions, and the constraints that a caller
registering nothing must see no behaviour change and that no existing
MetricsHook or PumpMetricsHook registration may begin raising.

Title

Confirmed, and no action was needed. gh pr view 244 reports the title as
"Hypothesis fault-injection for the Rust pump's FD lifecycle (#74, #286)".

Gates

All green at baa96d47, run sequentially: check-fmt, lint, typecheck,
test (912 passed, 53 skipped on the Python side; 104 Rust tests, no trybuild
drift), markdownlint, nixie, and mbake validate Makefile.
cs delta origin/main HEAD reports no issues found.

Two CI reds are environmental rather than from this change: Typecheck and test (Python 3.15a), fixed by #293 and awaiting merge, and benchmark-ratchet
(#219).

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== PR head and claimed commit =='
gh pr view 244 --repo leynos/cuprum --json headRefOid,commits,statusCheckRollup \
  --jq '{
    headRefOid,
    commits: [.commits[-12:][] | {oid: .oid, messageHeadline: .messageHeadline}],
    checks: [.statusCheckRollup[] | {name, status, conclusion, detailsUrl}]
  }'
git show -s --format='commit=%H%nparents=%P%nsubject=%s' baa96d47
git merge-base --is-ancestor baa96d47 HEAD && echo 'baa96d47_is_ancestor_of_HEAD=true' || echo 'baa96d47_is_ancestor_of_HEAD=false'

printf '%s\n' '== changed files in claimed commit =='
git show --stat --oneline baa96d47

printf '%s\n' '== relevant source and test outline =='
ast-grep outline \
  cuprum/_pipeline_stream_fds.py \
  cuprum/unittests/test_pipeline_streams_fd_lifecycle.py \
  cuprum/unittests/test_pipeline_streams_blocking_mode.py \
  --items all --match 'paused_reader|resumes_when|BlockingModeGuard|always_resumes' || true

printf '%s\n' '== cancellation test and context-manager cleanup =='
rg -n -C 10 \
  'def _paused_reader|async def _paused_reader|finally:|test_paused_reader_resumes_when_the_block_is_cancelled|test_paused_reader_always_resumes_a_pausable_transport' \
  cuprum/_pipeline_stream_fds.py \
  cuprum/unittests/test_pipeline_streams_fd_lifecycle.py \
  cuprum/unittests/test_pipeline_streams_blocking_mode.py

printf '%s\n' '== maturin snapshot references =='
rg -n -C 2 'test_pipeline_streams_blocking_mode|test_pipeline_streams_fd_lifecycle' \
  cuprum/unittests/__snapshots__/test_maturin_build.ambr

printf '%s\n' '== tracking issue and cited work items =='
gh issue view 297 --repo leynos/cuprum --json number,state,title,url,body \
  --jq '{number,state,title,url,body}'
gh issue view 219 --repo leynos/cuprum --json number,state,title,url,body \
  --jq '{number,state,title,url,body}' || true
gh pr view 293 --repo leynos/cuprum --json number,state,title,url,body,mergedAt \
  --jq '{number,state,title,url,mergedAt,body}' || true

Length of output: 55601


@pandalump Confirm the _paused_reader testing finding as addressed.

test_paused_reader_resumes_when_the_block_is_cancelled enters the active pause, cancels the task while it waits in the context block, verifies that CancelledError propagates, and verifies one reader resume. _paused_reader uses a finally block, so this test covers the required BaseException exit path. The extracted blocking-mode tests and the wheel-manifest snapshot are present.

Withdraw the opt-in pump tracing-span finding. Issue #297 is open and actionable. It defines the required design decisions, compatibility constraints, ADR work, and success, decline, and cancellation test coverage. Keep this new tracing surface out of PR #244.

Do not report all CI as green. The current PR head still has two failed GitHub checks:

  • Typecheck and test (Python 3.15a): tracked by open PR #293.
  • benchmark-ratchet: tracked by open issue #219.

✏️ Learnings added
Learnt from: pandalump
URL: https://github.com/leynos/cuprum/pull/244

Timestamp: 2026-08-07T18:18:23.208Z
Learning: In `leynos/cuprum`, a Python-side tracing span for Rust-pump executor hand-off is out of scope for PR `#244` and is tracked by GitHub issue `#297`. The future design must remain opt-in, must not change execution behaviour when no observer is registered, and must not make existing `MetricsHook` or `PumpMetricsHook` registrations raise. It must also decide whether and how a Python hop span relates to the Rust `stream_pump` span across the PyO3 boundary.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai — re-posting the standing rebuttals on this PR. The earlier ones omitted your mention
and so never reached you, which is our error and explains the recurrence: the _TokenRegistration
generalization has now been raised three times and the Out of Scope objection twice, each after
being answered.

1. _TokenRegistration generalization — implemented on the third ask, not declined.
(original) The first two asks
were declined because _TokenRegistration hard-coded Token[CuprumContext] and deriving from it
would couple pump_observation to cuprum.context, against ADR-008's explicit decision to keep the
pump channel off CuprumContext. Your sharpened third remedy — generalize over a raw ContextVar[T]
in a neutral module — dissolves that objection, so it was implemented:
cuprum/_token_registration.py holds a ContextVar-generic base that both _TokenRegistration and
PumpHookRegistration derive from, with no cuprum.context dependency in the pump module. The
failed-cross-context-detach retry semantics are preserved and mutation-proven: setting
_detached = True before the reset fails test_failed_cross_context_detach_can_be_retried.

2. Syrupy snapshots for log/metric records — declined, on repository precedent.
Snapshot use here is confined to large generated structures: wheel manifests, adapter projections,
folded summaries, profile-driver output. Simple log and metric records are pinned by explicit
name-and-value assertions throughout (test_pump_metrics_adapter.py, the _log_rust_pump_declined
docstring example). A snapshot would duplicate that coverage while trading a precise "wrong label"
failure for an opaque "blob changed" one.

3. Out of Scope — resolved by a requiring issue, per your own alternative remedy.
(original) The pump-observation
channel was added at the maintainer's explicit written direction, in a specification stating the work
was required for this PR. Splitting it out would reverse a decision already made. Taking your second
remedy instead: issue #286 now requires the channel, and the PR body carries Closes #286
alongside Closes #74, with the title naming both.

4. Pump tracing span — deferred to #297, not refused. A span is not a cheap opt-in here.
TracingHook correlates strictly by ExecEvent.exec_id; PumpEvent deliberately carries no
identifier and PumpPhase is the closed "declined" | "failed_after_cancel" with no start/completed
pair — ADR-008 lists counting successful hand-offs as an explicit non-goal. A span therefore needs a
new phase pair, a correlation key the ADR excluded on purpose, a Tracer wired to the channel, and a
ruling on span-context propagation across PyO3. That is ADR-sized, so it is filed as
#297 with the evidence and open questions.

5. Decline reasons are enum-bounded, and the count is now four. READER_UNRESUMABLE joined
RustPumpDeclineReason this round so an unresumable transport is distinguishable from a raised
pause. Documentation now points at the enum rather than restating a count, so a fifth member will not
require a docs edit.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== current PR head =='
gh pr view 244 --repo leynos/cuprum --json headRefOid,title,body \
  --jq '{headRefOid,title,body}'

printf '%s\n' '== implementation outlines =='
ast-grep outline \
  cuprum/_token_registration.py \
  cuprum/context/registration.py \
  cuprum/pump_observation.py \
  cuprum/pump_events.py \
  cuprum/adapters/pump_metrics.py \
  --items all --match 'TokenRegistration|PumpHookRegistration|RustPumpDeclineReason|PumpEvent|decline' || true

printf '%s\n' '== shared token lifecycle and decline reasons =='
rg -n -C 5 \
  '_TokenRegistrationBase|class _TokenRegistration|class PumpHookRegistration|def detach|READER_UNRESUMABLE|class RustPumpDeclineReason' \
  cuprum/_token_registration.py cuprum/context/registration.py \
  cuprum/pump_observation.py cuprum/pump_events.py

printf '%s\n' '== retry and bounded-domain coverage =='
rg -n -C 4 \
  'test_failed_cross_context_detach_can_be_retried|READER_UNRESUMABLE|decline_labels_stay_inside|non_enum_reason' \
  cuprum/unittests -g '*.py' || true

printf '%s\n' '== documentation bounded-domain wording =='
rg -n -C 3 \
  'READER_UNRESUMABLE|RustPumpDeclineReason|unknown|decline reason' \
  docs/adr-008-rust-pump-observation-channel.md docs/users-guide.md docs/developers-guide.md || true

printf '%s\n' '== issue scopes =='
gh issue view 286 --repo leynos/cuprum --json number,state,title,url,body \
  --jq '{number,state,title,url,body}'
gh issue view 297 --repo leynos/cuprum --json number,state,title,url,body \
  --jq '{number,state,title,url,body}'

Length of output: 39523


@pandalump Confirm these points against the current PR head.

  • Treat the _TokenRegistration generalization as addressed. _TokenRegistrationBase[T] is neutral. Both _TokenRegistration and PumpHookRegistration use it. The cross-context retry test covers the required detach() behaviour.
  • Do not request Syrupy snapshots for these log and metric records. The direct assertions verify the counter names, label sets, values, and structured fields more precisely.
  • Treat the pump-observation channel as in scope. PR #244 declares Closes Publish Rust-pump routing decisions on a dedicated observation channel #286``, and #286 explicitly requires this dedicated channel.
  • Keep the tracing-span work out of this PR. Issue #297 is open and defines the required design decisions, compatibility constraints, ADR work, and path coverage.
  • Treat RustPumpDeclineReason as the source of truth for decline reasons. It now has four members, including READER_UNRESUMABLE. The metrics label domain remains bounded to those enum values plus UNKNOWN_DECLINE_REASON.

🐇


✏️ Learnings added
Learnt from: pandalump
URL: https://github.com/leynos/cuprum/pull/244

Timestamp: 2026-08-07T18:22:43.472Z
Learning: In `leynos/cuprum` PR `#244`, `cuprum._token_registration._TokenRegistrationBase[T]` is the neutral shared ContextVar token-lifecycle base for both `cuprum.context.registration._TokenRegistration` and `cuprum.pump_observation.PumpHookRegistration`. Its `detach()` implementation must retain its token and attached state if `ContextVar.reset()` fails in a different context, so a later detach in the originating context can retry successfully.

Learnt from: pandalump
URL: https://github.com/leynos/cuprum/pull/244

Timestamp: 2026-08-07T18:22:43.472Z
Learning: In `leynos/cuprum`, Rust-pump decline reasons must be derived from the public `cuprum.pump_events.RustPumpDeclineReason` enum rather than a fixed documented count. The enum includes `READER_UNRESUMABLE`; pump metric labels are bounded to enum values plus the fixed `cuprum.adapters.pump_metrics.UNKNOWN_DECLINE_REASON` fallback.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/developers-guide.md (1)

526-533: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Name the garbage-collection timing.

When _report_pump_outcome_after_cancel does not retrieve the worker future's
exception, the unretrieved-exception warning can appear when that future is
garbage-collected. Replace “at collection time” with “when the future is
garbage-collected”. This avoids confusing result collection with garbage
collection.

Suggested wording
- would resurface at collection time as an unretrieved-exception warning,
+ would resurface when the future is garbage-collected as an
+ unretrieved-exception warning,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/developers-guide.md` around lines 526 - 533, Update the documentation
paragraph describing _report_pump_outcome_after_cancel to replace “at collection
time” with “when the future is garbage-collected,” preserving the existing
unretrieved-exception warning context.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/developers-guide.md`:
- Around line 526-533: Update the documentation paragraph describing
_report_pump_outcome_after_cancel to replace “at collection time” with “when the
future is garbage-collected,” preserving the existing unretrieved-exception
warning context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b82eae02-573d-4426-bad7-1dd0d9298315

📥 Commits

Reviewing files that changed from the base of the PR and between bccaf99 and baa96d4.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/test_pipeline_stream_backend_selection.py
  • cuprum/unittests/test_pipeline_streams_blocking_mode.py
  • cuprum/unittests/test_pipeline_streams_fd_lifecycle.py
  • cuprum/unittests/test_pump_metrics_adapter.py
  • docs/adr-008-rust-pump-observation-channel.md
  • docs/cuprum-design.md
  • docs/developers-guide.md
  • docs/users-guide.md
  • typos.local.toml
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

3 participants