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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/6548-a-pure-read-estimates-when-an-approved-p → main 1 commit fork 1 thread · 1 unresolved

  • Merged. torque/6548-a-pure-read-estimates-when-an-approved-p was merged into main as 34f9833.

Review

Sign in to review this change.
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;

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.

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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs