test: deflake fastutf8stream destroy and reopen tests - #65554

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream
Aug 26, 2026
Merged

test: deflake fastutf8stream destroy and reopen tests#65554
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream

Conversation

@christianaurichzm

Copy link
Copy Markdown
Contributor

test-fastutf8stream-destroy and test-fastutf8stream-reopen can
read their destination files before the underlying write operation has
completed.

In test-fastutf8stream-destroy, readFile() is called immediately
after destroy(), while an asynchronous fs.write() may still be in
flight.

In test-fastutf8stream-reopen, the reads are ordered on 'drain'.
However, 'drain' indicates that the internal buffer has drained
sufficiently to allow continued writing; it does not guarantee that the
write being asserted has completed. In the reopen path, a 'drain' is
scheduled with process.nextTick() after 'ready', so it can be observed
before the subsequent write has completed.

Order these reads on the 'write' event instead, which is emitted from
#release() after the underlying write operation completes.

For the synchronous reopen path, the 'write' listener is registered
before calling write(), since the event can be emitted from within the
write() call.

This only changes test synchronization. No Utf8Stream runtime behavior
is changed.

Testing

Before the change:

  • test-fastutf8stream-destroy: 34 failures / 2880 runs
  • test-fastutf8stream-reopen: 54 failures / 2880 runs

After the change, locally on Linux x64:

  • test-fastutf8stream-destroy: 0 failures / 3600 runs
  • test-fastutf8stream-reopen: 0 failures / 3600 runs
  • tools/test.py -J --repeat=40: 80/80
  • eslint: passes
  • core-validate-commit: passes

Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md

Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. test Issues and PRs related to Node.js core tests and test infrastructure. labels Aug 26, 2026
@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

cc @mcollina, since you were involved in the earlier fastutf8stream flake investigation in #59638.

This is test-only and ready for CI. Could you add the request-ci label when convenient? Thanks!

@codecov

codecovBot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (7b6b21a) to head (db8a012).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65554 +/- ##
==========================================
+ Coverage 90.05% 90.06% +0.01% 
==========================================
Files 751 751 Lines 254420 254420 Branches 47975 47972 -3 ==========================================
+ Hits 229121 229156 +35 + Misses 16483 16445 -38 - Partials 8816 8819 +3 

see 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@panvapanva added the flaky-test Issues and PRs involving tests that fail intermittently in CI. label Aug 26, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panvapanva added the review wanted PRs that need review. label Aug 26, 2026
@panvapanva added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 26, 2026
@panvapanva added the fast-track PRs proposed for a shorter-than-standard waiting period before landing. label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast-track has been requested by @panva. Please 👍 to approve.

@panva

panva commented Aug 26, 2026

Copy link
Copy Markdown
Member

pending node-stress-single-test started by @sxa

Edit: appears to ✅

@sxa

sxa commented Aug 26, 2026

Copy link
Copy Markdown
Member

Edit: appears to ✅
The one you linked to (848) was a different test and yours hasn't run through to completion yet yet - the ones I have are:

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

You're right that those runs don't prove much yet. One important difference from the reproducer I used is concurrency.

The race is between the fs.write() that is still in flight and the subsequent readFile() path, which is not ordered after that write has completed. I was able to make it reproduce often enough by running each test file in 24 concurrent processes, with a separate TEST_THREAD_ID per process so they do not share a tmpdir.

Across two Linux x64 environments, the rates varied quite a bit for destroy and were more stable for reopen:

  • test-fastutf8stream-destroy: 25 / 8640 (0.29%) and 34 / 2880 (1.18%)
  • test-fastutf8stream-reopen: 152 / 8640 (1.76%) and 54 / 2880 (1.88%)

Taking the lower destroy rate and assuming roughly independent runs, 100 runs still have about a 75% chance of producing zero failures, and even 1000 runs have about a 5% chance. So 844 coming back clean is an expected outcome rather than evidence against the race, and 853 may well come back clean too without disproving anything. Sequential runs on an otherwise idle worker are also much less favorable for reproducing the contention I was using.

Every failure I observed had the same signature: readFile() returning '' at the assertion this patch reorders. I did not observe another failure mode in the stress runs.

With the patch applied, both tests produced 0 failures in 8640 runs each in the environment that produced the 0.29% and 1.76% baselines, where those rates would correspond to roughly 25 and 152 failures.

The ordering issue itself does not depend on reproducing the flake every time: drain is not a completion barrier for the write being asserted, whereas the write event used here is emitted from #release() after the underlying write operation completes.

Happy to share the stress setup or run other configurations if useful.

@panvapanva added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 2e8a4b1 into nodejs:mainAug 26, 2026
102 of 103 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 2e8a4b1

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
aduh95 pushed a commit that referenced this pull request Aug 29, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.fast-trackPRs proposed for a shorter-than-standard waiting period before landing.flaky-testIssues and PRs involving tests that fail intermittently in CI.needs-ciPRs that need a full CI run.review wantedPRs that need review.testIssues and PRs related to Node.js core tests and test infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@christianaurichzm@nodejs-github-bot@panva@sxa@codebytere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test: deflake fastutf8stream destroy and reopen tests - #65554

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream
Aug 26, 2026
Merged

test: deflake fastutf8stream destroy and reopen tests#65554
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream

Conversation

@christianaurichzm

Copy link
Copy Markdown
Contributor

test-fastutf8stream-destroy and test-fastutf8stream-reopen can
read their destination files before the underlying write operation has
completed.

In test-fastutf8stream-destroy, readFile() is called immediately
after destroy(), while an asynchronous fs.write() may still be in
flight.

In test-fastutf8stream-reopen, the reads are ordered on 'drain'.
However, 'drain' indicates that the internal buffer has drained
sufficiently to allow continued writing; it does not guarantee that the
write being asserted has completed. In the reopen path, a 'drain' is
scheduled with process.nextTick() after 'ready', so it can be observed
before the subsequent write has completed.

Order these reads on the 'write' event instead, which is emitted from
#release() after the underlying write operation completes.

For the synchronous reopen path, the 'write' listener is registered
before calling write(), since the event can be emitted from within the
write() call.

This only changes test synchronization. No Utf8Stream runtime behavior
is changed.

Testing

Before the change:

  • test-fastutf8stream-destroy: 34 failures / 2880 runs
  • test-fastutf8stream-reopen: 54 failures / 2880 runs

After the change, locally on Linux x64:

  • test-fastutf8stream-destroy: 0 failures / 3600 runs
  • test-fastutf8stream-reopen: 0 failures / 3600 runs
  • tools/test.py -J --repeat=40: 80/80
  • eslint: passes
  • core-validate-commit: passes

Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md

Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. test Issues and PRs related to Node.js core tests and test infrastructure. labels Aug 26, 2026
@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

cc @mcollina, since you were involved in the earlier fastutf8stream flake investigation in #59638.

This is test-only and ready for CI. Could you add the request-ci label when convenient? Thanks!

@codecov

codecovBot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (7b6b21a) to head (db8a012).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65554 +/- ##
==========================================
+ Coverage 90.05% 90.06% +0.01% 
==========================================
Files 751 751 Lines 254420 254420 Branches 47975 47972 -3 ==========================================
+ Hits 229121 229156 +35 + Misses 16483 16445 -38 - Partials 8816 8819 +3 

see 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@panvapanva added the flaky-test Issues and PRs involving tests that fail intermittently in CI. label Aug 26, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panvapanva added the review wanted PRs that need review. label Aug 26, 2026
@panvapanva added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 26, 2026
@panvapanva added the fast-track PRs proposed for a shorter-than-standard waiting period before landing. label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast-track has been requested by @panva. Please 👍 to approve.

@panva

panva commented Aug 26, 2026

Copy link
Copy Markdown
Member

pending node-stress-single-test started by @sxa

Edit: appears to ✅

@sxa

sxa commented Aug 26, 2026

Copy link
Copy Markdown
Member

Edit: appears to ✅
The one you linked to (848) was a different test and yours hasn't run through to completion yet yet - the ones I have are:

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

You're right that those runs don't prove much yet. One important difference from the reproducer I used is concurrency.

The race is between the fs.write() that is still in flight and the subsequent readFile() path, which is not ordered after that write has completed. I was able to make it reproduce often enough by running each test file in 24 concurrent processes, with a separate TEST_THREAD_ID per process so they do not share a tmpdir.

Across two Linux x64 environments, the rates varied quite a bit for destroy and were more stable for reopen:

  • test-fastutf8stream-destroy: 25 / 8640 (0.29%) and 34 / 2880 (1.18%)
  • test-fastutf8stream-reopen: 152 / 8640 (1.76%) and 54 / 2880 (1.88%)

Taking the lower destroy rate and assuming roughly independent runs, 100 runs still have about a 75% chance of producing zero failures, and even 1000 runs have about a 5% chance. So 844 coming back clean is an expected outcome rather than evidence against the race, and 853 may well come back clean too without disproving anything. Sequential runs on an otherwise idle worker are also much less favorable for reproducing the contention I was using.

Every failure I observed had the same signature: readFile() returning '' at the assertion this patch reorders. I did not observe another failure mode in the stress runs.

With the patch applied, both tests produced 0 failures in 8640 runs each in the environment that produced the 0.29% and 1.76% baselines, where those rates would correspond to roughly 25 and 152 failures.

The ordering issue itself does not depend on reproducing the flake every time: drain is not a completion barrier for the write being asserted, whereas the write event used here is emitted from #release() after the underlying write operation completes.

Happy to share the stress setup or run other configurations if useful.

@panvapanva added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 2e8a4b1 into nodejs:mainAug 26, 2026
102 of 103 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 2e8a4b1

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
aduh95 pushed a commit that referenced this pull request Aug 29, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.fast-trackPRs proposed for a shorter-than-standard waiting period before landing.flaky-testIssues and PRs involving tests that fail intermittently in CI.needs-ciPRs that need a full CI run.review wantedPRs that need review.testIssues and PRs related to Node.js core tests and test infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@christianaurichzm@nodejs-github-bot@panva@sxa@codebytere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test: deflake fastutf8stream destroy and reopen tests - #65554

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream
Aug 26, 2026
Merged

test: deflake fastutf8stream destroy and reopen tests#65554
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream

Conversation

@christianaurichzm

Copy link
Copy Markdown
Contributor

