Retrofit fetch-thread-pool + compute-process-pool into _run_stage_c (issue #566) - #669
Merged
Merged
Conversation
…ion, gate inline mode by pytest env - Lockout-drain: switch fetch-future consumption from as_completed() (completion-order, racy) to a submission-order list blocked via .result() - a lockout on card B could otherwise outrace and discard an already-successful card A before its compute was ever submitted. Regression pin: test_lockout_mid_prefetch_drains_the_already_fetched_card_but_starts_no_more. - BrokenProcessPool cascade: _INLINE_COMPUTE_FOR_TESTS is now gated globally by PYTEST_CURRENT_TEST (checked at call time via _inline_compute_active(), not import time), so every test file runs the coordinator logic inline instead of forking a real ProcessPoolExecutor under pytest-django's connection-wrapped test transaction. Removes the now-redundant per-file fixture in test_stream_full_catalog.py. - Also removes the parent-side connection.close() before forking the compute pool - it was breaking this coordinator's own later DB writes (mark_ledger_failed) under pytest. Each compute worker already closes its own inherited connection in its initializer, matching run_image_evidence_cohort.py's _init_worker pattern. - image-cdn/wrangler.toml: IMAGE_FULL_TIER_RATE_LIMITER 150->60 (15/s->6/s), the owner-approved value staying under the 7/s client-side GOOGLE_IMAGE ceiling. Full local suite: 3568 passed, 8 skipped, 0 failed. black/isort/ruff clean. makemigrations --check: no changes detected.
This was referenced Aug 3, 2026
…or under test The inline PYTEST_CURRENT_TEST-gated Stage C compute path (_run_stage_c_phase2_inline / _inline_compute_active / _INLINE_COMPUTE_FOR_TESTS) is gone from stage_e_dispatch.py. Production no longer branches on test detection - the pooled ThreadPoolExecutor + ProcessPoolExecutor loop in _run_stage_c is now the ONLY implementation, and it is what runs under every test. test_stage_e_dispatch.py introduces _SyncStagePoolStub, a drop-in synchronous stand-in for both executors that still invokes the real initializer (needed for short_circuit propagation) and still runs the real coordinator loop (submission-order consumption, the _STAGE_C_POOL_QUEUE_DEPTH backpressure drain, the final as_completed drain) unmodified. test_stage_e_shakedown.py and test_stream_full_catalog.py each reuse _SyncStagePoolStub (imported, not copy-pasted) via their own autouse _sync_stage_c_pools fixture, since both dispatch through the real dispatch_micro_batch -> _run_stage_c path and had no stub of their own once the global pytest-detection gate was removed.
4 tasks
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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
Retrofit fetch-thread-pool + compute-process-pool into
_run_stage_c(issue #566), plus the fixes CI and review surfaced on top of the original design:ThreadPoolExecutor(3)for fetch (I/O-bound, benefits from concurrency)ProcessPoolExecutor(3)for compute (CPU-bound, previously sequential)_STAGE_C_POOL_QUEUE_DEPTH=6(2x workers) to keep RSS flatas_completed()(completion-order, racy) to a submission-order list blocked via.result(). Underas_completed(), a lockout on card B could outrace and silently discard an already-successful card A before its compute was ever submitted, violating the "in-flight work drains, nothing new starts" invariant from issue Retrofit decoupled fetch/compute into streaming Stage C (ratified §4 item 3, unimplemented in Phase 2) #472. The fetch threads still run concurrently — only result CONSUMPTION order is fixed, so there is no throughput loss.PYTEST_CURRENT_TEST, which meant every test took the inline path and the pooled loop that ships was executed by none of them._run_stage_c_phase2_inline,_inline_compute_activeand_INLINE_COMPUTE_FOR_TESTSare now deleted (105 lines removed, no replacement), sostage_e_dispatch.pycontains no test-mode detection at all. Tests instead substitute_SyncStagePoolStubfor the module-levelThreadPoolExecutorandProcessPoolExecutornames, which runs the single production loop under a synchronous pool. That is the techniquetest_run_image_evidence_cohort.pyalready uses via its own_SyncPoolStub.connection.close()before forking the compute pool — it was breaking this coordinator's own later DB writes (mark_ledger_failedon a compute-side crash). Each compute worker closes its own inherited connection copy in_stage_c_compute_worker_init, which is the side that actually needs a fresh connection post-fork.image-cdn/wrangler.toml:IMAGE_FULL_TIER_RATE_LIMITER150to60(15/s to 6/s), the owner-approved value, staying under the 7/s client-sideGOOGLE_IMAGEceiling so the worker limiter remains the binding politeness control.Test coverage, stated honestly: the coordinator loop that ships is now the one tests execute — lockout drain, error propagation, throttle handling, backpressure accounting and both pool shutdowns all run through the production code path. What the stub does not exercise is real process semantics: fork, worker crash recovery, and pool teardown under a genuine
ProcessPoolExecutor._stage_c_compute_worker_inithas direct unit coverage of its own. That residual gap is the same onerun_image_evidence_cohort.pycarries and is closed only by a production run.Closes #566.
Test plan
test_lockout_mid_prefetch_drains_the_already_fetched_card_but_starts_no_more— card A getsImageEvidencepersisted, cards B and C do not, status iscompleted-with-trip. Confirmed to FAIL against the pre-fixas_completed()consumption order across 8PYTHONHASHSEEDvalues and to pass with the fix, so it grades production code rather than a twin._stage_c_compute_worker_init(short-circuit on, default, short-circuit off).cd MPCAutofill && pytest .— the same invocation.github/actions/test-backenduses — run in a local venv (Django 4.2.30, pytest 9.1.1). 3571 passed, 8 skipped, 0 failed in 478.33s.manage.py makemigrations --check --dry-run --skip-checks— no changes detected.docs/upstreaming/extractable-primitives.md: no change —_run_stage_cis fork-entangled catalog pipeline code, not a new or destroyed no-dependency primitive.mergeable_state=clean.