Skip to content

fix(partners): keep shuffle index in bounds - #9322

Merged
bmuenzenmeyer merged 2 commits into
nodejs:mainfrom
bmuenzenmeyer:fix-partner-shuffle-bounds
Nov 6, 2026
Merged

bmuenzenmeyer merged 2 commits into
nodejs:mainfrom
bmuenzenmeyer:fix-partner-shuffle-bounds

Conversation

@bmuenzenmeyer

@bmuenzenmeyer bmuenzenmeyer commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes an off-by-one in shuffle that intermittently fails the build while prerendering partner pages. The failure depends on when the build runs, not on the code being built.

-    // Normalize to 0-1
-    const randomValue = hash[hashIndex] / 255;
+    // Normalize to [0, 1), since a value of 1 would put `j` out of bounds
+    const randomValue = hash[hashIndex] / 256;

The failing test and the fix are separate commits, so CI shows both states:

Commit Tests job
d691357f1, test only Failed: the 15 new tests, and nothing else
83eea28d4, with the fix Passed

How it happens

  1. generatePartners uses Math.floor(Date.now() / 300000) as the seed, so the clock alone picks the shuffle, in 5-minute windows.
  2. shuffle computes hash[hashIndex] / 255, which is exactly 1 when the byte is 255. That makes j = i + 1.
  3. When i is the last index, on the first pass of the loop, j is one past the end. undefined is swapped in and the array grows from 20 to 21 entries.
  4. On later passes i + 1 is still a valid index, so a 255 byte there can't produce undefined. Of the 200,000 seeds tested, 13,371 hit 255 only on a later pass, and none produced undefined.
  5. new Set() keeps the undefined, and it's still there when the page reads partners. The error depends on the page's code path:
Page Code path Error
/en no category, so it goes straight to partner.id Cannot read properties of undefined (reading 'id')
/en/download category filter Cannot read properties of undefined (reading 'categories')

Validation

CI history

  • Same commit, opposite results. In run 42258999022, Ubuntu finished at 17:14:51 UTC, 9 seconds before the bad window opened, and passed. Windows failed at 17:17:26, inside the window. Re-running Windows at 20:33 passed.
  • Two errors in one window. In run 34397952868, Windows failed on /en/download with 'categories' at 20:00:26 UTC, and Ubuntu failed on /en with 'id' at 20:01:26. Both are in the 20:00 window. The 'categories' failure isn't listed in the issue.

Both failing commits have the same 20-entry weighted partner list as main.

Tests with mocked time

partners.test.mjs pins Date.now to the times of the real failures and to the edges of their windows. Without the fix, the results flip at the millisecond:

Mocked time (UTC, 2026-11-06) Without the fix With the fix
17:14:59.999 pass pass
17:15:00.000 fail pass
17:19:59.999 fail pass
17:20:00.000 pass pass

Of the 22 new tests, 15 fail without the fix and all pass with it. They use a 20-entry fixture because which hash byte gets read depends on the size of the list, so the timestamps stay meaningful if the real partner list changes. array.test.mjs also checks a range of seeds at several list lengths, without relying on hand-picked seeds.

Seed analysis

Using the real shuffle and the real partner list, over 200,000 seeds:

  • "The first-pass byte is 255" predicts undefined with 0 mismatches.
  • 779 seeds are bad (0.3895%, against an expected 1 in 256, or 0.3906%). That's about one 5-minute window every 21 hours.
  • undefined falls inside /en's limit of 12 partners for all 779 seeds, so /en would crash in every one of them.

Real component at a mocked time

Calling the real PartnersList with the props used by each page reproduces the CI errors exactly, and only inside the window:

Time (UTC) Page Without the fix With the fix
17:14:51 /en renders 8 partners renders 8 partners
17:14:51 /en/download renders 3 partners renders 3 partners
17:17:26 /en reading 'id' renders 8 partners
17:17:26 /en/download reading 'categories' renders 3 partners
17:20:00 /en renders 8 partners renders 8 partners
17:20:00 /en/download renders 3 partners renders 3 partners

Real static export builds

These ran node --run deploy locally with NEXT_PUBLIC_STATIC_EXPORT_LOCALE=false, preloading a script through NODE_OPTIONS that overrides only Date.now, so that it stays inside a chosen window:

Code Window Result
Without fix 17:15 (bad) Crashed within about 40 seconds on /en/download/alpha with 'categories'
Without fix 17:10 (clean) Prerendered all 2,332 pages with no errors
With fix 17:15 (bad) No errors through 2,251 of 2,332 pages. The download and partners pages rendered with their partners

Neither of the last two runs is a complete export. The clean run didn't finish copying the export output, and the patched run was stopped before prerendering finished and before it reached /en, so /en's result with the fix comes from the component check above. CI on this PR completed the export with the fix on both Ubuntu and Windows (2,332 of 2,332 pages), though on the real clock and outside a bad window as far as the logs show.

Not checked

  • Deployed pages: pages revalidate every 300 seconds, so this could also trigger at request time. That wasn't tested.
  • Supporters: supportersData.mjs also calls shuffle, so the fix covers it too, but that caller wasn't tested with real data.

Related Issues

Fixes #9321

Check List

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.
  • I have run pnpm format to ensure the code follows the style guide.
  • I have run pnpm test to check if all tests are passing.
  • I have run pnpm build to check if the website builds without errors.
  • I've covered new added functionality with unit tests if necessary.

Commits passed the pre-commit Prettier, ESLint and type checks. With the fix, the apps/site unit tests pass (229 of 229). The remaining items are left for the author to confirm.

🤖 Generated with Claude Code

bmuenzenmeyer and others added 2 commits November 6, 2026 15:08
Pin Date.now to the times of real build failures and to the edges of
their 5-minute shuffle windows. These tests fail until shuffle is fixed:
the same code passes just outside each window and fails inside it.

Refs: nodejs#9321

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dividing a hash byte by 255 gives exactly 1 when the byte is 255, making
`j` equal to `i + 1`. On the first pass of the loop that is one past the
end of the array, so `undefined` is swapped in and crashes prerendering.

The partner shuffle seed changes every 5 minutes, so this failed builds
only during about 1 in 256 of those windows, whatever the code being
built. Dividing by 256 keeps `j` within `[0, i]`.

Fixes: nodejs#9321

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
nodejs-org Ready Ready Preview Nov 6, 2026 9:32pm UTC

Request Review

@codecov

codecov Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.14%. Comparing base (a396117) to head (83eea28).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9322      +/-   ##
==========================================
+ Coverage   86.97%   87.14%   +0.17%     
==========================================
  Files          88       90       +2     
  Lines        6378     6426      +48     
  Branches      386      390       +4     
==========================================
+ Hits         5547     5600      +53     
+ Misses        827      822       -5     
  Partials        4        4              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

📦 Build Size Comparison

Summary

Metric Value
Old Total First Load JS 7.19 MB
New Total First Load JS 7.19 MB
Delta 0 B (0.00%)

@bmuenzenmeyer
bmuenzenmeyer marked this pull request as ready for review November 6, 2026 22:07
@bmuenzenmeyer
bmuenzenmeyer requested a review from a team as a code owner November 6, 2026 22:07
Copilot AI balanced review requested due to automatic review settings November 6, 2026 22:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the shuffle bounds correction and accompanying regression tests.

No open issues 🎉

What changed in this PR

Fixes the shuffle off-by-one behind the intermittent partner-page prerender failures reported in #9321. Dividing hash bytes by 256 keeps the generated index within bounds and prevents undefined entries.

Changes:

  • Corrects hash-byte normalization to produce values in [0, 1).
  • Adds shuffle tests for determinism, input preservation, and permutation integrity across seeds and list sizes.
  • Adds partner-generator regression tests using mocked failure timestamps and five-minute window boundaries.
File Description
apps/​site/​util/​array.ts Keeps shuffle indices within array bounds.
apps/​site/​util/​__tests__/​array.test.mjs Tests shuffle invariants and known failing seeds.
apps/​site/​next-data/​generators/​__tests__/​partners.test.mjs Verifies partner-list integrity across historical failure windows.

Review effort: Balanced

@ovflowd ovflowd added the fast-track Fast Tracking PRs label Nov 6, 2026
@ovflowd

ovflowd commented Nov 6, 2026

Copy link
Copy Markdown
Member

If you need to fast-track, you have my 👍, @bmuenzenmeyer!

@bmuenzenmeyer

Copy link
Copy Markdown
Contributor Author

Thanks - I'd rather have this than keep allowing random failures confuse us and contributors 👍

@bmuenzenmeyer
bmuenzenmeyer added this pull request to the merge queue Nov 6, 2026
Merged via the queue into nodejs:main with commit 95f10a3 Nov 6, 2026
25 of 26 checks passed
@bmuenzenmeyer
bmuenzenmeyer deleted the fix-partner-shuffle-bounds branch November 6, 2026 22:26

This branch was successfully deployed

1 active deployment
Preview — 83eea28d Deployed Nov 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fast-track Fast Tracking PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Intermittent build failure: off-by-one in shuffle adds undefined to the partner list

3 participants