Skip to content

fix: close partially consumed Python LLM streams#465

Open
willkill07 wants to merge 1 commit into
NVIDIA:release/0.6from
willkill07:wkk_relay-496-stream-close
Open

fix: close partially consumed Python LLM streams#465
willkill07 wants to merge 1 commit into
NVIDIA:release/0.6from
willkill07:wkk_relay-496-stream-close

Conversation

@willkill07

@willkill07 willkill07 commented Jul 17, 2026

Copy link
Copy Markdown
Member

Overview

Add explicit closure for partially consumed Python LLM streams so producer tasks do not remain blocked.

  • I confirm this contribution is my own work, or I have the right to submit it under this project's license.
  • I searched existing issues and open pull requests, and this does not duplicate existing work.

Details

  • Add an idempotent LlmStream.aclose() API and cancellation propagation through the native stream bridge.
  • Cancel and close the underlying Python async generator when a consumer stops early.
  • Replace the unsafe receiver borrow with shared owned state.
  • Add regression and coverage tests for partial stream consumption and closure.

Where should the reviewer start?

Start with crates/python/src/py_callable.rs, which owns the async-generator bridge and cancellation path; then review python/tests/test_llm.py for the regression scenario.

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

  • Relates to: RELAY-496

Summary by CodeRabbit

  • New Features

    • Added aclose() support for LLM streaming results, allowing partially consumed streams to be stopped and resources released.
    • Streams now cancel promptly when closed or no longer consumed.
  • Documentation

    • Updated streaming guidance to recommend calling aclose() when stopping before completion.
  • Tests

    • Added coverage verifying stream cancellation, cleanup, and completion behavior.

Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07
willkill07 requested a review from a team as a code owner July 17, 2026 14:34
@github-actions github-actions Bot added size:M PR is medium Bug issue describes bug; PR fixes bug lang:python PR changes/introduces Python code lang:rust PR changes/introduces Rust code labels Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Streaming LLM plumbing now supports cooperative cancellation and explicit completion signaling across Rust and Python async iterators. LlmStream.aclose() cancels producers, waits for closure, and is covered by lifecycle tests and updated API documentation.

Changes

Streaming lifecycle coordination

Layer / File(s) Summary
Python async-iterator forwarding
crates/python/src/py_callable.rs, crates/python/tests/coverage/py_callable_coverage_tests.rs
Python awaitables are scheduled on asyncio, forwarded through cancellation-aware tasks, and closed through aclose when cancellation occurs.
Native stream cancellation and closure
crates/python/src/py_api/mod.rs, crates/python/src/py_callable.rs, crates/python/src/py_types/core.rs
Stream forwarding responds to cancellation while receiving or sending, publishes closed state, and exposes PyLlmStream.aclose() with shared receiver ownership.
Public API and lifecycle validation
python/nemo_relay/_native.pyi, python/nemo_relay/llm.py, python/tests/test_llm.py, crates/python/tests/coverage/*
The aclose() API and early-termination behavior are documented, typed, and tested across normal, error, completion, and dropped-receiver paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PythonConsumer
  participant PyLlmStream
  participant forward_stream_to_channel
  participant AsyncIterator
  PythonConsumer->>PyLlmStream: consume stream items
  PyLlmStream->>forward_stream_to_channel: receive forwarded items
  forward_stream_to_channel->>AsyncIterator: await next item
  PythonConsumer->>PyLlmStream: call aclose
  PyLlmStream->>forward_stream_to_channel: signal cancellation
  forward_stream_to_channel->>AsyncIterator: call aclose
  forward_stream_to_channel->>PyLlmStream: signal closed
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits format and accurately summarizes the main change: closing partially consumed Python LLM streams.
Description check ✅ Passed The description includes the required Overview, Details, review-start guidance, checklist items, and a related-issue reference.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
crates/python/src/py_callable.rs (1)

228-235: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Ensure receiver shutdown and errors finalize the Python iterator.

  • crates/python/src/py_callable.rs#L228-L235: race error delivery against cancellation and call close_async_iter before every terminal exit.
  • crates/python/tests/coverage/py_callable_coverage_tests.rs#L387-L399: use an observable aclose() implementation and assert receiver shutdown invokes it.
🤖 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 `@crates/python/src/py_callable.rs` around lines 228 - 235, Update the receiver
loop in py_callable.rs around the terminal branches to race error delivery
against cancellation, call close_async_iter before every terminal exit including
receiver shutdown and send failures, and preserve the existing break behavior.
In crates/python/tests/coverage/py_callable_coverage_tests.rs lines 387-399,
replace the iterator’s no-op close behavior with an observable aclose()
implementation and assert that receiver shutdown invokes it.

Source: Path instructions

🤖 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 `@crates/python/tests/coverage/py_callable_coverage_tests.rs`:
- Around line 387-399: Update the test around forward_async_iter to give the
Python iterator an observable aclose() method, then assert that aclose() is
invoked after the receiver is dropped. Keep the existing dropped-receiver setup
and await behavior, but verify the iterator cleanup side effect rather than only
confirming that forwarding returns.

---

Outside diff comments:
In `@crates/python/src/py_callable.rs`:
- Around line 228-235: Update the receiver loop in py_callable.rs around the
terminal branches to race error delivery against cancellation, call
close_async_iter before every terminal exit including receiver shutdown and send
failures, and preserve the existing break behavior. In
crates/python/tests/coverage/py_callable_coverage_tests.rs lines 387-399,
replace the iterator’s no-op close behavior with an observable aclose()
implementation and assert that receiver shutdown invokes it.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: d9a757f0-b569-48c9-a917-277eb89e1846

📥 Commits

Reviewing files that changed from the base of the PR and between 0325ba2 and cc37e98.

📒 Files selected for processing (10)
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_callable.rs
  • crates/python/src/py_types/core.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • python/nemo_relay/_native.pyi
  • python/nemo_relay/llm.py
  • python/tests/test_llm.py
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Check / Run
  • GitHub Check: Preview docs
🧰 Additional context used
📓 Path-based instructions (18)
{crates/python/src/py_api/mod.rs,python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go,crates/node/src/api/**/*.rs}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update the language-native bindings for every exposed surface in Python, Go, and Node.js.

