fix(credential-pool): lock the quarantine read-modify-write of _entries

#71775 moved deferred single-use-token refreshes outside the pool lock
(correct — they hold a cross-process flock plus network I/O). But
_refresh_entry_impl's three terminal-auth-failure quarantine paths do a
bare read-modify-write of self._entries. Those used to run with the
caller holding self._lock; on the deferred path they run unlocked, so a
concurrent mutation between the read and the write is silently lost.

Wrap all three in 'with self._lock' (an RLock, so locked callers
re-enter safely) and correct the _refresh_pending_entries docstring,
which claimed the mutations were already self-locking.

Post-merge gate-sweep finding on the #71775 salvage (#77714).
Sibling to the acquire_lease re-select fix.
This commit is contained in:
kshitijk4poor
2026-08-04 13:14:38 +05:30
committed by kshitij
parent db0bd42119
commit 4075c8fd5a
2 changed files with 187 additions and 36 deletions
+51 -36
View File
@@ -1458,17 +1458,22 @@ class CredentialPool:
logger.debug(
"Failed to clear terminal xAI OAuth state: %s", clear_exc
)
removed_ids = [
item.id for item in self._entries
if item.source == "device_code"
]
self._entries = [
item for item in self._entries
if item.source != "device_code"
]
if self._current_id == entry.id:
self._current_id = None
self._persist(removed_ids=removed_ids)
# Read-modify-write of self._entries: must be atomic.
# This runs on the DEFERRED refresh path (outside the
# pool lock), so take it here. self._lock is an RLock,
# so the still-locked callers re-enter safely.
with self._lock:
removed_ids = [
item.id for item in self._entries
if item.source == "device_code"
]
self._entries = [
item for item in self._entries
if item.source != "device_code"
]
if self._current_id == entry.id:
self._current_id = None
self._persist(removed_ids=removed_ids)
return None
# For openai-codex: same race as xAI/nous — another Hermes process
# may have consumed the refresh token between our proactive sync
@@ -1528,17 +1533,22 @@ class CredentialPool:
logger.debug(
"Failed to clear terminal Codex OAuth state: %s", clear_exc
)
removed_ids = [
item.id for item in self._entries
if item.source == "device_code"
]
self._entries = [
item for item in self._entries
if item.source != "device_code"
]
if self._current_id == entry.id:
self._current_id = None
self._persist(removed_ids=removed_ids)
# Read-modify-write of self._entries: must be atomic.
# This runs on the DEFERRED refresh path (outside the
# pool lock), so take it here. self._lock is an RLock,
# so the still-locked callers re-enter safely.
with self._lock:
removed_ids = [
item.id for item in self._entries
if item.source == "device_code"
]
self._entries = [
item for item in self._entries
if item.source != "device_code"
]
if self._current_id == entry.id:
self._current_id = None
self._persist(removed_ids=removed_ids)
return None
# For nous: another process may have consumed the refresh token
# between our proactive sync and the HTTP call. Re-sync from
@@ -1595,17 +1605,19 @@ class CredentialPool:
auth_mod.NOUS_DEVICE_CODE_SOURCE,
f"manual:{auth_mod.NOUS_DEVICE_CODE_SOURCE}",
}
removed_ids = [
item.id for item in self._entries
if item.source in singleton_sources
]
self._entries = [
item for item in self._entries
if item.source not in singleton_sources
]
if self._current_id == entry.id:
self._current_id = None
self._persist(removed_ids=removed_ids)
# Atomic read-modify-write; see the note above.
with self._lock:
removed_ids = [
item.id for item in self._entries
if item.source in singleton_sources
]
self._entries = [
item for item in self._entries
if item.source not in singleton_sources
]
if self._current_id == entry.id:
self._current_id = None
self._persist(removed_ids=removed_ids)
return None
self._mark_exhausted(entry, None)
return None
@@ -1716,9 +1728,12 @@ class CredentialPool:
On failure the entry is silently skipped.
"""
for entry, sync_fn in pending:
# _refresh_entry already merges the refreshed entry into the
# pool internally (its mutation primitives are self-locking),
# so no second _replace_entry is needed here.
# _refresh_entry merges the refreshed entry into the pool
# internally. Its mutation primitives (_replace_entry, _persist)
# are self-locking, and the quarantine paths inside
# _refresh_entry_impl take self._lock explicitly around their
# read-modify-write of self._entries — required because this
# call site runs OUTSIDE the pool lock.
self._refresh_entry(entry, force=False)
def _available_entries(