Skip to content

Commit 10d93ee

Browse files
committed
refactor(listener): let stock's mail hold replace the manager ready window
The listener mails a just-deployed manager at once. Interchange's hub-sessions sidecar handler parks mail to a run whose deploy is not recorded, redrives it when the deploy records the run's key, and drops it after HUB_MAIL_HOLD_TTL_MS. The listener's ready window, wake poll and its unwoken, retry and unwokenAgain steps are gone, as are the listener and manager waits #713 published in release.json. HUB_MAIL_HOLD_TTL_MS keeps its fifteen-minute default and is the one source for the hub's start grace, the time a manager may sit on its sidecar unstarted, the hold on a new deploy ask and the time a queued head may wait to start, so a failed manager start is asked again once the hold lapses.
1 parent 8f6e849 commit 10d93ee

32 files changed

Lines changed: 288 additions & 367 deletions

‎.env.example‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,12 @@ HUB_DATA_DIR=./tmp/hub-data
5252
# TRIAGE_RECONCILE_INTERVAL_MS=
5353
# Milliseconds before a pass that could not reach a listener tries again, doubling to two minutes. Default 15000.
5454
# TRIAGE_RECONCILE_RETRY_MS=
55-
# Milliseconds a listener waits for a just-deployed manager to reach a sidecar before it gives up on it. Default 120000.
56-
# TRIAGE_MANAGER_READY_MS=
57-
# Milliseconds a just-deployed manager may have no sidecar and still hold its pull request. Default 120000.
58-
# TRIAGE_MANAGER_START_GRACE_MS=
59-
# Milliseconds a manager waiting on CI or a reviewer waits between checks. Default 30000.
60-
# TRIAGE_MANAGER_POLL_MS=
61-
# Milliseconds between a listener's checks on whether its manager reached a sidecar. Default 2000.
62-
# TRIAGE_WAKE_POLL_MS=
6355

6456
# Pull request heads the reconciler lets a tenant have queued or running at once. Default 5.
6557
# TRIAGE_MAX_IN_FLIGHT=
6658
# Milliseconds after a head is capped by runs that never started before it gets a fresh set of attempts. Default 21600000.
6759
# TRIAGE_CAPPED_RETRY_MS=
68-
# Milliseconds without a new webhook event before a pull request's events go out as one triage run; at most four times this after the first. Default 30000.
60+
# Milliseconds without a new webhook event before a pull request's events go out as one triage run; at most four times this after the first, which must stay under HUB_MAIL_HOLD_TTL_MS. Default 30000.
6961
# TRIAGE_COALESCE_QUIET_MS=
7062
# Milliseconds a pull request may wait for Triage to start before the hub logs triage_stalled, shows it in the portal and emails the workspace's owners and admins. Default 300000.
7163
# TRIAGE_STALLED_AFTER_MS=
@@ -75,6 +67,8 @@ HUB_DATA_DIR=./tmp/hub-data
7567
# Interchange settings. Unset keeps the default shown: Interchange's stock default, except
7668
# HUB_MAIL_HOLD_TTL_MS, HUB_MAIL_ACK_RETRY_INTERVAL_MS and HUB_MAIL_ACK_MAX_RETRIES,
7769
# which the hub raises so routed triage mail survives a long pack receive.
70+
# HUB_MAIL_HOLD_TTL_MS is also how long a just-deployed manager that has not come up keeps
71+
# its pull request, and how long a queued head may wait to start before it is asked again.
7872
# Durations are whole numbers ending in s, m, h or d; the max lifetime and max disconnected
7973
# must be above zero.
8074
# HUB_MAX_TARBALL_BYTES=10485760

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ jobs:
9292
matrix:
9393
include:
9494
- shard: 1/5
95-
files: tests/chain/unwoken.test.ts
95+
files: tests/chain/unstarted.test.ts
9696
- shard: 2/5
9797
files: tests/chain/woken.test.ts tests/chain/local-sidecars.test.ts tests/chain/deterministic.test.ts tests/chain/unset.test.ts
9898
- shard: 3/5