test-fastutf8stream-destroy and test-fastutf8stream-reopen can
read their destination files before the underlying write operation has
completed.

In test-fastutf8stream-destroy, readFile() is called immediately
after destroy(), while an asynchronous fs.write() may still be in
flight.

In test-fastutf8stream-reopen, the reads are ordered on 'drain'.
However, 'drain' indicates that the internal buffer has drained
sufficiently to allow continued writing; it does not guarantee that the
write being asserted has completed. In the reopen path, a 'drain' is
scheduled with process.nextTick() after 'ready', so it can be observed
before the subsequent write has completed.

Order these reads on the 'write' event instead, which is emitted from
#release() after the underlying write operation completes.

For the synchronous reopen path, the 'write' listener is registered
before calling write(), since the event can be emitted from within the
write() call.

This only changes test synchronization. No Utf8Stream runtime behavior
is changed.

Testing

Before the change:

  • test-fastutf8stream-destroy: 34 failures / 2880 runs
  • test-fastutf8stream-reopen: 54 failures / 2880 runs

After the change, locally on Linux x64:

  • test-fastutf8stream-destroy: 0 failures / 3600 runs
  • test-fastutf8stream-reopen: 0 failures / 3600 runs
  • tools/test.py -J --repeat=40: 80/80
  • eslint: passes
  • core-validate-commit: passes

Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md

Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. test Issues and PRs related to Node.js core tests and test infrastructure. labels Aug 26, 2026
@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

cc @mcollina, since you were involved in the earlier fastutf8stream flake investigation in #59638.

This is test-only and ready for CI. Could you add the request-ci label when convenient? Thanks!

@codecov

codecovBot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (7b6b21a) to head (db8a012).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65554 +/- ##
==========================================
+ Coverage 90.05% 90.06% +0.01% 
==========================================
Files 751 751 Lines 254420 254420 Branches 47975 47972 -3 ==========================================
+ Hits 229121 229156 +35 + Misses 16483 16445 -38 - Partials 8816 8819 +3 

see 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@panvapanva added the flaky-test Issues and PRs involving tests that fail intermittently in CI. label Aug 26, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panvapanva added the review wanted PRs that need review. label Aug 26, 2026
@panvapanva added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 26, 2026
@panvapanva added the fast-track PRs proposed for a shorter-than-standard waiting period before landing. label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast-track has been requested by @panva. Please 👍 to approve.

@panva

panva commented Aug 26, 2026

Copy link
Copy Markdown
Member

pending node-stress-single-test started by @sxa

Edit: appears to ✅

@sxa

sxa commented Aug 26, 2026

Copy link
Copy Markdown
Member

Edit: appears to ✅
The one you linked to (848) was a different test and yours hasn't run through to completion yet yet - the ones I have are:

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

You're right that those runs don't prove much yet. One important difference from the reproducer I used is concurrency.

The race is between the fs.write() that is still in flight and the subsequent readFile() path, which is not ordered after that write has completed. I was able to make it reproduce often enough by running each test file in 24 concurrent processes, with a separate TEST_THREAD_ID per process so they do not share a tmpdir.

Across two Linux x64 environments, the rates varied quite a bit for destroy and were more stable for reopen:

  • test-fastutf8stream-destroy: 25 / 8640 (0.29%) and 34 / 2880 (1.18%)
  • test-fastutf8stream-reopen: 152 / 8640 (1.76%) and 54 / 2880 (1.88%)

Taking the lower destroy rate and assuming roughly independent runs, 100 runs still have about a 75% chance of producing zero failures, and even 1000 runs have about a 5% chance. So 844 coming back clean is an expected outcome rather than evidence against the race, and 853 may well come back clean too without disproving anything. Sequential runs on an otherwise idle worker are also much less favorable for reproducing the contention I was using.

Every failure I observed had the same signature: readFile() returning '' at the assertion this patch reorders. I did not observe another failure mode in the stress runs.

With the patch applied, both tests produced 0 failures in 8640 runs each in the environment that produced the 0.29% and 1.76% baselines, where those rates would correspond to roughly 25 and 152 failures.

The ordering issue itself does not depend on reproducing the flake every time: drain is not a completion barrier for the write being asserted, whereas the write event used here is emitted from #release() after the underlying write operation completes.

Happy to share the stress setup or run other configurations if useful.

@panvapanva added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 2e8a4b1 into nodejs:mainAug 26, 2026
102 of 103 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 2e8a4b1

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
aduh95 pushed a commit that referenced this pull request Aug 29, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.fast-trackPRs proposed for a shorter-than-standard waiting period before landing.flaky-testIssues and PRs involving tests that fail intermittently in CI.needs-ciPRs that need a full CI run.review wantedPRs that need review.testIssues and PRs related to Node.js core tests and test infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@christianaurichzm@nodejs-github-bot@panva@sxa@codebytere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test: deflake fastutf8stream destroy and reopen tests - #65554

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream
Aug 26, 2026
Merged

test: deflake fastutf8stream destroy and reopen tests#65554
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream

Conversation

@christianaurichzm

Copy link
Copy Markdown
Contributor

test-fastutf8stream-destroy and test-fastutf8stream-reopen can
read their destination files before the underlying write operation has
completed.

In test-fastutf8stream-destroy, readFile() is called immediately
after destroy(), while an asynchronous fs.write() may still be in
flight.

In test-fastutf8stream-reopen, the reads are ordered on 'drain'.
However, 'drain' indicates that the internal buffer has drained
sufficiently to allow continued writing; it does not guarantee that the
write being asserted has completed. In the reopen path, a 'drain' is
scheduled with process.nextTick() after 'ready', so it can be observed
before the subsequent write has completed.

Order these reads on the 'write' event instead, which is emitted from
#release() after the underlying write operation completes.

For the synchronous reopen path, the 'write' listener is registered
before calling write(), since the event can be emitted from within the
write() call.

This only changes test synchronization. No Utf8Stream runtime behavior
is changed.

Testing

Before the change:

  • test-fastutf8stream-destroy: 34 failures / 2880 runs
  • test-fastutf8stream-reopen: 54 failures / 2880 runs

After the change, locally on Linux x64:

  • test-fastutf8stream-destroy: 0 failures / 3600 runs
  • test-fastutf8stream-reopen: 0 failures / 3600 runs
  • tools/test.py -J --repeat=40: 80/80
  • eslint: passes
  • core-validate-commit: passes

Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md

Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. test Issues and PRs related to Node.js core tests and test infrastructure. labels Aug 26, 2026
@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

cc @mcollina, since you were involved in the earlier fastutf8stream flake investigation in #59638.

This is test-only and ready for CI. Could you add the request-ci label when convenient? Thanks!

@codecov

codecovBot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (7b6b21a) to head (db8a012).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65554 +/- ##
==========================================
+ Coverage 90.05% 90.06% +0.01% 
==========================================
Files 751 751 Lines 254420 254420 Branches 47975 47972 -3 ==========================================
+ Hits 229121 229156 +35 + Misses 16483 16445 -38 - Partials 8816 8819 +3 

see 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@panvapanva added the flaky-test Issues and PRs involving tests that fail intermittently in CI. label Aug 26, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panvapanva added the review wanted PRs that need review. label Aug 26, 2026
@panvapanva added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 26, 2026
@panvapanva added the fast-track PRs proposed for a shorter-than-standard waiting period before landing. label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast-track has been requested by @panva. Please 👍 to approve.

@panva

panva commented Aug 26, 2026

Copy link
Copy Markdown
Member

pending node-stress-single-test started by @sxa

Edit: appears to ✅

@sxa

sxa commented Aug 26, 2026

Copy link
Copy Markdown
Member

Edit: appears to ✅
The one you linked to (848) was a different test and yours hasn't run through to completion yet yet - the ones I have are:

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

You're right that those runs don't prove much yet. One important difference from the reproducer I used is concurrency.

The race is between the fs.write() that is still in flight and the subsequent readFile() path, which is not ordered after that write has completed. I was able to make it reproduce often enough by running each test file in 24 concurrent processes, with a separate TEST_THREAD_ID per process so they do not share a tmpdir.

Across two Linux x64 environments, the rates varied quite a bit for destroy and were more stable for reopen:

  • test-fastutf8stream-destroy: 25 / 8640 (0.29%) and 34 / 2880 (1.18%)
  • test-fastutf8stream-reopen: 152 / 8640 (1.76%) and 54 / 2880 (1.88%)

Taking the lower destroy rate and assuming roughly independent runs, 100 runs still have about a 75% chance of producing zero failures, and even 1000 runs have about a 5% chance. So 844 coming back clean is an expected outcome rather than evidence against the race, and 853 may well come back clean too without disproving anything. Sequential runs on an otherwise idle worker are also much less favorable for reproducing the contention I was using.

Every failure I observed had the same signature: readFile() returning '' at the assertion this patch reorders. I did not observe another failure mode in the stress runs.

With the patch applied, both tests produced 0 failures in 8640 runs each in the environment that produced the 0.29% and 1.76% baselines, where those rates would correspond to roughly 25 and 152 failures.

The ordering issue itself does not depend on reproducing the flake every time: drain is not a completion barrier for the write being asserted, whereas the write event used here is emitted from #release() after the underlying write operation completes.

Happy to share the stress setup or run other configurations if useful.

@panvapanva added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 2e8a4b1 into nodejs:mainAug 26, 2026
102 of 103 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 2e8a4b1

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
aduh95 pushed a commit that referenced this pull request Aug 29, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.fast-trackPRs proposed for a shorter-than-standard waiting period before landing.flaky-testIssues and PRs involving tests that fail intermittently in CI.needs-ciPRs that need a full CI run.review wantedPRs that need review.testIssues and PRs related to Node.js core tests and test infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@christianaurichzm@nodejs-github-bot@panva@sxa@codebytere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test: deflake fastutf8stream destroy and reopen tests - #65554

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream
Aug 26, 2026
Merged

test: deflake fastutf8stream destroy and reopen tests#65554
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream

Conversation

@christianaurichzm

Copy link
Copy Markdown
Contributor

test-fastutf8stream-destroy and test-fastutf8stream-reopen can
read their destination files before the underlying write operation has
completed.

In test-fastutf8stream-destroy, readFile() is called immediately
after destroy(), while an asynchronous fs.write() may still be in
flight.

In test-fastutf8stream-reopen, the reads are ordered on 'drain'.
However, 'drain' indicates that the internal buffer has drained
sufficiently to allow continued writing; it does not guarantee that the
write being asserted has completed. In the reopen path, a 'drain' is
scheduled with process.nextTick() after 'ready', so it can be observed
before the subsequent write has completed.

Order these reads on the 'write' event instead, which is emitted from
#release() after the underlying write operation completes.

For the synchronous reopen path, the 'write' listener is registered
before calling write(), since the event can be emitted from within the
write() call.

This only changes test synchronization. No Utf8Stream runtime behavior
is changed.

Testing

Before the change:

  • test-fastutf8stream-destroy: 34 failures / 2880 runs
  • test-fastutf8stream-reopen: 54 failures / 2880 runs

After the change, locally on Linux x64:

  • test-fastutf8stream-destroy: 0 failures / 3600 runs
  • test-fastutf8stream-reopen: 0 failures / 3600 runs
  • tools/test.py -J --repeat=40: 80/80
  • eslint: passes
  • core-validate-commit: passes

Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md

Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. test Issues and PRs related to Node.js core tests and test infrastructure. labels Aug 26, 2026
@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

cc @mcollina, since you were involved in the earlier fastutf8stream flake investigation in #59638.

This is test-only and ready for CI. Could you add the request-ci label when convenient? Thanks!

@codecov

codecovBot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (7b6b21a) to head (db8a012).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65554 +/- ##
==========================================
+ Coverage 90.05% 90.06% +0.01% 
==========================================
Files 751 751 Lines 254420 254420 Branches 47975 47972 -3 ==========================================
+ Hits 229121 229156 +35 + Misses 16483 16445 -38 - Partials 8816 8819 +3 

see 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@panvapanva added the flaky-test Issues and PRs involving tests that fail intermittently in CI. label Aug 26, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panvapanva added the review wanted PRs that need review. label Aug 26, 2026
@panvapanva added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 26, 2026
@panvapanva added the fast-track PRs proposed for a shorter-than-standard waiting period before landing. label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast-track has been requested by @panva. Please 👍 to approve.

@panva

panva commented Aug 26, 2026

Copy link
Copy Markdown
Member

pending node-stress-single-test started by @sxa

Edit: appears to ✅

@sxa

sxa commented Aug 26, 2026

Copy link
Copy Markdown
Member

Edit: appears to ✅
The one you linked to (848) was a different test and yours hasn't run through to completion yet yet - the ones I have are:

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

You're right that those runs don't prove much yet. One important difference from the reproducer I used is concurrency.

The race is between the fs.write() that is still in flight and the subsequent readFile() path, which is not ordered after that write has completed. I was able to make it reproduce often enough by running each test file in 24 concurrent processes, with a separate TEST_THREAD_ID per process so they do not share a tmpdir.

Across two Linux x64 environments, the rates varied quite a bit for destroy and were more stable for reopen:

  • test-fastutf8stream-destroy: 25 / 8640 (0.29%) and 34 / 2880 (1.18%)
  • test-fastutf8stream-reopen: 152 / 8640 (1.76%) and 54 / 2880 (1.88%)

Taking the lower destroy rate and assuming roughly independent runs, 100 runs still have about a 75% chance of producing zero failures, and even 1000 runs have about a 5% chance. So 844 coming back clean is an expected outcome rather than evidence against the race, and 853 may well come back clean too without disproving anything. Sequential runs on an otherwise idle worker are also much less favorable for reproducing the contention I was using.

Every failure I observed had the same signature: readFile() returning '' at the assertion this patch reorders. I did not observe another failure mode in the stress runs.

With the patch applied, both tests produced 0 failures in 8640 runs each in the environment that produced the 0.29% and 1.76% baselines, where those rates would correspond to roughly 25 and 152 failures.

The ordering issue itself does not depend on reproducing the flake every time: drain is not a completion barrier for the write being asserted, whereas the write event used here is emitted from #release() after the underlying write operation completes.

Happy to share the stress setup or run other configurations if useful.

@panvapanva added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 2e8a4b1 into nodejs:mainAug 26, 2026
102 of 103 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 2e8a4b1

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
aduh95 pushed a commit that referenced this pull request Aug 29, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.fast-trackPRs proposed for a shorter-than-standard waiting period before landing.flaky-testIssues and PRs involving tests that fail intermittently in CI.needs-ciPRs that need a full CI run.review wantedPRs that need review.testIssues and PRs related to Node.js core tests and test infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@christianaurichzm@nodejs-github-bot@panva@sxa@codebytere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test: deflake fastutf8stream destroy and reopen tests - #65554

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream
Aug 26, 2026
Merged

test: deflake fastutf8stream destroy and reopen tests#65554
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream

Conversation

@christianaurichzm

Copy link
Copy Markdown
Contributor

test-fastutf8stream-destroy and test-fastutf8stream-reopen can
read their destination files before the underlying write operation has
completed.

In test-fastutf8stream-destroy, readFile() is called immediately
after destroy(), while an asynchronous fs.write() may still be in
flight.

In test-fastutf8stream-reopen, the reads are ordered on 'drain'.
However, 'drain' indicates that the internal buffer has drained
sufficiently to allow continued writing; it does not guarantee that the
write being asserted has completed. In the reopen path, a 'drain' is
scheduled with process.nextTick() after 'ready', so it can be observed
before the subsequent write has completed.

Order these reads on the 'write' event instead, which is emitted from
#release() after the underlying write operation completes.

For the synchronous reopen path, the 'write' listener is registered
before calling write(), since the event can be emitted from within the
write() call.

This only changes test synchronization. No Utf8Stream runtime behavior
is changed.

Testing

Before the change:

  • test-fastutf8stream-destroy: 34 failures / 2880 runs
  • test-fastutf8stream-reopen: 54 failures / 2880 runs

After the change, locally on Linux x64:

  • test-fastutf8stream-destroy: 0 failures / 3600 runs
  • test-fastutf8stream-reopen: 0 failures / 3600 runs
  • tools/test.py -J --repeat=40: 80/80
  • eslint: passes
  • core-validate-commit: passes

Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md

Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. test Issues and PRs related to Node.js core tests and test infrastructure. labels Aug 26, 2026
@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

cc @mcollina, since you were involved in the earlier fastutf8stream flake investigation in #59638.

This is test-only and ready for CI. Could you add the request-ci label when convenient? Thanks!

@codecov

codecovBot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (7b6b21a) to head (db8a012).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65554 +/- ##
==========================================
+ Coverage 90.05% 90.06% +0.01% 
==========================================
Files 751 751 Lines 254420 254420 Branches 47975 47972 -3 ==========================================
+ Hits 229121 229156 +35 + Misses 16483 16445 -38 - Partials 8816 8819 +3 

see 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@panvapanva added the flaky-test Issues and PRs involving tests that fail intermittently in CI. label Aug 26, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panvapanva added the review wanted PRs that need review. label Aug 26, 2026
@panvapanva added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 26, 2026
@panvapanva added the fast-track PRs proposed for a shorter-than-standard waiting period before landing. label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast-track has been requested by @panva. Please 👍 to approve.

@panva

panva commented Aug 26, 2026

Copy link
Copy Markdown
Member

pending node-stress-single-test started by @sxa

Edit: appears to ✅

@sxa

sxa commented Aug 26, 2026

Copy link
Copy Markdown
Member

Edit: appears to ✅
The one you linked to (848) was a different test and yours hasn't run through to completion yet yet - the ones I have are:

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

You're right that those runs don't prove much yet. One important difference from the reproducer I used is concurrency.

The race is between the fs.write() that is still in flight and the subsequent readFile() path, which is not ordered after that write has completed. I was able to make it reproduce often enough by running each test file in 24 concurrent processes, with a separate TEST_THREAD_ID per process so they do not share a tmpdir.

Across two Linux x64 environments, the rates varied quite a bit for destroy and were more stable for reopen:

  • test-fastutf8stream-destroy: 25 / 8640 (0.29%) and 34 / 2880 (1.18%)
  • test-fastutf8stream-reopen: 152 / 8640 (1.76%) and 54 / 2880 (1.88%)

Taking the lower destroy rate and assuming roughly independent runs, 100 runs still have about a 75% chance of producing zero failures, and even 1000 runs have about a 5% chance. So 844 coming back clean is an expected outcome rather than evidence against the race, and 853 may well come back clean too without disproving anything. Sequential runs on an otherwise idle worker are also much less favorable for reproducing the contention I was using.

Every failure I observed had the same signature: readFile() returning '' at the assertion this patch reorders. I did not observe another failure mode in the stress runs.

With the patch applied, both tests produced 0 failures in 8640 runs each in the environment that produced the 0.29% and 1.76% baselines, where those rates would correspond to roughly 25 and 152 failures.

The ordering issue itself does not depend on reproducing the flake every time: drain is not a completion barrier for the write being asserted, whereas the write event used here is emitted from #release() after the underlying write operation completes.

Happy to share the stress setup or run other configurations if useful.

@panvapanva added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 2e8a4b1 into nodejs:mainAug 26, 2026
102 of 103 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 2e8a4b1

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
aduh95 pushed a commit that referenced this pull request Aug 29, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.fast-trackPRs proposed for a shorter-than-standard waiting period before landing.flaky-testIssues and PRs involving tests that fail intermittently in CI.needs-ciPRs that need a full CI run.review wantedPRs that need review.testIssues and PRs related to Node.js core tests and test infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@christianaurichzm@nodejs-github-bot@panva@sxa@codebytere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test: deflake fastutf8stream destroy and reopen tests - #65554

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream
Aug 26, 2026
Merged

test: deflake fastutf8stream destroy and reopen tests#65554
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream

Conversation

@christianaurichzm

Copy link
Copy Markdown
Contributor

test-fastutf8stream-destroy and test-fastutf8stream-reopen can
read their destination files before the underlying write operation has
completed.

In test-fastutf8stream-destroy, readFile() is called immediately
after destroy(), while an asynchronous fs.write() may still be in
flight.

In test-fastutf8stream-reopen, the reads are ordered on 'drain'.
However, 'drain' indicates that the internal buffer has drained
sufficiently to allow continued writing; it does not guarantee that the
write being asserted has completed. In the reopen path, a 'drain' is
scheduled with process.nextTick() after 'ready', so it can be observed
before the subsequent write has completed.

Order these reads on the 'write' event instead, which is emitted from
#release() after the underlying write operation completes.

For the synchronous reopen path, the 'write' listener is registered
before calling write(), since the event can be emitted from within the
write() call.

This only changes test synchronization. No Utf8Stream runtime behavior
is changed.

Testing

Before the change:

  • test-fastutf8stream-destroy: 34 failures / 2880 runs
  • test-fastutf8stream-reopen: 54 failures / 2880 runs

After the change, locally on Linux x64:

  • test-fastutf8stream-destroy: 0 failures / 3600 runs
  • test-fastutf8stream-reopen: 0 failures / 3600 runs
  • tools/test.py -J --repeat=40: 80/80
  • eslint: passes
  • core-validate-commit: passes

Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md

Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. test Issues and PRs related to Node.js core tests and test infrastructure. labels Aug 26, 2026
@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

cc @mcollina, since you were involved in the earlier fastutf8stream flake investigation in #59638.

This is test-only and ready for CI. Could you add the request-ci label when convenient? Thanks!

@codecov

codecovBot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (7b6b21a) to head (db8a012).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65554 +/- ##
==========================================
+ Coverage 90.05% 90.06% +0.01% 
==========================================
Files 751 751 Lines 254420 254420 Branches 47975 47972 -3 ==========================================
+ Hits 229121 229156 +35 + Misses 16483 16445 -38 - Partials 8816 8819 +3 

see 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@panvapanva added the flaky-test Issues and PRs involving tests that fail intermittently in CI. label Aug 26, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panvapanva added the review wanted PRs that need review. label Aug 26, 2026
@panvapanva added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 26, 2026
@panvapanva added the fast-track PRs proposed for a shorter-than-standard waiting period before landing. label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast-track has been requested by @panva. Please 👍 to approve.

@panva

panva commented Aug 26, 2026

Copy link
Copy Markdown
Member

pending node-stress-single-test started by @sxa

Edit: appears to ✅

@sxa

sxa commented Aug 26, 2026

Copy link
Copy Markdown
Member

Edit: appears to ✅
The one you linked to (848) was a different test and yours hasn't run through to completion yet yet - the ones I have are:

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

You're right that those runs don't prove much yet. One important difference from the reproducer I used is concurrency.

The race is between the fs.write() that is still in flight and the subsequent readFile() path, which is not ordered after that write has completed. I was able to make it reproduce often enough by running each test file in 24 concurrent processes, with a separate TEST_THREAD_ID per process so they do not share a tmpdir.

Across two Linux x64 environments, the rates varied quite a bit for destroy and were more stable for reopen:

  • test-fastutf8stream-destroy: 25 / 8640 (0.29%) and 34 / 2880 (1.18%)
  • test-fastutf8stream-reopen: 152 / 8640 (1.76%) and 54 / 2880 (1.88%)

Taking the lower destroy rate and assuming roughly independent runs, 100 runs still have about a 75% chance of producing zero failures, and even 1000 runs have about a 5% chance. So 844 coming back clean is an expected outcome rather than evidence against the race, and 853 may well come back clean too without disproving anything. Sequential runs on an otherwise idle worker are also much less favorable for reproducing the contention I was using.

Every failure I observed had the same signature: readFile() returning '' at the assertion this patch reorders. I did not observe another failure mode in the stress runs.

With the patch applied, both tests produced 0 failures in 8640 runs each in the environment that produced the 0.29% and 1.76% baselines, where those rates would correspond to roughly 25 and 152 failures.

The ordering issue itself does not depend on reproducing the flake every time: drain is not a completion barrier for the write being asserted, whereas the write event used here is emitted from #release() after the underlying write operation completes.

Happy to share the stress setup or run other configurations if useful.

@panvapanva added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 2e8a4b1 into nodejs:mainAug 26, 2026
102 of 103 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 2e8a4b1

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
aduh95 pushed a commit that referenced this pull request Aug 29, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.fast-trackPRs proposed for a shorter-than-standard waiting period before landing.flaky-testIssues and PRs involving tests that fail intermittently in CI.needs-ciPRs that need a full CI run.review wantedPRs that need review.testIssues and PRs related to Node.js core tests and test infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@christianaurichzm@nodejs-github-bot@panva@sxa@codebytere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test: deflake fastutf8stream destroy and reopen tests - #65554

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream
Aug 26, 2026
Merged

test: deflake fastutf8stream destroy and reopen tests#65554
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
christianaurichzm:deflake-fastutf8stream

Conversation

@christianaurichzm

Copy link
Copy Markdown
Contributor

test-fastutf8stream-destroy and test-fastutf8stream-reopen can
read their destination files before the underlying write operation has
completed.

In test-fastutf8stream-destroy, readFile() is called immediately
after destroy(), while an asynchronous fs.write() may still be in
flight.

In test-fastutf8stream-reopen, the reads are ordered on 'drain'.
However, 'drain' indicates that the internal buffer has drained
sufficiently to allow continued writing; it does not guarantee that the
write being asserted has completed. In the reopen path, a 'drain' is
scheduled with process.nextTick() after 'ready', so it can be observed
before the subsequent write has completed.

Order these reads on the 'write' event instead, which is emitted from
#release() after the underlying write operation completes.

For the synchronous reopen path, the 'write' listener is registered
before calling write(), since the event can be emitted from within the
write() call.

This only changes test synchronization. No Utf8Stream runtime behavior
is changed.

Testing

Before the change:

  • test-fastutf8stream-destroy: 34 failures / 2880 runs
  • test-fastutf8stream-reopen: 54 failures / 2880 runs

After the change, locally on Linux x64:

  • test-fastutf8stream-destroy: 0 failures / 3600 runs
  • test-fastutf8stream-reopen: 0 failures / 3600 runs
  • tools/test.py -J --repeat=40: 80/80
  • eslint: passes
  • core-validate-commit: passes

Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md

Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. test Issues and PRs related to Node.js core tests and test infrastructure. labels Aug 26, 2026
@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

cc @mcollina, since you were involved in the earlier fastutf8stream flake investigation in #59638.

This is test-only and ready for CI. Could you add the request-ci label when convenient? Thanks!

@codecov

codecovBot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (7b6b21a) to head (db8a012).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65554 +/- ##
==========================================
+ Coverage 90.05% 90.06% +0.01% 
==========================================
Files 751 751 Lines 254420 254420 Branches 47975 47972 -3 ==========================================
+ Hits 229121 229156 +35 + Misses 16483 16445 -38 - Partials 8816 8819 +3 

see 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@panvapanva added the flaky-test Issues and PRs involving tests that fail intermittently in CI. label Aug 26, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panvapanva added the review wanted PRs that need review. label Aug 26, 2026
@panvapanva added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Aug 26, 2026
@panvapanva added the fast-track PRs proposed for a shorter-than-standard waiting period before landing. label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast-track has been requested by @panva. Please 👍 to approve.

@panva

panva commented Aug 26, 2026

Copy link
Copy Markdown
Member

pending node-stress-single-test started by @sxa

Edit: appears to ✅

@sxa

sxa commented Aug 26, 2026

Copy link
Copy Markdown
Member

Edit: appears to ✅
The one you linked to (848) was a different test and yours hasn't run through to completion yet yet - the ones I have are:

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

@christianaurichzm

Copy link
Copy Markdown
ContributorAuthor

So I'm not sure we've necessarily proven anything as non of those reproduced the original error :-) Have just kicked off another from the current main on four platforms at https://ci.nodejs.org/view/Stress/job/node-stress-single-test/852

You're right that those runs don't prove much yet. One important difference from the reproducer I used is concurrency.

The race is between the fs.write() that is still in flight and the subsequent readFile() path, which is not ordered after that write has completed. I was able to make it reproduce often enough by running each test file in 24 concurrent processes, with a separate TEST_THREAD_ID per process so they do not share a tmpdir.

Across two Linux x64 environments, the rates varied quite a bit for destroy and were more stable for reopen:

  • test-fastutf8stream-destroy: 25 / 8640 (0.29%) and 34 / 2880 (1.18%)
  • test-fastutf8stream-reopen: 152 / 8640 (1.76%) and 54 / 2880 (1.88%)

Taking the lower destroy rate and assuming roughly independent runs, 100 runs still have about a 75% chance of producing zero failures, and even 1000 runs have about a 5% chance. So 844 coming back clean is an expected outcome rather than evidence against the race, and 853 may well come back clean too without disproving anything. Sequential runs on an otherwise idle worker are also much less favorable for reproducing the contention I was using.

Every failure I observed had the same signature: readFile() returning '' at the assertion this patch reorders. I did not observe another failure mode in the stress runs.

With the patch applied, both tests produced 0 failures in 8640 runs each in the environment that produced the 0.29% and 1.76% baselines, where those rates would correspond to roughly 25 and 152 failures.

The ordering issue itself does not depend on reproducing the flake every time: drain is not a completion barrier for the write being asserted, whereas the write event used here is emitted from #release() after the underlying write operation completes.

Happy to share the stress setup or run other configurations if useful.

@panvapanva added the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 2e8a4b1 into nodejs:mainAug 26, 2026
102 of 103 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 2e8a4b1

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Aug 26, 2026
aduh95 pushed a commit that referenced this pull request Aug 29, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
Both tests read the destination file with no ordering guarantee against
the fs.write() that Utf8Stream still has in flight, so under load the
read can observe an empty file.
In test-fastutf8stream-destroy the read is issued right after destroy().
In test-fastutf8stream-reopen it is ordered on 'drain', documented as
emitted when the buffer has drained enough to allow continued writing,
which says nothing about the bytes being observable in the file. The
reopen path also emits a 'drain' of its own from a nextTick before the
write has landed.
Order both reads on 'write' instead, documented as emitted when a write
operation has completed and emitted from #release() once the underlying
write returned. In sync mode it is emitted from within write(), so the
listener is attached before the write call.
No data is lost by Utf8Stream here: re-reading the file after a failed
assertion shows the expected content. This corrects an expectation of
the tests, not the runtime.
Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: #65554
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-20.md
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.fast-trackPRs proposed for a shorter-than-standard waiting period before landing.flaky-testIssues and PRs involving tests that fail intermittently in CI.needs-ciPRs that need a full CI run.review wantedPRs that need review.testIssues and PRs related to Node.js core tests and test infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@christianaurichzm@nodejs-github-bot@panva@sxa@codebytere