fix: stop downloads when destination is full (#116) - #150
Conversation
LeyckerS
left a comment
There was a problem hiding this comment.
Read the diff against #116's acceptance criteria rather than the description, and all five hold:
- errno-based detection —
moon_download.py,except OSError as e: if e.errno == errno.ENOSPC:. Not a string match on the message, which is what the issue asked for. - The run aborts —
RunFatalControlholds the firstDiskFullErrorunder a lock and both_do_dland_browser_workercheckfatal_control.is_set()before taking new work. Keeping this separate from the stall-kill state was the right call; overloadingkill_evtwould have made a full disk look like a stall and re-extract. - The log names the folder —
Disk full in {folder}: need {n:,} bytes to continue, plus the run-level summary line. .tmppreserved — confirmed byos.path.getsize(tmp)being read back to compute the shortfall; nothing unlinks it on this path.- Regression test — thirteen of them.
Three things I want to call out because they are better than what the issue asked for:
_do_dl looks like a 60-line deletion in the diff, but it is the existing body re-indented into try: so that self._inc("_dls",-1) can move to a finally:. It was previously repeated on three exit paths — and this PR adds a fourth, which is exactly the situation where that pattern leaks the counter. Good catch.
The memoryview write loop is not padding either. write() can return a short count on a nearly-full disk instead of raising, and without the loop that silently truncates the file. test_short_write_then_enospc_reports_actual_remaining_bytes is the test that earns it.
Mapping "aborted" to "queue" in _FILE_UI_STATE rather than to "fail" is the detail I would have argued for. #116's actual complaint was that a full disk got reported as an ordinary per-file failure; marking each file fail would have reproduced the bug in the UI while fixing it in the engine.
One sequencing note, not a change request: #149 rewrites the same moon_engine.py region (lines ~175–200) for #65. Merging this first means that PR needs a rebase — that is on me to coordinate, not on you.
Merging.
|
@shard872 — this is approved and ready to go on my side: CI is green on all eight checks, The only thing stopping the merge is that the PR is still marked draft, which I cannot flip for you. The body reads as finished work, so I assume it was an oversight — click "Ready for review" and I will squash it straight away. If it is not an oversight and something is still outstanding, say so and I will hold. No rush either way. |
|
Correcting myself: in the review above I said that merging this first would put the rebase on #149. It went the other way. This PR is still in draft and I cannot flip that flag, #149 was complete and CI-green, so I merged #149 and this one now conflicts with Here is exactly what moved under you, so you are not reverse-engineering it from a conflict marker. All of it is in
elif msg == "stall_killed" and self._get("_stop_flag"):
rec.status = "stopped"It sits above the generic
One thing that should make your diff smaller: I have already added Nothing about the approval changes — the review stands, it is still a yes from me. When you have rebased, mark it ready and I will squash it. |
b1489c7 to
7f46f32
Compare
|
Rebase checked — the branch order in if ok: ...
elif msg == "stall_killed" and self._get("_stop_flag"): # → "stopped"
elif msg == "aborted_disk_full": # → "aborted"
elif msg == "stall_killed": # → re-queue
All eight checks green. Merging — thanks for the rebase, and for the patch. |
Every entry in this release came from an outside contributor. - #150 (@shard872, #116) a full disk aborts the run instead of retrying - #149 (@Allen58562, #65) Stop interrupts transfers already in flight - #153 (@AdvaitVarhade, #32) structured CLI exit codes - #161 (@Divesh-Kshirsagar, #151) pytest.ini with a narrow warning filter - #158 (@nightcityblade, #81) ruff runs once, not once per Python version - #157 (@XEDAB, #155) the assertion that could not fail - #162 (@XEDAB, #160) the stub that left the engine on the real network - #159 (@AashishGupta2007, #145) the generated-era THEME block README: consolidated the changelog sections, refreshed the stale test count, and replaced the stars badge with a contributors badge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Description
Fixes #116.
A destination filesystem running out of space is now treated as a run-level fatal condition instead of an ordinary per-file failure. The first
ENOSPCevent publishes a shared fatal signal, stops new queue intake and retries, and lets active transfers unwind cooperatively at their next stream/write boundary.The triggering and interrupted transfers keep their
.tmpfiles. Only the triggering URL is added to the failed list; peers interrupted by the shared fatal state are classified as aborted. Both the engine and CLI await their active work, attempt telemetry/report persistence, contain secondary persistence failures, and finish with disk-full-specific messaging.Cooperative unwinding was chosen over immediate task cancellation because active file writes run in executor threads and cannot be safely cancelled mid-write.
Root cause
The downloader's catch-all per-file error path classified
OSError(errno.ENOSPC)like a normal transfer error, allowing the queue and retry machinery to continue consuming bandwidth for data that could not be persisted.Type of change
Testing
pytest tests/test_disk_full.py -q— 13 passed (run twice before rebase; passed again after integration)pytest tests/ -q— 42 passedruff check moon_download.py moon_engine.py moon_cli.py tests/test_disk_full.py tests/test_exit_cleanup.py— passedgit diff --check— passedThe network-free regressions cover partial and short writes, exact remaining-byte reporting, active-peer exception races, retry/backoff suppression, retry-list and telemetry failures, terminal engine state, both frontends, and integration with #149's in-flight stop handling.
Disclosure
Implementation and review were assisted by OpenAI Codex using GPT-5.6 Terra and GPT-5.6 Sol.