fix(vector): propagate cancel in cleanup task; cover 403 + sweep-failure
Address round-1 review on #913 and the SonarCloud new_reliability_rating gate: - credential_cleanup_task no longer catches the cancellation exception (Sonar python:S7497). A task-group cancel must propagate for structured- concurrency teardown; graceful shutdown still flows through shutdown_event, so the sleep no longer needs a cancel/break. - Parametrize the scanner self-heal tests over 401 AND 403 (handled identically at both call sites) and add a test that a failing periodic sweep is logged non-fatally and does not crash the task. - Log the stored-user count before the startup sweep (operability signal), add a debug line when the credential row was already gone, and document the at-most-one extra-401 convergence in _remove_stale_credential. Refs Deck #198. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3790cf6d60
commit
7a9e4a8681
@@ -136,6 +136,11 @@ async def _remove_stale_credential(user_id: str, status_code: int) -> None:
|
||||
#198). Deleting the row drops the user out of
|
||||
``get_all_app_password_user_ids()`` so the scanner is not recreated.
|
||||
|
||||
Convergence: if ``user_manager_task`` already snapshotted the user IDs for
|
||||
the current poll before this deletion, it re-spawns the scanner once more on
|
||||
the next cycle, which fails auth and deletes again — at most one extra 401
|
||||
per manager poll interval, versus the unbounded loop before this fix.
|
||||
|
||||
Best-effort: a storage failure here is logged, not raised — the periodic
|
||||
``credential_cleanup_task`` sweep (and the next startup sweep) are backstops.
|
||||
"""
|
||||
@@ -147,6 +152,14 @@ async def _remove_stale_credential(user_id: str, status_code: int) -> None:
|
||||
user_id,
|
||||
status_code,
|
||||
)
|
||||
else:
|
||||
# Row already gone — raced with the periodic sweep or another
|
||||
# scanner exit. Harmless; logged for diagnostics.
|
||||
logger.debug(
|
||||
"[BasicAuth] No stale app password to remove for %s (HTTP %s)",
|
||||
user_id,
|
||||
status_code,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"[BasicAuth] Failed to remove stale app password for %s: %s", user_id, e
|
||||
@@ -473,12 +486,13 @@ async def credential_cleanup_task(
|
||||
task_status.started()
|
||||
|
||||
while not shutdown_event.is_set():
|
||||
# Sleep first — startup already swept; wake early on shutdown.
|
||||
try:
|
||||
with anyio.move_on_after(CREDENTIAL_CLEANUP_INTERVAL):
|
||||
await shutdown_event.wait()
|
||||
except anyio.get_cancelled_exc_class():
|
||||
break
|
||||
# Sleep first — startup already swept; wake early on shutdown. The
|
||||
# graceful path goes through shutdown_event (move_on_after returns once
|
||||
# teardown sets it), so we deliberately do NOT catch the cancellation
|
||||
# exception: a task-group cancel must propagate for structured-
|
||||
# concurrency teardown (re-swallowing it would breach anyio's contract).
|
||||
with anyio.move_on_after(CREDENTIAL_CLEANUP_INTERVAL):
|
||||
await shutdown_event.wait()
|
||||
if shutdown_event.is_set():
|
||||
break
|
||||
|
||||
|
||||
Reference in New Issue
Block a user