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:
Chris Coutinho
2026-06-16 19:23:46 +02:00
co-authored by Claude Opus 4.8
parent 3790cf6d60
commit 7a9e4a8681
3 changed files with 67 additions and 15 deletions
+20 -6
View File
@@ -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