Skip to content

Make ConcurrentStream.FlushAsync reliable while a download runs (#259) - #260

Draft
bezzad wants to merge 2 commits into
developfrom
claude/project-thread-8pqaji
Draft

bezzad wants to merge 2 commits into
developfrom
claude/project-thread-8pqaji

Conversation

@bezzad

@bezzad bezzad commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Fixes #259.

Problem

To save a DownloadPackage for resume while it downloads, the safe order is: serialize the package, flush, then save the bytes. But ConcurrentStream.FlushAsync could not do this:

  1. It waited until the write queue was empty. While chunks write faster than the disk, the queue is never empty, so the flush did not end.
  2. On a FileStream it called Flush(false): the bytes stayed in the OS cache, not on the disk.
  3. When the stream was closed with blocks still in the queue, it returned with no error, but those blocks were dropped.
  4. The flush ran on the caller's thread, while the writer task could write to the same stream.

Fix

  • FlushAsync adds a small flush marker (a Packet with no bytes and a TaskCompletionSource) to the write queue and waits only for it. The queue is FIFO with one writer, so every block given before the call is written first.
  • The writer task flushes the base stream when it takes the marker (FileStream.Flush(true), else FlushAsync), so a flush never runs at the same time as a write.
  • A marker is not data: no progress, no buffer size.
  • If the writer fails, FlushAsync throws the writer's error. If the stream is closed first, it throws ObjectDisposedException. It never hangs.
  • The 20 ms Task.Delay in DownloadPackage.FlushInternalAsync is removed (it is not needed now).
  • XML docs on DownloadPackage.FlushAsync and ConcurrentStream.FlushAsync show the safe order.
  • No public API change. AbstractDownloadService.UpdatePackage needs no change: its metadata goes through the same FIFO queue after the blocks it counts.

Tests

New file UnitTests/ConcurrentStreamFlushTest.cs. The first commit has only the tests; these 5 fail on the old code:

  • FlushAsyncEndsWhileOtherWritesKeepComing
  • FlushAsyncWritesEveryBlockGivenBeforeIt
  • PackageFlushAsyncEndsWhileChunksKeepWriting
  • FlushAsyncOnAFileStreamWritesToTheDisk
  • FlushAsyncDoesNotReportSuccessWhenTheStreamIsClosedBeforeTheBytesAreWritten

The guard test FlushAsyncThrowsTheWriterErrorWhenTheWriterFailsDuringTheFlush keeps the error path working.

Local run (net10.0): build has 0 warnings, and all 714 tests pass.

Notes


Generated by Claude Code

… a download

Five tests that fail on the current code, and the OpenSpec change reliable-flush.
… to the disk (#259)

FlushAsync puts a flush marker into the write queue and waits for it. The
queue is FIFO with one writer, so every block given before the call is
written first. The writer then flushes the base stream (FileStream.Flush(true))
and completes the marker. When the writer fails or the stream is closed,
FlushAsync throws instead of hanging or reporting dropped bytes as written.
The 20 ms delay after each flush is removed.

This branch was successfully deployed

2 active and 1 inactive deployments
MacOS — 85be726f Deployed Nov 12, 2026 by bezzad via build (10.0.x) #582
Linux — 85be726f Deployed Nov 12, 2026 by bezzad via build (10.0.x) #1047
Windows — 85be726f Deployed Nov 12, 2026 by bezzad via build (10.0.x) #1047
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.

2 participants