test(indexing): pin _active_runs cancellation contract (refs #640)#648
Merged
test(indexing): pin _active_runs cancellation contract (refs #640)#648
Conversation
Issue #640 hypothesizes that ``IndexEngine._active_runs`` leaks when an SSE ``StreamingResponse`` consumer disconnects mid-stream — the path isn't covered by the PR #609 tests, which only exercise explicit ``aclose()`` and exhausted async-for. Adds two regression tests in ``TestActiveRunsCounter``: - ``test_decrements_on_task_cancellation`` — cancels a consumer task iterating ``index_path_stream`` directly. Confirms Python's async-gen stack-unwind on ``CancelledError`` runs the ``finally`` block that decrements the counter. - ``test_decrements_through_generator_wrapper_on_cancel`` — cancels a consumer task iterating a wrapper async generator that mirrors the SSE route's ``_generate()`` shape (catches ``Exception`` but not ``BaseException``). Pins that the stacked-generator path is also clean — the production code shape behind #640. Both tests pass against current ``main``: the engine-layer hypothesis isn't reproduced. They're landed regardless to keep the cancellation contract pinned, so a future refactor that breaks unwind cleanup (e.g., switching ``try/finally`` to ``ExitStack`` or moving ``_active_runs`` mutation outside the engine) fails visibly. Follow-up investigation of #640 needs to look elsewhere — likely watcher / client-state — and the issue will be updated with that finding separately. Co-Authored-By: Claude <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TestActiveRunsCountercovering taskcancellation of
index_path_stream— the path Investigation:/api/indexing/activeendpoint reports active indefinitely after watcher / SSE indexing #640 hypothesizes asthe source of the stuck
/api/indexing/activeindicator.main: the engine-layercancellation hypothesis from Investigation:
/api/indexing/activeendpoint reports active indefinitely after watcher / SSE indexing #640 doesn't reproduce. They're landedto pin the contract so a future refactor that breaks unwind cleanup
fails visibly.
Why
PR #609's existing tests only exercise:
index_path/index_path_streamcounter inc/decaclose()on the stream generatorThe task-cancellation path — what really happens when a Starlette
StreamingResponseconsumer disconnects mid-stream — is not exercised.That's the gap #640 points at. Adding coverage:
test_decrements_on_task_cancellationcancels a task iteratingindex_path_streamdirectly. ConfirmsCancelledErrorpropagationthrough async-gen stack-unwind runs the
finallyblock.test_decrements_through_generator_wrapper_on_cancelmirrors theSSE route's
_generate()wrapper shape (catchesException,re-yields error, lets
BaseExceptionpropagate). Confirms thestacked-generator path is also clean.
Status of #640
Hypothesis tested, not reproduced. Will follow up on #640 with a
comment proposing narrower investigation paths (watcher concurrency,
client-side state machine, hot-reload engine swap).
Test plan
uv run pytest packages/memtomem/tests/test_indexing_engine.py::TestActiveRunsCounter -xvs— 8/8 passuv run pytest packages/memtomem/tests/test_indexing_engine.py -m "not ollama"— 133/133 passuv run pytest packages/memtomem/tests/test_web_routes.py::TestIndexingActive— 3/3 passuv run ruff check packages/memtomem/tests/test_indexing_engine.pyuv run ruff format --check packages/memtomem/tests/test_indexing_engine.py🤖 Generated with Claude Code