Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/realm-server/prerender/page-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,25 @@ export class PagePool {
throwIfAborted(signal);
}
if (entry.affinityKey !== affinityKey) {
// The only path that returns an entry tagged for a different
// affinity is the brand-new-affinity cross-affinity-steal
// fallback in `#selectEntryForAffinity`. It runs only when
// `#ensureStandbyPool` couldn't produce a standby — usually a
// transient browser-context creation failure. The reassignment
// below keeps the donor entry's `pageId`, so callers that ask
// "did I get a distinct page from the previous render?" will
// observe equality across two different affinities. Surfacing
// the path here gives CI logs the breadcrumb when that
// assertion trips. Kept at `warn` because hitting this path
// also means we're rendering one affinity's content on a tab
// whose CDP runtime state was warmed for a different affinity.
log.warn(
`cross-affinity steal: reassigning pageId=${entry.pageId} ` +
`from ${entry.affinityKey} to ${affinityKey} ` +
`(standby refill failed to produce a fresh tab; ` +
`standbys=${this.#standbys.size} creating=${this.#creatingStandbys} ` +
`active=${this.#poolEntryCount()} maxPages=${this.#maxPages})`,
);
entry = this.#reassignAffinityTab(entry, affinityKey);
reused = false;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/realm-server/prerender/prerenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,15 @@ export class Prerenderer {
await this.#pagePool.disposeAffinity(affinityKey);
}

// Block until the standby pool has reached its desired count. The
// constructor and `disposeAffinity` both kick refill fire-and-forget;
// tests that need a fresh tab in their *next* `prerenderVisit` call
// (rather than racing the kicked refill) await this instead. Wraps
// `PagePool.warmStandbys`, which dedupes against any in-flight kick.
async warmStandbys(): Promise<void> {
await this.#pagePool.warmStandbys();
}

// Emit the `render cancelled` log line (format from CS-10872)
// and, on a `rendering`-state cancel, tear down the affinity so
// the next request gets a fresh tab rather than one whose
Expand Down
26 changes: 25 additions & 1 deletion packages/realm-server/tests/prerendering-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3494,6 +3494,16 @@ module(basename(__filename), function () {
affinityValue: realmURL3,
}),
]);
// `disposeAffinity` only KICKS standby refill — it doesn't await
// it. If the next test claims a tab before that kick produces a
// standby AND `#ensureStandbyPool`'s awaited retry also can't
// produce one, `#selectEntryForAffinity` falls through to the
// cross-affinity-steal escape hatch and reassigns an idle tab
// from another realm — keeping the donor's `pageId`. The
// "distinct pages per realm" test then sees `r1.pool.pageId ===
// r2.pool.pageId` and fails. Waiting here makes the next
// affinity-pooling test deterministic on the standby path.
await prerenderer.warmStandbys();
};

hooks.before(async function () {
Expand Down Expand Up @@ -4913,10 +4923,24 @@ module(basename(__filename), function () {
url: testCardURL2,
auth: auth(),
});
// When this fails it's almost always the cross-affinity-steal
// fallback in `#selectEntryForAffinity` — `pageId` is equal
// because realm2's `getPage` repurposed realm1's idle tab
// when `#ensureStandbyPool` couldn't conjure a standby. Dump
// both call's `tabStartupMs` / `tabQueueMs` (a steal returns
// `tabStartupMs=0`, the standby path is non-zero) and the
// live queue snapshot so the CI log shows where the standby
// pool was when the race fired.
let queueSnapshot = prerenderer.getQueueDepthSnapshot();
assert.notStrictEqual(
r1.pool.pageId,
r2.pool.pageId,
'distinct pages per realm',
`distinct pages per realm — ` +
`r1.pageId=${r1.pool.pageId} ` +
`r2.pageId=${r2.pool.pageId} ` +
`r1.waits=${JSON.stringify(r1.timings.waits)} ` +
`r2.waits=${JSON.stringify(r2.timings.waits)} ` +
`queueSnapshot=${JSON.stringify(queueSnapshot)}`,
);
assert.false(r1.pool.reused, 'first realm first call not reused');
assert.false(r2.pool.reused, 'second realm first call not reused');
Expand Down
Loading