‎apps/hub/src/env.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { LifecycleDuration, lifecycleDurationMs, type ResolvedWorkflowLifecycleP
66
import { DEFAULT_GITHUB_API_ORIGIN } from "./github/github-app-credential-adapter.js";
77
import type { CoalesceWindow } from "./github/coalescer.js";
88
import { DEFAULT_RECONCILE_POLICY, type ReconcilePolicy } from "./github/reconcile-plan.js";
9+
import { DEFAULT_MAIL_HOLD_MS } from "./timings.js";
910

1011
const secret = type("string").narrow((value, ctx) =>
1112
/^[0-9a-fA-F]{64}$/.test(value) || ctx.mustBe("32 bytes encoded as 64 hexadecimal characters"));
@@ -73,10 +74,6 @@ const HubEnvSchema = type({
7374
"RAILWAY_GIT_REPO_OWNER?": "string",
7475
"RAILWAY_GIT_REPO_NAME?": "string",
7576
"RAILWAY_ENVIRONMENT_ID?": "string",
76-
"TRIAGE_MANAGER_READY_MS?": positiveInteger,
77-
"TRIAGE_MANAGER_START_GRACE_MS?": positiveInteger,
78-
"TRIAGE_MANAGER_POLL_MS?": positiveInteger,
79-
"TRIAGE_WAKE_POLL_MS?": positiveInteger,
8077
"TRIAGE_RECONCILE_INTERVAL_MS?": positiveInteger,
8178
"TRIAGE_RECONCILE_RETRY_MS?": positiveInteger,
8279
"TRIAGE_COALESCE_QUIET_MS?": positiveInteger,
@@ -274,9 +271,11 @@ export function migrationEnv(db: DatabaseConfig): Record<string, string> {
274271
}
275272

276273
/** Heads the reconciler lets a tenant have queued or running at once. */
277-
export function triageReconcilePolicy(env: Pick<HubEnv, "TRIAGE_MAX_IN_FLIGHT" | "TRIAGE_CAPPED_RETRY_MS">): ReconcilePolicy {
274+
/** A queued head counts as never started once stock's mail hold has dropped its manager's wake, `mailHoldMs`. */
275+
export function triageReconcilePolicy(env: Pick<HubEnv, "TRIAGE_MAX_IN_FLIGHT" | "TRIAGE_CAPPED_RETRY_MS">, mailHoldMs: number): ReconcilePolicy {
278276
return {
279277
...DEFAULT_RECONCILE_POLICY,
278+
unstartedAfterMs: mailHoldMs,
280279
maxInFlight: numberOr(env.TRIAGE_MAX_IN_FLIGHT, DEFAULT_RECONCILE_POLICY.maxInFlight),
281280
cappedRetryAfterMs: numberOr(env.TRIAGE_CAPPED_RETRY_MS, DEFAULT_RECONCILE_POLICY.cappedRetryAfterMs),
282281
};
@@ -386,9 +385,10 @@ export function interchangeSettings(env: HubEnv): InterchangeSettings {
386385
...(env.HUB_API_ORIGIN !== undefined && { hubApiOrigin: env.HUB_API_ORIGIN }),
387386
// Triage mail is routed, not dispatched, so only the router's ack retries keep it alive. A
388387
// pack receive holds the connection queue for minutes and the stock 5 x 10 s give-up drops it.
388+
// The mail hold is the hub's start grace for a just-deployed manager too, and how long a queued head may wait to start.
389389
sidecarLink: {
390390
...(env.HUB_SIDECAR_REQUEST_TIMEOUT_MS !== undefined && { requestTimeoutMs: Number(env.HUB_SIDECAR_REQUEST_TIMEOUT_MS) }),
391-
mailHoldTTLMs: numberOr(env.HUB_MAIL_HOLD_TTL_MS, 15 * 60_000),
391+
mailHoldTTLMs: numberOr(env.HUB_MAIL_HOLD_TTL_MS, DEFAULT_MAIL_HOLD_MS),
392392
mailAckRetryIntervalMs: numberOr(env.HUB_MAIL_ACK_RETRY_INTERVAL_MS, 30_000),
393393
mailAckMaxRetries: numberOr(env.HUB_MAIL_ACK_MAX_RETRIES, 10),
394394
},

‎apps/hub/src/github/reconcile-plan.test.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { DEFAULT_RECONCILE_POLICY, planTenant, pullKey, requeue, runKey, type Ob
55
const MINUTE = 60_000;
66
const T0 = new Date("2026-10-07T00:00:00.000Z");
77
const policy = DEFAULT_RECONCILE_POLICY;
8+
/** Minutes a queued head may wait to start. */
9+
const UNSTARTED = policy.unstartedAfterMs / MINUTE;
810

911
function at(minutes: number): Date {
1012
return new Date(T0.getTime() + minutes * MINUTE);
@@ -77,8 +79,8 @@ describe("reconcile plan", () => {
7779

7880
test("a queued mail lost before any run started is queued again", () => {
7981
const queued = plan([pr(1)], [], at(0)).rows;
80-
expect(enqueued(plan([pr(1)], queued, at(9)))).toEqual([]);
81-
expect(enqueued(plan([pr(1)], queued, at(11)))).toEqual([1]);
82+
expect(enqueued(plan([pr(1)], queued, at(UNSTARTED - 1)))).toEqual([]);
83+
expect(enqueued(plan([pr(1)], queued, at(UNSTARTED + 1)))).toEqual([1]);
8284
});
8385

8486
test("failed runs retry with backoff and stop at the attempt cap", () => {
@@ -160,7 +162,7 @@ describe("reconcile plan", () => {
160162
const queued = plan([pr(1)], [], at(0)).rows;
161163
const result = plan([pr(1)], queued, at(3), [[pr(1), [{ ...run("run_a", "failed", 0), startedAt: at(-0.5).toISOString() }]]]);
162164
expect(status(result, 1)).toMatchObject({ status: "failed", runId: "run_a", attempts: 1, error: "run failed" });
163-
const before = plan([pr(1)], queued, at(11), [[pr(1), [run("run_old", "failed", -5)]]]);
165+
const before = plan([pr(1)], queued, at(UNSTARTED + 1), [[pr(1), [run("run_old", "failed", -5)]]]);
164166
expect(status(before, 1)).toMatchObject({ status: "queued", attempts: 2 });
165167
});
166168

@@ -295,8 +297,8 @@ describe("a head whose manager was refused for capacity", () => {
295297
const placed = plan([pr(1)], waiting, at(31));
296298
expect(enqueued(placed)).toEqual([]);
297299
expect(status(placed, 1)).toEqual(expect.not.objectContaining({ error: expect.anything() }));
298-
expect(status(plan([pr(1)], placed.rows, at(41)), 1)).toMatchObject({ status: "queued", attempts: 1 });
299-
expect(status(plan([pr(1)], placed.rows, at(42)), 1)).toMatchObject({ status: "queued", attempts: 2 });
300+
expect(status(plan([pr(1)], placed.rows, at(31 + UNSTARTED)), 1)).toMatchObject({ status: "queued", attempts: 1 });
301+
expect(status(plan([pr(1)], placed.rows, at(32 + UNSTARTED)), 1)).toMatchObject({ status: "queued", attempts: 2 });
300302
});
301303
});
302304

‎apps/hub/src/github/reconcile-plan.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// requests to queue again. Pure, so every recovery path is decided in one place.
33
import { PR_TRIAGE_STUCK_RUN_MS, WAITING_FOR_CAPACITY, type AwaitedStage, type PrTriageRow, type RepoSource } from "@corbits/triage-contracts";
44
import { MODEL_NOT_ASKED } from "@corbits/triage-workflows/logic/settled";
5+
import { DEFAULT_MAIL_HOLD_MS } from "../timings.js";
56

67
/** `labels` only where the host lists them. */
78
export type OpenPr = { number: number; headSha: string; updatedAt: string; draft: boolean; labels?: string[] };
@@ -53,7 +54,7 @@ const MINUTE = 60_000;
5354

5455
export const DEFAULT_RECONCILE_POLICY: ReconcilePolicy = {
5556
webhookGraceMs: 2 * MINUTE,
56-
unstartedAfterMs: 10 * MINUTE,
57+
unstartedAfterMs: DEFAULT_MAIL_HOLD_MS,
5758
stuckAfterMs: PR_TRIAGE_STUCK_RUN_MS,
5859
clockSkewMs: MINUTE,
5960
backoffBaseMs: 5 * MINUTE,

‎apps/hub/src/github/triage-reconciler.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { CheckRun } from "@corbits/github-reads";
55
import { unsettledBy } from "@corbits/triage-workflows/logic/settled";
66
import { DEFAULT_RECONCILE_POLICY, planTenant, runKey, type ObservedRun, type OpenPr } from "./reconcile-plan.js";
77
import { triageReconcilePolicy } from "../env.js";
8+
import { DEFAULT_MAIL_HOLD_MS } from "../timings.js";
89
import { TriageStateConflictError, type TriageStateStore } from "./triage-state-store.js";
910
import { ListenerUnroutedError, type LiveDeployment } from "./deployment.js";
1011
import type { HostedVerdict } from "./verdict-record-store.js";
@@ -154,7 +155,7 @@ function harness(runs: Runs | (() => Runs), prs: OpenPr[] | Record<string, OpenP
154155
delivered.push({ payload: payload as Record<string, unknown> });
155156
},
156157
unroutedTries,
157-
policy: triageReconcilePolicy({ ...(options.maxInFlight !== undefined && { TRIAGE_MAX_IN_FLIGHT: options.maxInFlight }) }),
158+
policy: triageReconcilePolicy({ ...(options.maxInFlight !== undefined && { TRIAGE_MAX_IN_FLIGHT: options.maxInFlight }) }, DEFAULT_MAIL_HOLD_MS),
158159
now,
159160
log: (entry) => logged.push(entry),
160161
});

‎apps/hub/src/interchange-hub.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
// the hub, so an ended run's sidecar has nothing left to keep. `sidecarLink`
1313
// is spread into createSidecarRouter with longer mail hold and ack retries
1414
// than the router's defaults: triage mail is routed, not dispatched, so only
15-
// those retries keep it alive while a pack receive holds the connection.
15+
// those retries keep it alive while a pack receive holds the connection. The
16+
// hub's start grace for a just-deployed manager is that same mail hold.
1617
// - `adapterManifest` replaces stock's `sidecarAdapterManifest` and has no
1718
// SIDECAR_ADAPTER_MANIFEST fallback: the hub's adapters are fixed in
1819
// sidecar-config.ts, not operator configuration.

‎apps/hub/src/manager-intake.postgres.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createPauseGate } from "./github/pr-workflow.js";
1010
import { createManagerIntake, createManagerWake, createOverdueWake, createSupersede } from "./manager-intake.js";
1111
import { ListenerUnroutedError, RunUnavailableError } from "./github/deployment.js";
1212
import { recordEvent } from "./manager-state.js";
13+
import { timings } from "./timings.js";
1314
import { migrateTriageTables } from "./triage-db.js";
1415

1516
const TEST_DB = { host: "localhost", port: 5432, user: "postgres", password: "postgres", database: "interchange" };
@@ -282,7 +283,7 @@ test("PostgreSQL: the reconcile pass wakes a manager for a pull request whose re
282283
expect(logged).toEqual([{ level: "info", msg: "manager_woken_overdue", tenantId: TENANT, source: "github", repo: RECORD.name, number: 7, head: "h1" }]);
283284

284285
// A manager the wake started took the claim and went away before done; past the start grace the next pass wakes again.
285-
const earlier = new Date(Date.now() - 3 * 60_000);
286+
const earlier = new Date(Date.now() - timings().mailHoldMs - 60_000);
286287
await db.execute(sql`UPDATE corbits_triage.manager_state SET state = state || ${JSON.stringify({ overdueWokenAt: earlier.toISOString(), deployAskedAt: earlier.toISOString(), claimedAt: new Date(earlier.getTime() + 1_000).toISOString() })}::jsonb WHERE key = ${`mgr:github:${RECORD.name}#7`}`);
287288
await wakeOverdue(TENANT);
288289
expect(mailed).toEqual([woken, woken]);
@@ -332,7 +333,7 @@ test("PostgreSQL: an overdue wake is recorded only once it succeeded, and one pu
332333

333334
test("PostgreSQL: a manager deployed without a sidecar past the startup window is not mailed, and the listener is", async () => {
334335
await withDatabase(async function stuck(db) {
335-
await claimManager(db, "run_stuck", 7, "deployed", new Date(Date.now() - 3 * 60_000));
336+
await claimManager(db, "run_stuck", 7, "deployed", new Date(Date.now() - timings().mailHoldMs - 60_000));
336337
const { toListener, mailed } = stack(db, LISTENER);
337338
expect(await toListener(TENANT, requeued(7, "h1"))).toBe(true);
338339
expect(mailed).toEqual([{ address: LISTENER, payload: { kind: "pr", repo: RECORD.name, tenantId: TENANT, items: [{ prNumber: 7, headSha: "h1" }] } }]);

‎apps/hub/src/manager-routes.postgres.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { MAX_BODY_BYTES } from "./read-json.js";
1616
import { askDeploy, managedPulls, passRequest, recordEvent, recordRefused, type PullRequestRef } from "./manager-state.js";
1717
import { createVerdictRecordStore } from "./github/verdict-record-store.js";
1818
import { mountTenantRoutes } from "./tenant-routes.js";
19+
import { timings } from "./timings.js";
1920
import { migrateTriageTables } from "./triage-db.js";
2021

2122
const TEST_DB = { host: "localhost", port: 5432, user: "postgres", password: "postgres", database: "interchange" };
@@ -243,7 +244,7 @@ test("PostgreSQL: a result whose wake fails stays recorded with nothing else, th
243244
const wakeOverdue = createOverdueWake({ db, wake: waker(woken), log });
244245
await wakeOverdue(TENANT);
245246
expect(woken).toEqual([]);
246-
const earlier = new Date(Date.now() - 2 * 60_000 - 1_000).toISOString();
247+
const earlier = new Date(Date.now() - timings().mailHoldMs - 1_000).toISOString();
247248
await db.execute(sql`UPDATE corbits_triage.manager_state SET state = jsonb_set(state, '{receivedAt}', to_jsonb(${earlier}::text)) WHERE key LIKE '%:run_reviewer'`);
248249
await wakeOverdue(TENANT);
249250
expect(woken).toEqual([{ tenantId: TENANT, pr: GITHUB, head: "h1" }]);

‎apps/hub/src/manager-state.postgres.test.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { sql } from "drizzle-orm";
55
import { createDB, runMigrations, schema } from "@intx/db";
66
import type { Tx } from "./github/admin-github-grant.js";
77
import { done, endNeverRan, liveListener, liveManager, notePlaced, recordEvent, recordResult, take, type PullRequestRef } from "./manager-state.js";
8+
import { timings } from "./timings.js";
89
import { migrateTriageTables } from "./triage-db.js";
910

1011
const TEST_DB = { host: "localhost", port: 5432, user: "postgres", password: "postgres", database: "interchange" };
@@ -402,7 +403,7 @@ async function claimRun(db: Db, runId: string): Promise<void> {
402403
test("PostgreSQL: a manager deployed without a sidecar past the startup window does not hold the pull request", async () => {
403404
await withDatabase(async function stuck(db) {
404405
await recordEvent(db, TENANT, GITHUB, "d1", webhook("h1"));
405-
await deployedRun(db, "run_stuck", new Date(Date.now() - 3 * MINUTE));
406+
await deployedRun(db, "run_stuck", new Date(Date.now() - timings().mailHoldMs - MINUTE));
406407
await claimRun(db, "run_stuck");
407408
await liveRun(db, "run_next");
408409
expect(await take(db, TENANT, GITHUB, "run_next")).toMatchObject({ outcome: "pass", pass: { head: "h1" } });
@@ -422,7 +423,7 @@ test("PostgreSQL: a manager deployed inside the startup window, or one with a si
422423
await liveRun(db, "run_next");
423424
expect(await take(db, TENANT, GITHUB, "run_next")).toEqual({ outcome: "refused" });
424425

425-
await db.update(schema.workflowRun).set({ createdAt: new Date(Date.now() - 3 * MINUTE) }).where(sql`${schema.workflowRun.id} = 'run_fresh'`);
426+
await db.update(schema.workflowRun).set({ createdAt: new Date(Date.now() - timings().mailHoldMs - MINUTE) }).where(sql`${schema.workflowRun.id} = 'run_fresh'`);
426427
await db.insert(schema.sidecar).values({
427428
id: "sdc_up",
428429
tokenHashSha256: new Uint8Array(32).fill(7),
@@ -453,7 +454,7 @@ test("PostgreSQL: a manager being cancelled is mailed nothing and takes nothing,
453454
await withDatabase(async function cancelling(db) {
454455
await recordEvent(db, TENANT, GITHUB, "d1", webhook("h1"));
455456
await startManager(db, GITHUB, "run_cancelling");
456-
await db.update(schema.workflowRun).set({ cancellationRequestedAt: new Date(), cancellationDeadline: new Date(), cancellationReason: "unwoken" }).where(sql`${schema.workflowRun.id} = 'run_cancelling'`);
457+
await db.update(schema.workflowRun).set({ cancellationRequestedAt: new Date(), cancellationDeadline: new Date(), cancellationReason: "stopped" }).where(sql`${schema.workflowRun.id} = 'run_cancelling'`);
457458
await liveRun(db, "run_next");
458459
expect(await liveManager(db, TENANT, GITHUB)).toBeUndefined();
459460
expect(await take(db, TENANT, GITHUB, "run_cancelling")).toEqual({ outcome: "refused" });
@@ -469,14 +470,14 @@ async function allocate(db: Db, runId: string, fields: { status: "allocated"; si
469470
await db.insert(schema.sidecarAllocation).values({ id: `alc_${runId}`, anchorRunId: runId, tenantId: TENANT, provisionerId: "local-process", provisionerApiVersion: 2, provisionerBindingFingerprint: "fp", adapterManifestDigest: "digest", maxDisconnectedMs: 60_000, ...fields });
470471
}
471472

472-
test("PostgreSQL: a manager on its sidecar five minutes without running gives up the pull request, and the reaper ends it but never the listener", async () => {
473+
test("PostgreSQL: a manager on its sidecar past the mail hold without running gives up the pull request, and the reaper ends it but never the listener", async () => {
473474
await withDatabase(async function neverRan(db) {
474475
await db.insert(schema.workflowDefinition).values({ id: "wfd_listener", tenantId: TENANT, name: "triage-listener-github" });
475476
await db.insert(schema.workflowRun).values({ id: "run_listener", anchorRunId: "run_listener", definitionId: "wfd_listener", tenantId: TENANT, status: "deployed", createdAt: new Date(Date.now() - 60 * MINUTE) });
476477
await allocate(db, "run_listener", { status: "allocated", sidecarId: "sdc_local" });
477-
await deployedRun(db, "run_quiet", new Date(Date.now() - 6 * MINUTE));
478+
await deployedRun(db, "run_quiet", new Date(Date.now() - timings().mailHoldMs - MINUTE));
478479
await allocate(db, "run_quiet", { status: "allocated", sidecarId: "sdc_local" });
479-
notePlaced({ allocationId: "alc_run_quiet", generation: 0 }, new Date(Date.now() - 6 * MINUTE));
480+
notePlaced({ allocationId: "alc_run_quiet", generation: 0 }, new Date(Date.now() - timings().mailHoldMs - MINUTE));
480481
await claimRun(db, "run_quiet");
481482
const cancelled: string[][] = [];
482483
const logs: Record<string, unknown>[] = [];
@@ -491,9 +492,9 @@ test("PostgreSQL: a manager on its sidecar five minutes without running gives up
491492
});
492493
}, 120_000);
493494

494-
test("PostgreSQL: a manager that waited past five minutes for a slot is timed from its placement, not its deploy", async () => {
495+
test("PostgreSQL: a manager that waited past the mail hold for a slot is timed from its placement, not its deploy", async () => {
495496
await withDatabase(async function placedLate(db) {
496-
await deployedRun(db, "run_late", new Date(Date.now() - 10 * MINUTE));
497+
await deployedRun(db, "run_late", new Date(Date.now() - timings().mailHoldMs - 5 * MINUTE));
497498
await allocate(db, "run_late", { status: "allocated", sidecarId: "sdc_local" });
498499
notePlaced({ allocationId: "alc_run_late", generation: 0 }, new Date(Date.now() - MINUTE));
499500
await claimRun(db, "run_late");

0 commit comments

Comments
 (0)