|
104 | 104 | * the measured span and the arithmetic that rules a fast runner out, so the red |
105 | 105 | * discriminates itself instead of requiring a reader to check §1 and §2 first. |
106 | 106 | * |
| 107 | + * ## [#13691] §3's validity ceiling had to move with §3's conclusion |
| 108 | + * |
| 109 | + * The `span` bound sitting above §3's distinctness assertion is a VALIDITY |
| 110 | + * PRECONDITION, not a claim about the product: it establishes that truncation |
| 111 | + * could not have produced the distinctness §3 then concludes from. Its correct |
| 112 | + * value is therefore whatever the CONCLUSION needs — and #11572 changed the |
| 113 | + * conclusion. |
| 114 | + * |
| 115 | + * #11224's §3 asked for two distinct stamps out of {@link ROUNDS}, and two |
| 116 | + * truncated values need one whole second, so `span < 1000` was the TIGHT bound |
| 117 | + * for it. #11572 strengthened the conclusion to `ROUNDS` distinct stamps — |
| 118 | + * `ROUNDS` multiples of a second cannot span less than `ROUNDS - 1` of them — |
| 119 | + * and wrote exactly that arithmetic into {@link MIN_GAP_MS}'s note in the same |
| 120 | + * commit ("the 5 whole SECONDS second-precision stamps would need to yield |
| 121 | + * `ROUNDS` distinct values"), while leaving the executable bound on the |
| 122 | + * superseded number. From then on this file's prose and its code disagreed by |
| 123 | + * a factor of `ROUNDS - 1`. |
| 124 | + * |
| 125 | + * The cost of that is one-sided, which is what makes it worth a section rather |
| 126 | + * than a comment. An over-strict validity precondition cannot fail in a way |
| 127 | + * that says anything about the product; it can only red on time this test did |
| 128 | + * not have, on somebody else's PR. It did: a merge-queue build on a diff |
| 129 | + * touching no file under `packages/drivers/` reported `span = 1006` — six |
| 130 | + * milliseconds over — while the real invariant (monotonicity, asserted on the |
| 131 | + * line above) HELD, and the same commit passed on a re-queue with nothing |
| 132 | + * changed. Meanwhile #11572's fix spends real wall-clock time inside that same |
| 133 | + * budget by design, so the two guards had come to bound §3 from both sides. |
| 134 | + * |
| 135 | + * {@link MAX_SPAN_MS} is now DERIVED from `ROUNDS` and the truncation grain |
| 136 | + * instead of written as a literal, and the derivation is pinned by arithmetic |
| 137 | + * at the foot of this file, so the two halves cannot drift apart in silence |
| 138 | + * again. ⛔ Nothing #11572 established was traded for it: the clock is still |
| 139 | + * DRIVEN, both gap assertions still run, and the conclusion is still `ROUNDS` |
| 140 | + * distinct stamps. |
| 141 | + * |
107 | 142 | * ## Reverse verification (direction predicted before running) |
108 | 143 | * |
109 | 144 | * Restoring `main`'s `updatedAtStamp()` body turns §1, §2 and §3 red on the |
@@ -155,9 +190,52 @@ const ROUNDS = 6; |
155 | 190 | * SECONDS second-precision stamps would need to yield `ROUNDS` distinct |
156 | 191 | * values — which is what makes §3's distinctness unreachable by truncation |
157 | 192 | * instead of merely unlikely under it. |
| 193 | + * |
| 194 | + * ⚠️ [#13691] `ROUNDS - 1` gaps of this size is the FLOOR of what the spacing |
| 195 | + * costs, never an estimate of it. {@link awaitClockAdvance} waits with |
| 196 | + * `setTimeout`, whose delivery is bounded by the event loop rather than by the |
| 197 | + * clock, so the spacing is `>= 25 ms` and grows with load with no upper bound |
| 198 | + * in the code. Measured on the container #13691 was fixed on, over 300 trials |
| 199 | + * of the five gaps §3 takes: 26 ms median (64 ms max) on an idle loop, and |
| 200 | + * 60 ms median (86 ms max) with the loop held by a re-scheduling 12 ms hog. |
| 201 | + * That is why {@link MAX_SPAN_MS} is derived from the truncation arithmetic and |
| 202 | + * not from this floor — a budget sized on `ROUNDS * MIN_GAP_MS` would be a |
| 203 | + * budget sized on a number the machine is free to exceed. |
158 | 204 | */ |
159 | 205 | const MIN_GAP_MS = 5; |
160 | 206 |
|
| 207 | +/** |
| 208 | + * [#13691] The grain a SECOND-precision stamp truncates to — the defect class |
| 209 | + * §3 exists to catch, on every cell it runs on. |
| 210 | + * |
| 211 | + * One grain covers the whole matrix because the defect has one shape wherever |
| 212 | + * it can occur: on MySQL an unqualified `CURRENT_TIMESTAMP` truncates to whole |
| 213 | + * seconds (#11224 itself), and the SQLite ablation this file's head note |
| 214 | + * prescribes is that same truncation written into the branch that cell |
| 215 | + * executes (`new Date().toISOString()` forced to `.000Z`). Postgres stamps at |
| 216 | + * microsecond precision and has nothing to truncate, so the bound is vacuous |
| 217 | + * there rather than wrong. |
| 218 | + */ |
| 219 | +const TRUNCATED_STAMP_GRAIN_MS = 1_000; |
| 220 | + |
| 221 | +/** |
| 222 | + * [#13691] §3's validity ceiling: the span at or above which second-precision |
| 223 | + * truncation could ITSELF have produced the distinctness §3 concludes from. |
| 224 | + * |
| 225 | + * Derived, not chosen. {@link ROUNDS} truncated stamps are `ROUNDS` multiples |
| 226 | + * of {@link TRUNCATED_STAMP_GRAIN_MS}, so they cannot span less than |
| 227 | + * `ROUNDS - 1` whole grains; below that, truncation cannot reach `ROUNDS` |
| 228 | + * distinct values at all, which is precisely what makes §3's distinctness |
| 229 | + * assertion readable as a statement about resolution. |
| 230 | + * |
| 231 | + * ⛔ This is not a raised ceiling. The literal it replaces was the TIGHT bound |
| 232 | + * for the assertion §3 used to make — see the `[#13691]` section of the head |
| 233 | + * note for the drift, and `maxDistinctUnderTruncation` at the foot of this file |
| 234 | + * for the pin that keeps this constant and §3's conclusion from parting company |
| 235 | + * again. |
| 236 | + */ |
| 237 | +const MAX_SPAN_MS = (ROUNDS - 1) * TRUNCATED_STAMP_GRAIN_MS; |
| 238 | + |
161 | 239 | /** |
162 | 240 | * [#11572] Block until the process clock has ADVANCED by at least `ms`, and |
163 | 241 | * return the advance actually observed. |
@@ -382,13 +460,27 @@ function measure(cell: DialectCell): void { |
382 | 460 | ).toBeGreaterThanOrEqual(MIN_GAP_MS); |
383 | 461 |
|
384 | 462 | // Monotone regardless (the invariant), and — the point — the run spans |
385 | | - // less than the full second a truncated stamp would need to distinguish |
386 | | - // any two of these at all. |
| 463 | + // less than the `ROUNDS - 1` whole grains a truncated stamp would need to |
| 464 | + // reach the `ROUNDS` distinct values concluded below. See |
| 465 | + // {@link MAX_SPAN_MS} for why that is `ROUNDS - 1` grains and not one. |
387 | 466 | for (let i = 1; i < stamps.length; i++) expect(stamps[i]).toBeGreaterThanOrEqual(stamps[i - 1]); |
388 | 467 | const span = stamps[stamps.length - 1] - stamps[0]; |
389 | | - expect(span, 'this run took over a second, so second-precision stamps could have differed too').toBeLessThan( |
390 | | - 1_000, |
391 | | - ); |
| 468 | + // [#13691] Reported in the failure, because the first question a reader |
| 469 | + // has is which half of the budget went where — and neither half is a |
| 470 | + // product signal. |
| 471 | + const deliberate = gaps.reduce((total, gap) => total + gap, 0); |
| 472 | + expect( |
| 473 | + span, |
| 474 | + `[#13691] §3's VALIDITY PRECONDITION failed, which says nothing about the product: the ` + |
| 475 | + `run spanned ${span} ms, at or above the ${MAX_SPAN_MS} ms (${ROUNDS} - 1 grains of ` + |
| 476 | + `${TRUNCATED_STAMP_GRAIN_MS} ms) a second-precision stamp would need to reach the ` + |
| 477 | + `${ROUNDS} distinct values asserted below — so truncation is no longer ruled out and ` + |
| 478 | + `the distinctness below is unreadable, whichever way it lands. Of that span, ` + |
| 479 | + `${deliberate} ms was clock-advance spacing this cell spent ON PURPOSE ` + |
| 480 | + `(${gaps.join('/')} ms) and ${span - deliberate} ms was everything else (round-trips ` + |
| 481 | + `and scheduling). The ordering invariant asserted on the line above is unaffected ` + |
| 482 | + `either way — read this as a slow runner, not as a regression.`, |
| 483 | + ).toBeLessThan(MAX_SPAN_MS); |
392 | 484 |
|
393 | 485 | // The property, now entailed rather than raced: with every update spaced |
394 | 486 | // by a measured gap five times the declared resolution, a stamp that kept |
@@ -479,3 +571,61 @@ function measure(cell: DialectCell): void { |
479 | 571 | for (const cell of DIALECT_CELLS) { |
480 | 572 | declareDialectCell(cell, 'update stamp precision (#11224)', measure); |
481 | 573 | } |
| 574 | + |
| 575 | +/** |
| 576 | + * [#13691] The most distinct values SECOND-precision truncation can show across |
| 577 | + * a span of `ms` — the model {@link MAX_SPAN_MS} is derived from. |
| 578 | + * |
| 579 | + * Truncated stamps are multiples of {@link TRUNCATED_STAMP_GRAIN_MS}, so a span |
| 580 | + * of `ms` covers at most `floor(ms / grain) + 1` of them. |
| 581 | + */ |
| 582 | +function maxDistinctUnderTruncation(ms: number): number { |
| 583 | + return Math.floor(ms / TRUNCATED_STAMP_GRAIN_MS) + 1; |
| 584 | +} |
| 585 | + |
| 586 | +// ── The coupling #11572 broke, asserted instead of commented ───────────────── |
| 587 | +// |
| 588 | +// §3's ceiling and §3's conclusion are two halves of one argument, and they |
| 589 | +// drifted apart in silence: #11572 strengthened the conclusion from "at least |
| 590 | +// two distinct" to "`ROUNDS` distinct" and left the ceiling on the number that |
| 591 | +// had been tight for the OLD conclusion. Nothing failed at the time — the |
| 592 | +// precondition merely became `ROUNDS - 1` times stricter than its own argument |
| 593 | +// needed, and stayed that way until a slow runner paid for it on an unrelated |
| 594 | +// PR. |
| 595 | +// |
| 596 | +// So the relationship is pinned. It runs on every runner with no server and no |
| 597 | +// clock: this is arithmetic about the defect class, not a measurement of the |
| 598 | +// machine, and it is the assertion that reds if the ceiling is ever put back on |
| 599 | +// a literal belonging to a superseded conclusion. |
| 600 | +describe("#13691 — §3's span ceiling is the tight bound for §3's conclusion", () => { |
| 601 | + it('rules truncation out below the ceiling, and is not stricter than that above it', () => { |
| 602 | + // SOUND — one millisecond under the ceiling, truncation cannot reach the |
| 603 | + // `ROUNDS` distinct values §3 concludes from. This is the whole reason the |
| 604 | + // precondition exists, and it is the leg a LOOSENED ceiling reds. |
| 605 | + expect( |
| 606 | + maxDistinctUnderTruncation(MAX_SPAN_MS - 1), |
| 607 | + `a span of ${MAX_SPAN_MS - 1} ms must leave second-precision truncation unable to reach ` + |
| 608 | + `${ROUNDS} distinct stamps, or §3's distinctness assertion concludes nothing`, |
| 609 | + ).toBeLessThan(ROUNDS); |
| 610 | + |
| 611 | + // TIGHT — at the ceiling truncation CAN reach them, so the ceiling is not |
| 612 | + // stricter than the argument requires. This is the leg a ceiling restored |
| 613 | + // to `1_000` reds, and over-strictness here is not conservatism: a validity |
| 614 | + // precondition cannot fail informatively, so every millisecond of slack it |
| 615 | + // demands is a false red carrying no product signal. |
| 616 | + expect( |
| 617 | + maxDistinctUnderTruncation(MAX_SPAN_MS), |
| 618 | + `the ceiling is ${MAX_SPAN_MS} ms, but second-precision truncation still cannot reach ` + |
| 619 | + `${ROUNDS} distinct stamps at that span — so the precondition is stricter than §3's own ` + |
| 620 | + `argument needs, and the excess can only ever red on time the run did not have`, |
| 621 | + ).toBeGreaterThanOrEqual(ROUNDS); |
| 622 | + }); |
| 623 | + |
| 624 | + it("is the number #11224's superseded conclusion needed, one grain for two stamps", () => { |
| 625 | + // Why `1_000` was right until #11572 and wrong after it, kept executable so |
| 626 | + // the head note's history is checkable rather than remembered: the original |
| 627 | + // §3 asked for TWO distinct stamps, and two need exactly one grain. |
| 628 | + expect(maxDistinctUnderTruncation(TRUNCATED_STAMP_GRAIN_MS - 1)).toBe(1); |
| 629 | + expect(maxDistinctUnderTruncation(TRUNCATED_STAMP_GRAIN_MS)).toBe(2); |
| 630 | + }); |
| 631 | +}); |
0 commit comments