Backup pipeline robustness follow-up: event-loop blocking, task refs, argv#180
Open
ClaydeCode wants to merge 2 commits into
Open
Backup pipeline robustness follow-up: event-loop blocking, task refs, argv#180ClaydeCode wants to merge 2 commits into
ClaydeCode wants to merge 2 commits into
Conversation
… as list Follow-up to a4a80bc (issue #127). Four robustness fixes in shard_core/service/backup.py, each with tests in tests/test_backup.py: 1. Offload the Azure marker-blob upload to a worker thread. `_write_marker_blob` called the sync `BlobClient.upload_blob` directly on the event loop; a slow Azure call stalled the whole loop (including the auth path). Now awaits `asyncio.to_thread(...)`. The function becomes async and its caller awaits it. 2. Run `rclone obscure` via `asyncio.create_subprocess_exec` instead of the blocking `subprocess.run`, so it no longer stalls the loop. Also check the exit code and raise BackupFailedError on failure — previously a failed obscure silently returned an empty crypt-password (broken encryption). 3. Hold strong references to fire-and-forget tasks. asyncio holds tasks weakly, so a long backup task (and the unreferenced on_task_done wrapper) could be garbage-collected mid-run. Add a module-level `background_tasks` set with a discard callback, mirroring app_lifecycle.py. 4. Build the rclone argv as a list instead of formatting a string and calling `command.split()`, which would break on any path or SAS URL containing whitespace. Replaces the COMMAND_TEMPLATE / CLEARTEXT_COMMAND_TEMPLATE strings with `_build_backup_command` / `_build_cleartext_backup_command`; the #117 unsupported-flag guard test now runs against both argv builders. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…e signal Test-adversary panel found two blocking gaps and cheap advisories: - Strong-ref test only proved the main backup task was registered, not the spawned on_task_done wrapper (reverting its registration stayed green). Now spies on _register_background_task and asserts both tasks are registered. - The backup-outcome signal was entirely unasserted. Add tests that the success path sends on_backup_update() and the failure path sends it with the exception — the path that tells the UI a backup failed. - Assert the built argv list reaches create_subprocess_exec unsplit (the actual call site of the whitespace fix). - Exercise the Path->str conversion (flat-list test now passes a Path) and the marker-blob URL surgery + payload forwarding; cover an error raised inside the upload thread, not just at client setup. - Rename the done-callback lambda param to stop shadowing the outer task. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to a4a80bc, closes #127. Four robustness fixes in
shard_core/service/backup.py, each with tests intests/test_backup.py. This file has bitten repeatedly (#52, #58, #74, #106 → #117/#119), so it's treated with tests-first care.Changes
_write_marker_blobcalled the syncBlobClient.upload_blobdirectly on the loop; a slow Azure call stalled everything, including the auth path. Nowawait asyncio.to_thread(...); the function is async and its caller awaits it.rclone obscure. Replaced blockingsubprocess.runwithasyncio.create_subprocess_exec. Also added an exit-code check that raisesBackupFailedError— previously a failed obscure silently returned an empty crypt-password (broken encryption). (Small deliberate hardening beyond the literal ask, consistent with the "robustness follow-up" theme; called out for review.)on_task_donewrapper) could be GC'd mid-run. Added a module-levelbackground_tasksset with a discard callback, mirroringapp_lifecycle.py.command.split(), which broke on any path or SAS URL containing whitespace. Replaces theCOMMAND_TEMPLATE/CLEARTEXT_COMMAND_TEMPLATEstrings with_build_backup_command/_build_cleartext_backup_command; the v18: all backups fail — unknown rclone flag --azureblob-no-check-container (release blocker) #117 unsupported-flag guard now runs against both argv builders.Verification
tests/test_backup.py: 15 passed (8 new/strengthened). New tests teeth-checked by breaking the code under them and confirming they fail.233 passed, 1 failed. The single failure (test_app_lifecycle.py::test_app_starts_and_stops) is environmental — the dev VM's ownclayde-traefik-1permanently binds host port 80, which thequick_stopmock app (ports: 80:80) collides with (Bind for :::80 failed: port is already allocated). Unrelated to this diff; it passes on a clean CI runner.ruff check .: clean.Review panel
Four adversarial reviewers ran (diff + issue only): adversarial correctness, test adversary, DevEx/readability, security (SAS URL + passphrase are passed as subprocess argv).
Blocking findings (both from the test adversary — fixed in 63d7200):
on_task_donewrapper → now spies on_register_background_taskand asserts both tasks register.Advisory, addressed: assert unsplit argv reaches
create_subprocess_exec; exercisePath→strand the marker URL surgery / payload forwarding + an in-thread upload error; rename the done-callback lambda param to stop shadowingtask.Advisory, dismissed with reason:
rclone obscuredoesn't echo its argv, and the passphrase is machine-generated diceware. Matches the existing_backup_directorystderr handling; changing both sites is out of scope._build_cleartext_backup_commandis dead in production (correctness/DevEx). Pre-existing (the oldCLEARTEXT_COMMAND_TEMPLATEwas likewise test-only); kept so the v18: all backups fail — unknown rclone flag --azureblob-no-check-container (release blocker) #117 flag guard covers both argv paths. Not deleting pre-existing dead code in this PR.🤖 Generated with Claude Code