feat(player-info): integrate warm pool with buffered execution - #142
Merged
Conversation
- Replace gate._open = False private mutation with gate.signal_rate_limit() public API in test_gate_reopens_only_after_mark_engine_ready - Add explicit test asserting engine.close() fires in finally when claim_loop_fn returns -1 (documents intentional lifecycle ownership)
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.
Scope
PR 6b of the player_info buffered-mode series. Integrates the
WarmBrowserPoolscaffold (PR #138) into the buffered player_info execution path. When bothplayer_info_dispatch_buffer_enabled=Trueandplayer_info_warm_pool_enabled=True, the pool owns the engine lifecycle;CandidateProducerandBoundedCandidateBuffer(PR #141) remain in place. This closes the warm-pool gap deferred from PR #141.Branch
feat/player-info-buffered-warm-pool→mainHead:
18be778Relationship to merged PRs
WarmBrowserPoolscaffold +BrowserSlotEngineProtocol +BackoffPolicy, feature flagoffCandidateBuffer+CandidateProducer, claim-at-handoff semanticsRateLimitGate+ dispatch buffer integration — buffered mode wired, warm pool deferredWarmBrowserPoolinto the buffered execution path whenplayer_info_warm_pool_enabled=TrueFeature flag matrix
dispatch_buffer_enabledwarm_pool_enabledfalserun_direct()independentlytruefalserun_buffered(buffer)independentlytruetrueWarmBrowserPoolowns engine lifecycle per slotBoth flags default to
false. This PR is additive and completely backward-compatible.Default-disabled behavior
With
player_info_warm_pool_enabled=False(default), execution is unchanged from PR #141: buffered workers each own their ownPydollEngineviarun_buffered(). The warm pool code path is never entered.Direct mode preservation
The
if not _sc.player_info_dispatch_buffer_enabled:branch is untouched. Direct mode workers callrun_direct()with no gate, no buffer, and no pool — same as before PR #139.Buffered mode without warm pool
When
dispatch_buffer_enabled=Trueandwarm_pool_enabled=False:_warm_poolisNoneasyncio.gather(*[w.run_buffered(_dispatch_buffer) ...])as in PR feat(player-info): wire dispatch buffer execution gate #141RateLimitGateandCandidateProducerstill function normally_producer.request_stop()+_gate.shutdown()in finallyBuffered mode with warm pool
When both flags are
True:_warm_pool = WarmBrowserPool(size=workers, slot_factory=_make_slot)is createdresults = await _warm_pool.run()replaces theasyncio.gathercall_warm_pool.shutdown()added to finally blockWorkerSlotreceives a freshPydollEngineviaengine_factory, using the matching worker'sprofile_dirandengine_nameclaim_loop_fnwrapsworker._run_buffered_loop(engine, buffer), catchingCooldownRequired→ returns-1(triggers slot task_backoff + engine restart)WarmBrowserPool lifecycle ownership
WarmBrowserPool.run()supervisesNconcurrentWorkerSlottasksengine.start()→engine.warmup()→on_warmup_success()→claim_loop_fn(engine)loop →on_engine_teardown()→engine.close()in finallyon_engine_teardown()fires, slot waitsbrowser_backoff, then restartson_engine_teardown()andengine.close()fire in thefinallyblock; slot then waitstask_backoffand the next iteration creates a fresh engine viaengine_factory()— intentional lifecycle ownershipshutdown()cancels all slot tasksWorker/slot lifecycle
_make_slot(slot_id)is a closure that captures:_w = _worker_instances[slot_id - 1]— worker instance for this slot_disp = shared_display— singleton display bound at slot-creation time (not at call time)engine_factorycreates a newPydollEngineper slot using the worker's profile/name_worker_instancesare still constructed but their internal engine lifecycle is NOT used in warm_pool mode — the pool provides the engine viaclaim_loop_fn.Readiness/warmup behavior
on_warmup_success=_gate.mark_engine_ready— called byWorkerSlotafter a successfulengine.warmup(). This opens theRateLimitGatefor production work.mark_engine_ready(idempotent for an already-open gate).RateLimitGate interaction
on_warmup_success=_gate.mark_engine_ready— reopens gate after any engine finishes warmupon_engine_teardown=_gate.cancel_recovery— cancels stale probe task when engine is torn down, WITHOUT reopening the gate (gate stays CLOSED until a new engine warms up)CooldownRequiredinclaim_fn→ returns-1→ WorkerSlot triggers task_backoff + engine restart. A successful restart warmup callsmark_engine_readyagain.gate.shutdown()in finally cancels any in-flight recovery task on clean process exit.CandidateProducer/BoundedCandidateBuffer interaction
_dispatch_bufferis shared across all slots viaclaim_fnclosure captureCandidateProducerstill runs withwait_fn=gate.wait_if_closed— pauses during rate-limit cooldownbuffer.get()yieldsPlayerInfoRow;claim_by_id()acquires at handoffDB impact
No new queries.
release_to_pending()(introduced in PR #141) still handles the gate-after-claim race. No schema changes.Migration impact
None. No Alembic migrations added or modified.
Changed files
infrastructure/browser/pool.pyon_warmup_successandon_engine_teardownoptional callbacks toWorkerSlotscripts/scrape_player_info.pymain()(three-mode matrix), mode log updatedtests/unit/infrastructure/browser/test_warm_browser_pool.pymake_slothelper extended,TestWorkerSlotCallbacksclass (8 tests),test_run_cancelled_error_propagateshardenedtests/unit/infrastructure/player_info/test_warm_pool_integration.pyTests added/changed
TestWorkerSlotCallbacks(8 new tests intest_warm_browser_pool.py):test_on_warmup_success_called_after_warmup— ordering: warmup → callback → claimtest_on_warmup_success_not_called_on_warmup_failure— suppressed on exceptiontest_on_engine_teardown_called_in_finally— fires even when claim raisestest_on_engine_teardown_called_before_engine_close— ordering: callback → closetest_on_engine_teardown_called_on_browser_failure— fires on engine crashtest_default_none_callbacks_no_error— None is safe (no AttributeError)test_both_callbacks_called_per_engine— per-engine semantics, not per-pooltest_run_cancelled_error_propagatesnow assertsengine.close.assert_awaited_once()test_warm_pool_integration.py(24 tests, new file):TestModeSelection(6): direct mode, buffered without pool, buffered with pool, flag matrix, default behavior, mode log stringTestWarmPoolGateInteraction(8): mark_engine_ready on warmup, cancel_recovery on teardown, gate stays closed after teardown, CooldownRequired → -1, multiple engines gate semantics, shutdown wires gate, gate open via warmup, gate stays closed without warmupTestDispatchWarmPoolInteraction(6): buffer shared across slots, claim_fn passes buffer, producer runs with pool, producer stop on exit, pool shutdown in finally, buffer get semanticsTestSecurityInvariants(4): no sensitive data in claim_fn log, engine name not logged verbosely, profile_dir not logged, gate state not exposed in logsUnit test evidence
Quality gates
Security notes
_disp = shared_displaybinding makes singleton capture explicit — no accidental scope leakgate.pyandpool.pyhave noconfigorpersistenceimports (enforced by lint-imports)CooldownRequiredcaught in claim_fn — exception message never logged; only converted to return code-1Unresolved risks
_worker_instancesare fully constructed even in warm_pool mode but their internal engine lifecycle is not used — minor overhead, not a correctness issuerecover_failed()(pre-existing) missinglocked_at = NULLin recovery SQL — not in scopeScrapeStatus.valuevs Postgres enum label mismatch (pre-existing) — not in scopePR 7 status
PR 7 has not been started.
Merge status
This PR has not been merged and has not been configured for auto-merge.