Skip to content

fix: flush handling (port of nodejs/node#66473) - #259

Merged
mcollina merged 3 commits into
masterfrom
fix/flush-handling
Oct 5, 2026
Merged

mcollina merged 3 commits into
masterfrom
fix/flush-handling

Conversation

@mcollina

@mcollina mcollina commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Ports the fixes from nodejs/node#66473 (Utf8Stream is a port of sonic-boom, and the same bugs exist here).

cc @jasnell

flush()

  • flush(cb) with minLength: 0 called cb synchronously while a write was still in flight. It now waits for pending writes.
  • flush(cb) followed by end() (with minLength > 0) never called cb. end() now waits for pending flushes before closing.
  • Concurrent flush() calls are tracked with counters instead of a shared boolean.
  • fsync errors meaning the fd cannot be synced (EBADF, EINVAL, ENOTSUP, EOPNOTSUPP, EROFS, e.g. stdout on a pipe or TTY) no longer fail the flush. stdout/stderr redirected to a regular file are still fsynced.
  • Periodic flushes no longer stack while a flush is pending.

flushSync()

No longer throws or changes behaviour in the common case; it preserves ordering where possible:

  • While waiting to retry an EAGAIN/EBUSY write, it cancels the retry and writes everything synchronously, in order.
  • From a 'write' listener, it writes the unwritten remainder of a partial write before newly queued data ('write' is now emitted after the written bytes are released).
  • While reopening, it writes the buffered data to the previous file.
  • With an async write in flight, only the queued data is written; it may reach the file before the in-flight data (documented in the README).
  • flushBufferSync() now keeps _lens in sync when re-queuing the current write buffer.

Lifecycle

  • end() + destroy() while the file is opening crashed with an uncaught EBADF (double close); destroy() + end() threw from the ready listener. Both now close cleanly.
  • destroy() with a write in flight lets it complete: a pending flush(cb) succeeds if all the data was written, and receives a SonicBoom destroyed error otherwise. With nothing in flight, it fails immediately.
  • Writes completing after destroy() no longer continue writing to the closed fd, and a pending EAGAIN/EBUSY retry is cancelled.

Differences from upstream

  • When a pending flush cannot complete because of destroy(), upstream emits a public 'error'; here only the flush callback receives the error. The destroy-while-opening case is aligned with upstream in a follow-up PR.

Tests

New cases in test/flush.test.js and test/flush-sync.test.js (sync and async, utf8 and buffer modes) fail without the fix and pass with it. README flush()/flushSync() docs updated.

- flush() with minLength 0 now waits for in-flight writes before
  calling the callback.
- end() waits for pending flush callbacks before closing.
- Concurrent flush() calls are tracked with counters.
- flush() no longer fsyncs stdout/stderr and ignores EBADF.
- Periodic flushes do not stack while a flush is pending.
- end()/reopen()/destroy() while opening no longer double-close or throw;
  pending flushes fail with an error when the stream is destroyed.

The flushSync() change from the upstream PR (throwing while a write
is in progress) is intentionally left out as it is breaking.
@mcollina
mcollina force-pushed the fix/flush-handling branch from d9ad48b to 3026b23 Compare October 4, 2026 17:47
- flushSync() preserves ordering where it can: it cancels a pending
  EAGAIN/EBUSY retry and writes everything in order, writes the
  remainder of a partial write first when called from a 'write'
  listener, and writes to the previous file while reopening. With an
  async write in flight, only the queued data is written.
- 'write' is emitted after the written bytes are released.
- flushBufferSync() keeps _lens in sync when re-queuing _writingBuf.
- flush() fsyncs stdout/stderr again (useful when redirected to a
  file) and ignores EBADF/EINVAL/ENOTSUP/EOPNOTSUPP/EROFS from fsync.
Align with nodejs/node#66473: when destroy() is called while a write
is in flight, a pending flush() now succeeds if all the data was
written, and fails only if data is left. A pending EAGAIN/EBUSY retry
is cancelled on destroy instead of writing to the closed fd.
@mcollina
mcollina merged commit 1bb2b6a into master Oct 5, 2026
22 checks passed
@mcollina
mcollina deleted the fix/flush-handling branch October 5, 2026 19:26
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