Public: anyone can read this; the crew can change it.
A pure read estimates when an approved pull lands, from its place in the queue and the recent landing pace #6561
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/6548-a-pure-read-estimates-when-an-approved-p → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +106
−0
src/flow/landing_eta.ts added +49
| @@ -0,0 +1,49 @@ | ||
| 1 | /** | |
| 2 | * When an approved pull lands (#6548, part of #5930): a pure read from its | |
| 3 | * place in the merge queue and the recent landing pace, so an approved pull | |
| 4 | * says how long it will wait and nobody pokes it. | |
| 5 | * | |
| 6 | * The pull lands after the `ahead` pulls in front of it and then itself: | |
| 7 | * | |
| 8 | * minutes = (ahead + 1) × per landing | |
| 9 | * per landing = 60 / landedLastHour when anything landed in the last hour | |
| 10 | * = medianGateMinutes otherwise, one gate after another | |
| 11 | * | |
| 12 | * The observed pace wins over the gate time when both are known: it already | |
| 13 | * counts batching, red gates and the queue's idle gaps. With neither there is | |
| 14 | * no estimate (null). Plain data in, plain data out; no clock read. | |
| 15 | */ | |
| 16 | ||
| 17 | export interface LandingEtaInput { | |
| 18 | /** Pulls in front of this one in the queue; 0 is first in line. */ | |
| 19 | readonly ahead: number; | |
| 20 | /** Pulls the queue landed in the last hour. */ | |
| 21 | readonly landedLastHour: number; | |
| 22 | /** The median merge-queue gate in minutes, or null when none is known. */ | |
| 23 | readonly medianGateMinutes: number | null; | |
| 24 | } | |
| 25 | ||
| 26 | const count = (n: number): number => (Number.isFinite(n) && n > 0 ? Math.floor(n) : 0); | |
| 27 | ||
| 28 | /** Minutes until the pull lands, or null when nothing landed in the last hour and no gate time is known. */ | |
| 29 | export function landingEta(input: LandingEtaInput): number | null { | |
| 30 | const slots = count(input.ahead) + 1; | |
| 31 | const landed = Number.isFinite(input.landedLastHour) && input.landedLastHour > 0 ? input.landedLastHour : 0; | |
| 32 | if (landed > 0) return (slots * 60) / landed; | |
| 33 | const gate = input.medianGateMinutes; | |
| 34 | if (gate !== null && Number.isFinite(gate) && gate > 0) return slots * gate; | |
| 35 | return null; | |
| 36 | } | |
| 37 | ||
| 38 | /** | |
| 39 | * The estimate in words. Under an hour it rounds to the nearest 5 min (at | |
| 40 | * least 5): "lands in about 40 min"; from 57.5 min on, to the nearest hour | |
| 41 | * (at least 1): "lands in about 3 h". An estimate is not a promise, so it | |
| 42 | * says no more precision than the pace can carry. | |
| 43 | */ | |
| 44 | export function etaSays(minutes: number | null): string { | |
| 45 | if (minutes === null || !Number.isFinite(minutes) || minutes < 0) return "no estimate: nothing landed in the last hour"; | |
| 46 | const five = Math.max(5, Math.round(minutes / 5) * 5); | |
| 47 | if (five < 60) return `lands in about ${five} min`; | |
| 48 | return `lands in about ${Math.max(1, Math.round(minutes / 60))} h`; | |
| 49 | } |
test/flow-landing-eta.test.ts added +57
| @@ -0,0 +1,57 @@ | ||
| 1 | /** When an approved pull lands (#6548): its place in the queue times the recent landing pace, in words. */ | |
| 2 | import { describe, expect, test } from "bun:test"; | |
| 3 | import { etaSays, landingEta } from "../src/flow/landing_eta.ts"; | |
| 4 | ||
| 5 | describe("landingEta", () => { | |
| 6 | test("first in line waits one landing at the last hour's pace", () => { | |
| 7 | // 6 landed in the last hour: one every 60 / 6 = 10 min. | |
| 8 | expect(landingEta({ ahead: 0, landedLastHour: 6, medianGateMinutes: 12 })).toBe(10); | |
| 9 | }); | |
| 10 | ||
| 11 | test("deep in line waits for every pull ahead and then itself", () => { | |
| 12 | // 11 ahead + itself = 12 landings × 60 / 4 = 180 min. | |
| 13 | expect(landingEta({ ahead: 11, landedLastHour: 4, medianGateMinutes: 8 })).toBe(180); | |
| 14 | }); | |
| 15 | ||
| 16 | test("a stalled queue falls back to one gate after another", () => { | |
| 17 | // Nothing landed: 2 ahead + itself = 3 gates × 14 min = 42 min. | |
| 18 | expect(landingEta({ ahead: 2, landedLastHour: 0, medianGateMinutes: 14 })).toBe(42); | |
| 19 | }); | |
| 20 | ||
| 21 | test("a stalled queue with no gate time has no estimate", () => { | |
| 22 | expect(landingEta({ ahead: 0, landedLastHour: 0, medianGateMinutes: null })).toBeNull(); | |
| 23 | expect(landingEta({ ahead: 3, landedLastHour: 0, medianGateMinutes: 0 })).toBeNull(); | |
| 24 | }); | |
| 25 | ||
| 26 | test("nonsense counts are read as nothing, never as a negative wait", () => { | |
| 27 | expect(landingEta({ ahead: -4, landedLastHour: 3, medianGateMinutes: null })).toBe(20); | |
| 28 | expect(landingEta({ ahead: Number.NaN, landedLastHour: Number.NaN, medianGateMinutes: 10 })).toBe(10); | |
| 29 | }); | |
| 30 | }); | |
| 31 | ||
| 32 | describe("etaSays", () => { | |
| 33 | test("under an hour rounds to the nearest 5 min", () => { | |
| 34 | expect(etaSays(40)).toBe("lands in about 40 min"); | |
| 35 | expect(etaSays(42)).toBe("lands in about 40 min"); | |
| 36 | expect(etaSays(42.5)).toBe("lands in about 45 min"); | |
| 37 | expect(etaSays(1)).toBe("lands in about 5 min"); | |
| 38 | expect(etaSays(0)).toBe("lands in about 5 min"); | |
| 39 | }); | |
| 40 | ||
| 41 | test("an hour and more rounds to the nearest hour", () => { | |
| 42 | expect(etaSays(57.5)).toBe("lands in about 1 h"); | |
| 43 | expect(etaSays(89)).toBe("lands in about 1 h"); | |
| 44 | expect(etaSays(180)).toBe("lands in about 3 h"); | |
| 45 | expect(etaSays(209)).toBe("lands in about 3 h"); | |
| 46 | expect(etaSays(210)).toBe("lands in about 4 h"); | |
| 47 | }); | |
| 48 | ||
| 49 | test("no estimate says why", () => { | |
| 50 | expect(etaSays(null)).toBe("no estimate: nothing landed in the last hour"); | |
| 51 | }); | |
| 52 | ||
| 53 | test("the pieces agree end to end", () => { | |
| 54 | expect(etaSays(landingEta({ ahead: 11, landedLastHour: 4, medianGateMinutes: null }))).toBe("lands in about 3 h"); | |
| 55 | expect(etaSays(landingEta({ ahead: 3, landedLastHour: 6, medianGateMinutes: null }))).toBe("lands in about 40 min"); | |
| 56 | }); | |
| 57 | }); |
new above
offline
The finite-and-positive check on landedLastHour repeats the logic in
count. Share one helper, and keep the fractional value here, since a pace of 0.5 an hour is legitimate.