Skip to content

Commit d14451d

Browse files
committed
Require sustained, restart-free checks in wait
Settle only after stableChecks consecutive checks with full replicas, no active update, and no newly failed tasks. A new task failure keeps the service unsettled for a full stableChecks window, so a container that crash-loops faster than that window can never report success.
1 parent 0a18a01 commit d14451d

1 file changed

Lines changed: 36 additions & 2 deletions

File tree

src/commands/wait-cmd.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@ export async function handler (args: ArgumentsCamelCase) {
2626
assertNumber(timeout, "timeout must be a number in ms");
2727
const interval = args.interval;
2828
assertNumber(interval, "interval must be a number in ms");
29+
const stableChecks = args.stableChecks;
30+
assertNumber(stableChecks, "stableChecks must be a number");
2931

3032
const dockerode = new Docker();
3133

3234
console.log(`Awaiting task reconciliation for ${timeout}ms`);
3335

3436
let services: Service[], tasks: Task[], timedout, bail, serviceStateMap;
37+
let stableStreak = 0;
38+
const seenFailedTaskIds = new Set<string>();
39+
const restartCooldown = new Map<string, number>();
40+
let firstCheck = true;
3541
const start = Date.now();
3642
do {
3743
// To prevent high cpu usage
@@ -55,17 +61,40 @@ export async function handler (args: ArgumentsCamelCase) {
5561
}
5662
}
5763

64+
for (const t of tasks) {
65+
if (!["failed", "rejected"].includes(t.Status.State) || seenFailedTaskIds.has(t.ID)) {
66+
continue;
67+
}
68+
seenFailedTaskIds.add(t.ID);
69+
if (!firstCheck) {
70+
restartCooldown.set(t.ServiceID, stableChecks);
71+
}
72+
}
73+
firstCheck = false;
74+
75+
for (const [serviceId, remaining] of restartCooldown) {
76+
serviceStateMap.set(serviceId, "restarting");
77+
restartCooldown.set(serviceId, remaining - 1);
78+
if (remaining - 1 <= 0) {
79+
restartCooldown.delete(serviceId);
80+
}
81+
}
82+
5883
const servicesUpdating = [...serviceStateMap.entries()];
5984

60-
bail = servicesUpdating.length === 0;
61-
if (!bail) {
85+
if (servicesUpdating.length === 0) {
86+
stableStreak++;
87+
} else {
88+
stableStreak = 0;
6289
for (const [serviceId, state] of servicesUpdating) {
6390
const serviceName = services.find((s) => s.ID === serviceId)?.Spec?.Name;
6491
assert(serviceName != null, "serviceName must be a string");
6592
const errMsg = tasks.find((t) => t.ServiceID === serviceId && t.Status.Err)?.Status.Err;
6693
console.log(`${serviceName} is in ${state} state${errMsg ? ", error: '" + errMsg + "'" : ""}`);
6794
}
6895
}
96+
97+
bail = stableStreak >= stableChecks;
6998
} while (!timedout && !bail);
7099

71100
if (timedout) {
@@ -88,6 +117,11 @@ export function builder (yargs: Argv) {
88117
description: "How often reconciliation should run",
89118
default: 5000,
90119
});
120+
yargs.positional("stableChecks", {
121+
type: "number",
122+
description: "Consecutive settled checks required before success",
123+
default: 3,
124+
});
91125
yargs.hide("help");
92126
yargs.hide("version");
93127
return yargs;

0 commit comments

Comments
 (0)