Skip to content

Commit 355d098

Browse files
committed
fix(engine): reap stale warm spare when desired fingerprint changes
When idle TTL is disabled, a ready spare whose fingerprint no longer matches _desired blocked refill indefinitely. Reap on mismatch before refill.
1 parent a5bfca3 commit 355d098

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

app/engine/warm_pool.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,15 +367,30 @@ def _maybe_reap_ttl(self) -> None:
367367
def _schedule_deferred_refill(self, not_before: float) -> None:
368368
self._next_refill_at = not_before
369369

370+
def _maybe_reap_stale_spare(self, desired: DriverFingerprint) -> None:
371+
"""Requires ``_lock`` held. Drop ready spare when it no longer matches desired."""
372+
spare = self._spare
373+
if spare is None or spare.fingerprint == desired:
374+
return
375+
logger.info(
376+
"warm_pool_reap reason=stale_fingerprint fingerprint_hash=%s",
377+
spare.fingerprint.fingerprint_hash[:12],
378+
)
379+
self._close_spare_unlocked(spare, reason="stale_fingerprint")
380+
self._spare = None
381+
370382
def _maybe_refill(self) -> None:
371383
with self._lock:
372384
if self._stop.is_set():
373385
return
374-
if self._spare is not None or self._building:
386+
if self._building:
375387
return
376388
desired = self._desired
377389
if desired is None:
378390
return
391+
self._maybe_reap_stale_spare(desired)
392+
if self._spare is not None:
393+
return
379394
if self._headless_only and not desired.headless:
380395
return
381396
now = self._clock()

tests/engine/test_warm_pool.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,23 @@ def test_take_miss_on_fingerprint_mismatch(self) -> None:
153153
self.assertTrue(pool.live_spare_dirs())
154154
pool.shutdown()
155155

156+
def test_stale_spare_reaped_when_desired_fingerprint_changes(self) -> None:
157+
pool = self._pool(idle_ttl_seconds=0)
158+
fp_a = _fp(user_agent="A")
159+
fp_b = _fp(user_agent="B")
160+
pool.notify_scrape_finished(fp_a)
161+
self._wait_spare(pool)
162+
stale_driver = self.built[0]
163+
self.spare_ready.clear()
164+
165+
pool.notify_scrape_finished(fp_b)
166+
self._wait_spare(pool)
167+
168+
self.assertTrue(stale_driver.closed)
169+
self.assertEqual(len(self.built), 2)
170+
self.assertIsNotNone(pool.take(fp_b))
171+
pool.shutdown()
172+
156173
def test_unhealthy_spare_is_reaped_not_handed_out(self) -> None:
157174
pool = self._pool(health_check=HealthCheckStub(healthy=False))
158175
fp = _fp()

0 commit comments

Comments
 (0)