Skip to content

Fix the critical and high issues of the 7.0.1 review - #258

Draft
bezzad wants to merge 14 commits into
developfrom
claude/project-thread-6is2im
Draft

bezzad wants to merge 14 commits into
developfrom
claude/project-thread-6is2im

Conversation

@bezzad

@bezzad bezzad commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Fixes the 2 critical and 3 high issues from the 7.0.1 review, and adds OpenSpec changes for all 34 issues.

OpenSpec

8 new changes in openspec/changes/ cover all 34 issues (the 16 older changes are not touched):

Change Issues
always-end-and-report C1, C2, L6
safe-download-setup H1, L1, L2
safe-download-files H2, H3, M10, M11, L7, L9
writer-queue-accounting M1, M3, L8, L11
faster-read-loop M2, M14
errors-and-retries M4, M8, M9, M13, L3, L5
safer-links M5, M6, M7, M12, L4
release-and-housekeeping M15, L10, L12, L13, L14

Only the C and H tasks are ticked. Medium and low issues are not fixed here.

Fixes (each test failed on the old code first)

  • C1. A disk error never ended the download. A storage error is now a StorageException (an IOException) and is never retried. When the writer dies, waiting chunks and flushes wake up. Test: StorageErrorTest (Running for ever before; even cancel hung with a memory limit).
  • C2. The end of a download could be lost. DownloadPackage.TryFinishAsync does the flush, close and rename first and sets the status last. If that work fails the status is Failed and DownloadFileCompleted comes once with the error. The file is always closed. Test: StorageErrorTest (memory and file downloads, failed rename).
  • H1. A setup error made Dispose() and the next download hang. The caller still gets the exception, but the status is Failed and the completion task ends. Test: SetupErrorTest (bad Content-Type header, throwing client factory).
  • H2. Resume from a package could give a broken file. The download starts from zero when the temp file is missing or cut, or the server file changed (size, ETag, Last-Modified). Test: PackageResumeTest.
  • H3. Two downloads to the same file lost the file. TempFileClaims lets one download per process write a name.download. With Rename the second one gets another name; otherwise it fails at once. FileShare.Read was not used: on Linux it takes a shared lock and does not stop a second writer. Test: SameFileTwiceTest.
  • Found on the way: a refused block returned its read buffer to the ArrayPool twice, so two later reads could share one array and mix the bytes of two downloads (seen in a full test run). Test: ChunkBufferOwnershipTest.

Small behavior change

ConcurrentStream.FlushAsync() throws StorageException after a write error (it threw AggregateException).

Tests

Full suite on .NET 10, Linux: 721 passed, 0 failed, build with 0 warnings. Other target frameworks and Windows/macOS are checked by CI.


Generated by Claude Code

C1, C2, L6: always-end-and-report
H1, L1, L2: safe-download-setup
H2, H3, M10, M11, L7, L9: safe-download-files
M1, M3, L8, L11: writer-queue-accounting
M2, M14: faster-read-loop
M4, M8, M9, M13, L3, L5: errors-and-retries
M5, M6, M7, M12, L4: safer-links
M15, L10, L12, L13, L14: release-and-housekeeping
…d (C1, C2)

C1: a write error of the storage is now a StorageException. It is not
retried, so the download fails at once. When the writer dies, waiting
chunks and flushes wake up.

C2: the end of a download does the work first (flush, close, rename)
and sets the status last. If the work fails the status is Failed and
DownloadFileCompleted comes once with the error. The file is always
closed.
When the storage refused a block, the rented buffer went back to the
ArrayPool twice (by the packet and by the read loop). Two later reads
then shared one array, and a download could write the bytes of another
download into its file. The storage now owns the buffer, also when the
write throws.
A bad request header or a client factory that throws left the status
Created and the completion task open, so Clear(), Dispose() and the
next download waited for ever. The caller still gets the exception, but
the status is Failed and the task ends.
…(H2, H3)

H2: a resume from a DownloadPackage starts from zero when the temp file
is missing or cut, or when the server file changed (size, ETag or
Last-Modified). Before, it gave a broken file that said Completed.

H3: only one download in the process may write a name.download file.
With FileExistPolicy.Rename the second download gets another name;
otherwise it fails at once with an IOException. Before, both said
Completed and no file was left.
@bezzad bezzad self-assigned this Nov 11, 2026
CI runs the net10.0 and net11.0 tests at the same time in one folder.
With the fixed name f.bin the two runs moved and deleted each other's
file. The old code hid it (a lost rename still said Completed); now the
lost rename is reported as Failed, so the test failed.
@ktxiaok

ktxiaok commented Nov 11, 2026

Copy link
Copy Markdown

There are still some problems at present. I will create a issue later.

Two downloads that start at the same moment with FileExistPolicy.Rename
could both pick the same free name; the second then failed its claim.
Now a lost claim asks for the next free name again.
The test checks the file bytes. The 500 ms BlockTimeout made its
one-connection resume fail on the slow AppVeyor box.
The temp file claim was freed after DownloadFileCompleted, so a retry
started from the event failed with 'another download is writing'. A new
download started right after a stop failed the same way. Now the claim
is freed before the event, and a new download waits while a cancelled
holder still closes the file.
…s the file

A resume from a DownloadPackage did not claim its temp file, so a second
download to the same path could still write it.
With ClearPackageOnCompletionWithFailure, a failed rename at the end
deleted the fully downloaded temp file. It is kept now.
On the Windows CI box the second download started before the resume
had claimed its file. Now the resume waits in its first progress event
until the second download has ended.
DownloadProgressChanged does not stop the download, so on the slow
Windows box the resume ended before the second download started.
A chunk event runs on the download thread and really holds it.
On the slow Windows box the first download was cancelled before it
started, so its package had no file name and the resume went to memory.
The test now waits for the first chunk event before the stop.

This branch was successfully deployed

3 active deployments
Windows — 5eabe410 Deployed Nov 12, 2026 by bezzad via build (10.0.x) #1048
Linux — 5eabe410 Deployed Nov 12, 2026 by bezzad via build (10.0.x) #1048
MacOS — 5eabe410 Deployed Nov 12, 2026 by bezzad via build (10.0.x) #583
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants