Render a newly declared sitemap URL soon, not a full interval later (v0.74.0) - #177
Conversation
…terval later; v0.74.0
A sitemap CREATE took `Target.put` with no explicit time, so the first render landed at
`getInitialRenderTime` = `hash(url) % interval`: jitter across the target's WHOLE render
interval. On a 48h PDP cadence a product published this morning can wait two days to be
rendered once, while the sitemap has been declaring it the whole time. A declaration is
the strongest signal a site gives that a URL matters.
That jitter exists for a real reason — the first ingest of a large sitemap must not
stampede the queue — but it is sized for bulk population and applies just as hard to the
handful of genuinely new URLs a mature corpus gains each day (tens to hundreds).
New `sitemap.newTargets`:
window (15m) jitter window for a new target's FIRST render. Jittered rather than set
to "now" for exactly the reason the interval jitter exists: a batch must land
across minutes, not in one. 0 disables and restores the old behaviour.
maxPerRun (5000) creates per WALK that may take the fast path; past it, new targets fall
back to full-interval jitter. This is the bulk-population guard — a first
ingest creating hundreds of thousands exceeds it immediately and behaves
exactly as before.
ONLY THE FIRST RENDER MOVES. `Target.put` still files `effectiveInterval` from the
route/stored cadence, so every render after this one is on the normal schedule.
The cap is per WALK, not per child: an index fanning out to 17 children must not multiply
it by 17, so it reads the run's own running total (`fastPathTaken`).
Reported as `createdSoon` on the result, the progress row and the finish log, and as the
`sitemap_created_soon` metric — `created - created_soon` is the overflow that fell back,
which is the number that says whether the cap bound.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a fast-path configuration (newTargets) for newly declared sitemap URLs, allowing their first render to be scheduled within a shorter jitter window (default 15 minutes) instead of the full render interval, up to a configurable cap per walk. Feedback on these changes suggests ensuring the fast path is only applied when the jitter window is shorter than the route's render interval to prevent unintended delays. Additionally, it is recommended to clarify the minimum window size required for effective jitter and enforce a maximum limit of 2147483647 on the window configuration to avoid Node.js timeout overflow warnings.
| // route/stored cadence, so every render after this one is on the normal schedule. | ||
| run.count('created'); | ||
| inflight.push(Target.put(cacheUrl, { renderInterval, sitemapUrl })); | ||
| const fast = newTargetWindow > 0 && run.fastPathTaken() < newTargetCap; |
There was a problem hiding this comment.
If a route has a very short renderInterval (e.g., 5 or 10 minutes) that is shorter than the newTargetWindow (default 15 minutes), using the fast path would actually delay the first render compared to the standard full-interval jitter. Additionally, counting such cases as createdSoon would pollute the metric since no actual acceleration occurred.
Consider ensuring the fast path is only taken when newTargetWindow < renderInterval.
| const fast = newTargetWindow > 0 && run.fastPathTaken() < newTargetCap; | |
| const fast = newTargetWindow > 0 && newTargetWindow < renderInterval && run.fastPathTaken() < newTargetCap; |
| window: option( | ||
| 15 * MINUTE, | ||
| 'Jitter window for a newly declared target’s first render. Small values approximate ' + | ||
| '"immediately" while still spreading a batch across minutes rather than firing it into ' + | ||
| 'one. `0` disables the fast path entirely and restores full-interval jitter.', | ||
| { unit: 'ms', min: 0 } | ||
| ), |
There was a problem hiding this comment.
Since getInitialRenderTime floors the calculated time to the minute (currentMinuteMs), any window value less than 1 * MINUTE (60,000 ms) will not effectively jitter the renders across multiple minutes. Instead, they will almost all collapse into the same minute, potentially causing a queue stampede.
Consider updating the option description to clarify that the window should be at least 2 * MINUTE to achieve multi-minute spreading, or enforce a minimum of 1 * MINUTE (60000) when window > 0.
Additionally, ensure that this configuration option enforces a maximum limit of 2147483647 (the maximum 32-bit signed integer) to prevent potential TimeoutOverflowWarning issues in Node.js if passed to setTimeout.
window: option(
15 * MINUTE,
'Jitter window for a newly declared target’s first render. Small values approximate ' +
'"immediately" while still spreading a batch across minutes rather than firing it into ' +
'one. Values should be at least 2 * MINUTE to ensure effective multi-minute jitter. ' +
'"0" disables the fast path entirely and restores full-interval jitter.',
{ unit: 'ms', min: 0, max: 2147483647 }
),References
- In Node.js, configuration options representing timeouts or delays passed to 'setTimeout' must enforce a maximum limit of 2147483647 (the maximum 32-bit signed integer). Exceeding this limit causes Node.js to emit a 'TimeoutOverflowWarning' and execute the callback immediately (after 1ms), which can lead to unexpected immediate timeouts and system failures.
…ng; v0.77.0 `POST /prerender_admin/explain` exists to answer "why does this URL behave this way", and it could not answer it for the one property operators ask about most. It reported Target.renderInterval and stopped — but that is the CEILING, not the cadence. The value a row is actually scheduled from is resolveEffectiveInterval(url, target), which folds four inputs (route interval, stored interval, default, ladder rung) through two clamps (the route's demandFloor, the route's interval as a ceiling), and the answer is routinely none of the numbers an operator can see. Measured on a production cluster this week: a PDP route configured `renderInterval: 96h` was rendering every 48h. 95.7% of its targets carried a ladder rung, the route's `demandFloor: 48h` equalled the ladder's slowest rung, so `max(rung, floor)` clamped every rung to exactly 48h and the configured 96h ceiling never bound once. The knob named "floor" was the real cadence and the knob named "ceiling" was inert. Establishing that took fifteen explain calls plus reading Target.demandInterval out of the table by hand for 162 URLs and working the algebra backwards — because `demandInterval` was not in explain's select at all. So: select it, and add a `cadence` block reporting the whole chain — every input, which one supplied the base, and what clamped the result. `clampedBy` is the field to read first: 'floor' means the ladder wanted this page faster and the route refused (seeing it across a route means the ladder has no dynamic range there), 'ceiling' means a stored rung is slower than the route allows (what a rung outliving a lowered interval looks like), null means no rung. `explainCadence` lives in util/routeClass.js beside the resolver, not in the admin view that renders it, and derives from the same functions rather than recomputing the algebra. A second implementation would be a second thing to keep correct, and its failure mode is the worst available to a diagnostic: a view that confidently explains a cadence the scheduler is not using. A test cross-checks the two across the input matrix rather than trusting they stay in step. MERGE AFTER #181 (v0.76.0). Open PRs #165, #177 and #178 reserve v0.73.0, v0.74.0 and v0.75.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reviewed as part of the combined train. Trial-merged with #165/#178/#181/#182 — 1148 tests pass together. Two notes, one of which is a number I think needs reconciling before Confirmed safe — I went looking and did not find a problem
1. The creates/day figure is off by ~25× somewhereThe PR body sizes Those can't both be right, and the gap matters: at 134/day the fast path is a rounding error; at 3.4k/day a single walk can approach I did not re-measure; flagging rather than asserting which is right. Two smaller scale notes: 2. Gemini's open inline comment looks validIf a route's effective interval is shorter than 3. Merge mechanics with #165You conflict in five files — Two traps I hit doing this: a union resolve of Also: 🤖 Generated with Claude Code |
…erval Two review guards on the fast path. A `window` WIDER than the route's `renderInterval` made the fast path SLOWER than the jitter it replaces, while still counting as `createdSoon` — so the metric would report an acceleration that did not happen. Not reachable on a corpus whose shortest interval is a day, but the guard is free and the metric has to stay honest. And the option now says what happens below ~2 minutes: `getInitialRenderTime` floors to the minute, so a window under 60s collapses every create in a walk onto ONE minute — the stampede the jitter exists to avoid, reached by asking for less jitter. Capped at 2147483647 for the same reason sweepInterval is: a larger delay is not 'effectively never', it overflows the signed 32-bit timer and fires immediately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both inline notes addressed in 6fef613, pushed to this branch. The window-vs-interval guard is in: The sub-minute note is now in the option description rather than enforced as a minimum, because 1148 tests pass on the branch, lint and format clean. 🤖 Generated with Claude Code |
…et-fast-path # Conflicts: # package-lock.json # packages/plugin/METRICS.md # packages/plugin/package.json # packages/plugin/src/configSchema.js # packages/plugin/src/resources/Sitemap.js
The gap
A sitemap
CREATEcallsTarget.putwith no explicit time, so the first render lands atgetInitialRenderTime:— jitter across the target's whole render interval. On a 48h PDP cadence, a product published this morning can wait two days to be rendered once, while the sitemap has been declaring it the entire time. A declaration is the strongest signal a site gives that a URL matters.
That jitter exists for a real reason: the first ingest of a large sitemap must not stampede the queue. But it's sized for bulk population and applies just as hard to the handful of genuinely new URLs a mature corpus gains each day — measured on one deployment, 65–134 creates/day against ~847k products.
The change
New
sitemap.newTargets:window0disables and restores the old behaviour.maxPerRunJittered across the window, not set to "now" — for precisely the reason the interval jitter exists: a batch of creates must land across minutes rather than firing into one.
The cap is the bulk-population guard. A first ingest creating hundreds of thousands of targets exceeds it immediately, and everything past it takes the old path — so the case the original jitter was written for is unchanged. Steady-state churn never approaches it.
Only the first render moves.
Target.putstill fileseffectiveIntervalfrom the route/stored cadence, so every render after this one is on the normal schedule. This is not a cadence change.The cap is per walk, not per child sitemap — an index fanning out to 17 children must not multiply it by 17 — so it reads the run's own running total via
fastPathTaken().Observability
createdSoonon the result, the progress row and the finish log, plus asitemap_created_soonmetric.created - created_soonis the overflow that fell back to full-interval jitter, which is the number that tells you whether the cap bound.Tests
npm testinpackages/plugin: 1108 pass, 0 fail. Lint andformat:checkclean.time.test.js: over 200 URLs, a 15-minute window confines every first render to the window while a 48h interval spreads far past it — and the windowed times are still spread across many distinct minutes, pinning that this is jitter and not a single instant.sitemapRun.test.js: the cap is per walk and carries across children, andcreatedSoonis a strict subset ofcreatedso the overflow stays readable.Scope
Sitemap creates only. Bot-discovered targets (
handlePageScheduling) still take full-interval jitter — a crawler hitting an unknown URL is a much weaker signal than a site declaring one, and that path has its own discovery gates. Worth revisiting separately.Versioning
v0.74.0, leavingv0.73.0to #165 which is still open. If this should merge first, renumber it to 0.73.0 and bump #165 — merge order has to ascend.🤖 Generated with Claude Code