Files:

  • python/nemo_relay/_native.pyi
  • python/nemo_relay/llm.py
  • crates/python/src/py_api/mod.rs
{python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update language wrapper helpers such as Python wrapper modules, Python type stubs, and Go shorthand packages when the new behavior belongs in those helper layers.

Files:

  • python/nemo_relay/_native.pyi
  • python/nemo_relay/llm.py
**/*

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

Files:

  • python/nemo_relay/_native.pyi
  • crates/python/tests/coverage/coverage_tests.rs
  • python/nemo_relay/llm.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • python/tests/test_llm.py
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
python/nemo_relay/**/*

⚙️ CodeRabbit configuration file

python/nemo_relay/**/*: Review Python wrapper changes for typed API consistency, contextvars-based scope isolation, async behavior, and parity with the native extension.
Stubs and runtime implementations should stay aligned.

Files:

  • python/nemo_relay/_native.pyi
  • python/nemo_relay/llm.py
**/*.rs

📄 CodeRabbit inference engine (.agents/skills/prepare-pr/SKILL.md)

**/*.rs: Any Rust change must run just test-rust
Any Rust change must run cargo fmt --all
Any Rust change must run cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all for all FFI work since it is Rust work
Run just test-rust to validate FFI changes
Run cargo clippy --workspace --all-targets -- -D warnings to enforce strict linting on FFI work

When Rust files changed as part of Go work, also run cargo fmt --all, just test-rust, and cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all when Rust files are changed as part of Node work
Run cargo clippy --workspace --all-targets -- -D warnings when Rust files are changed as part of Node work
Run just test-rust when Rust files are changed as part of Node work

When changing the core Rust runtime or Rust-facing API surface, format Rust code with cargo fmt (rustfmt defaults), keep cargo clippy -- -D warnings clean, and satisfy cargo deny check per deny.toml.

**/*.rs: If any Rust code changed, always run just test-rust.
If any Rust code changed, also run cargo fmt --all.
If any Rust code changed, also run cargo clippy --workspace --all-targets -- -D warnings.
For Rust changes headed for review, run cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings even if relying on pre-commit.

Files:

  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Follow binding naming conventions in Rust and Python: use snake_case.

Files:

  • crates/python/tests/coverage/coverage_tests.rs
  • python/nemo_relay/llm.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • python/tests/test_llm.py
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
**/*.{rs,py,js,mjs,cjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{rs,py,js,mjs,cjs,ts,tsx}: Use Json = serde_json::Value in Rust-facing runtime APIs where the existing code expects JSON payloads.
Use Result<T> with FlowError in core runtime paths, and keep errors explicit and binding-appropriate at the wrapper layer.
Keep async behavior on the existing tokio-based model; bindings should preserve callback and future lifetimes rather than blocking or hiding async work unexpectedly.

Files:

  • crates/python/tests/coverage/coverage_tests.rs
  • python/nemo_relay/llm.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • python/tests/test_llm.py
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
**/*.{rs,py,go,js,ts,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use language-appropriate naming conventions: Rust snake_case, C FFI exports prefixed nemo_relay_, Go PascalCase, Node.js camelCase, and Python snake_case.

Files:

  • crates/python/tests/coverage/coverage_tests.rs
  • python/nemo_relay/llm.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • python/tests/test_llm.py
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
**/*.{rs,go,js,ts}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Add the SPDX license header to all Rust, Go, JavaScript, and TypeScript source files using the corresponding // comment form.

Files:

  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
**/*.{rs,py,go,js,ts}

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

If a language surface changed, always run that language's test target even when Rust core did not change.

Files:

  • crates/python/tests/coverage/coverage_tests.rs
  • python/nemo_relay/llm.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • python/tests/test_llm.py
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
**/*.{rs,py,js,ts,tsx,go,java,kt,swift}

📄 CodeRabbit inference engine (.agents/skills/add-middleware/SKILL.md)

Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.

Files:

  • crates/python/tests/coverage/coverage_tests.rs
  • python/nemo_relay/llm.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • python/tests/test_llm.py
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
crates/{python,ffi,node}/**/*

⚙️ CodeRabbit configuration file

crates/{python,ffi,node}/**/*: Treat binding changes as public API changes. Check for parity with the other language bindings, FFI ownership/lifetime safety,
callback error propagation, stable type conversion, and consistent async/stream semantics.
Flag changes that update one binding without corresponding tests or documentation for the same surface elsewhere.

Files:

  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}

⚙️ CodeRabbit configuration file

{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}: Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant.
Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests.

Files:

  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_api_coverage_tests.rs
  • crates/python/tests/coverage/py_types_coverage_tests.rs
  • python/tests/test_llm.py
python/nemo_relay/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Python wrapper modules live under python/nemo_relay/, and the native extension is built from crates/python with maturin.

Files:

  • python/nemo_relay/llm.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: When changing the Python wrapper package, tests, or docs tooling, lint with Ruff (E, F, W, I), format with Ruff formatter (120-character lines, double quotes), and pass ty type checking.
Add the SPDX license header to all Python source files using the # comment form.

Files:

  • python/nemo_relay/llm.py
  • python/tests/test_llm.py
{crates/**/src/**/*.rs,python/**/*.py}

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Do not add tests under src; Rust tests belong in crate tests/ trees, and Python SDK tests belong under python/tests.

Files:

  • python/nemo_relay/llm.py
  • python/tests/test_llm.py
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_types/core.rs
  • crates/python/src/py_callable.rs
**/*.{py,go,js,ts}

📄 CodeRabbit inference engine (.agents/skills/maintain-observability/SKILL.md)

Keep Python, Go, and Node.js config objects and subscriber/exporter methods aligned so all bindings expose the same logical knobs and semantics.

Files:

  • python/nemo_relay/llm.py
  • python/tests/test_llm.py
python/tests/**/*.py

📄 CodeRabbit inference engine (.agents/skills/test-python-binding/SKILL.md)

python/tests/**/*.py: Pytest is used to run tests.
Do not add @pytest.mark.asyncio to any test; async tests are automatically detected and run by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, do not define a new class; use unittest.mock.MagicMock or unittest.mock.AsyncMock, with the spec constructor argument when necessary.
Name mocked classes with the mock prefix, not fake.
Prefer pytest fixtures over helper methods.
Do not repeat fixtures; if a fixture is needed in multiple test files, place it in a conftest.py file.
When creating a fixture, use @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and define the fixture function as def <fixture_name>_fixture() -> <return_type>:; only specify scope when it is not function.
Prefer pytest.mark.parametrize over creating individual tests for different input types.

Files:

  • python/tests/test_llm.py
🪛 Ruff (0.15.21)
python/tests/test_llm.py

[warning] 520-520: Missing return type annotation for private function stream_func

(ANN202)


[warning] 520-520: Unused function argument: request

(ARG001)


[warning] 531-531: Unused lambda argument: chunk

(ARG005)


[warning] 532-532: Prefer dict over useless lambda

Replace with lambda with dict

(PIE807)

🔇 Additional comments (10)
crates/python/tests/coverage/coverage_tests.rs (1)

223-226: LGTM!

crates/python/tests/coverage/py_api_coverage_tests.rs (1)

822-825: LGTM!

crates/python/tests/coverage/py_types_coverage_tests.rs (1)

789-796: LGTM!

Also applies to: 826-833, 851-858

python/nemo_relay/_native.pyi (1)

936-938: LGTM!

python/nemo_relay/llm.py (1)

317-319: LGTM!

python/tests/test_llm.py (1)

6-6: LGTM!

Also applies to: 516-540

crates/python/src/py_callable.rs (1)

26-26: LGTM!

Also applies to: 35-38, 126-227, 241-259, 270-278, 474-486

crates/python/tests/coverage/py_callable_coverage_tests.rs (1)

357-385: LGTM!

crates/python/src/py_api/mod.rs (1)

103-126: LGTM!

Also applies to: 882-894

crates/python/src/py_types/core.rs (1)

4-5: LGTM!

Also applies to: 28-31, 40-80

Comment on lines 387 to 399
let (tx, rx) = tokio::sync::mpsc::channel(1);
drop(rx);
let (_cancel, cancel_rx) = tokio::sync::watch::channel(false);
forward_async_iter(
Arc::new(Python::attach(|py| {
value_iter_cls
.call1(py, (dropped_payload.bind(py),))
.unwrap()
})),
tx,
cancel_rx,
)
.await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Assert that dropping the receiver closes the iterator.

This test only verifies that forwarding returns, so it misses the cleanup regression at crates/python/src/py_callable.rs Lines 228-235. Give the iterator an observable aclose() and assert it was invoked.

As per path instructions, “Tests should cover the behavior promised by the changed API surface, including error paths.”

🤖 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 `@crates/python/tests/coverage/py_callable_coverage_tests.rs` around lines 387
- 399, Update the test around forward_async_iter to give the Python iterator an
observable aclose() method, then assert that aclose() is invoked after the
receiver is dropped. Keep the existing dropped-receiver setup and await
behavior, but verify the iterator cleanup side effect rather than only
confirming that forwarding returns.

Source: Path instructions

@github-actions

Copy link
Copy Markdown

@willkill07 willkill07 self-assigned this Jul 17, 2026
@willkill07 willkill07 added this to the 0.6 milestone Jul 17, 2026
Comment on lines +63 to +76
pyo3_async_runtimes::tokio::future_into_py(py, async move {
cancel.send_replace(true);
while !*closed.borrow() {
closed.changed().await.map_err(|_| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
"stream close task ended before releasing the native stream",
)
})?;
}
Ok(())
})
}
}

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.

