BlobServiceClient pool — finalizer + idle TTL¶
Motivation¶
_BLOB_SERVICE_POOL keyed entries by (id(credential), account_name).
That worked while the credential lived but had two latent risks:
id()is recycled after Python GC, so if a credential died while its pool entry was still cached and a brand-new credential was assigned the same id, the next_blob_service(new_cred, …)would return the dead credential's BlobServiceClient — silently authenticating with a stale token cache.- Clients held forever. With one account in production the pool never
evicted, so a transient credential reset that did NOT call
reset_blob_service_pool()would leak that client + its HTTP pool.
User-facing change¶
None. The pool still services the steady-state hot path with a cached
BlobServiceClient; behaviour change is invisible unless a credential is
collected (then its clients are evicted before the id can be reused) or
the new prune_idle_blob_service_clients() helper is called.
API / IaC diff¶
api/services/storage_data.py- Each pool entry now stores
(BlobServiceClient, last_used_monotonic)._blob_serviceupdateslast_usedon every hit so an idle sweep can prune accurately. _ensure_credential_eviction(credential)registers aweakref.finalizecallback that evicts every pooled client matching that credential's id the moment the credential is GC'd. A_BLOB_SERVICE_CREDENTIAL_FINALIZEDset guards against double registration so the per-cred overhead is one weakref total.prune_idle_blob_service_clients(*, idle_ttl_seconds)(default 1800 s, overridable viaBLOB_SERVICE_POOL_IDLE_TTL_SECONDS) closes pooled clients whoselast_usedis older than the TTL.reset_blob_service_poolupdated to also clear_BLOB_SERVICE_CREDENTIAL_FINALIZEDso the next access re-registers the finalizer against the current credential.
Validation¶
uv run pytest -q api/tests/test_storage_data.py api/tests/test_db_sharding.py api/tests/test_blast_oracles.py— 84 passed (the existing per-credential / per-account pool semantics tests cover the new key shape).uv run ruff check api/services/storage_data.py— clean.