Nice cleanup. This closes and drains buffered chunks before aclose() returns, so the next read stops consistently.

Suggested change
pyo3_async_runtimes::tokio::future_into_py(py, async move {
cancel.send_replace(true);
while !*closed.borrow() {
closed.changed().await.map_err(|_| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
"stream close task ended before releasing the native stream",
)
})?;
}
Ok(())
})
}
}
let cancel = self.cancel.clone();
let mut closed = self.closed.clone();
let receiver = Arc::clone(&self.receiver);
pyo3_async_runtimes::tokio::future_into_py(py, async move {
cancel.send_replace(true);
while !*closed.borrow() {
closed.changed().await.map_err(|_| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
"stream close task ended before releasing the native stream",
)
})?;
}
let mut receiver = receiver.lock().await;
receiver.close();
while receiver.try_recv().is_ok() {}
Ok(())
})

@ericevans-nv ericevans-nv 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.

The new stream-close API is a valuable improvement, but the closure acknowledgment is currently nondeterministic. Repeating the new regression test caused stream.aclose() to return while the underlying generator’s finally block had not completed; run 18 of a 20-run loop timed out waiting for cleanup.

Please make aclose() wait for the full downstream Python-generator cleanup—not only the outer forwarding task’s cancellation—and ensure cleanup failures can be represented appropriately. Once that lifecycle is deterministic, the existing regression should pass consistently under repetition.

}
}
}
closed.send_replace(true);

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.

aclose() waits for this closed flag, but it is published before the owned stream is dropped and before downstream Python-generator cleanup is acknowledged. Repeating the new regression test caused aclose() to return while the generator’s finally block never completed within the timeout. Could the completion signal represent the full underlying iterator cleanup before aclose() returns?

@ericevans-nv ericevans-nv 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.

Approved. The intermittent close-acknowledgment behavior identified in review is a non-critical reliability concern and can be handled as follow-up work.

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

Labels

Bug issue describes bug; PR fixes bug lang:python PR changes/introduces Python code lang:rust PR changes/introduces Rust code size:M PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants