Skip to content

Commit a65c120

Browse files
committed
Adopt docker-stack-wait logic in wait command
1 parent 76509c5 commit a65c120

1 file changed

Lines changed: 22 additions & 51 deletions

File tree

src/commands/wait-cmd.ts

Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -26,76 +26,52 @@ 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");
3129

3230
const dockerode = new Docker();
3331

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

36-
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;
34+
const reportedStates = new Map<string, string>();
35+
let services: Service[], tasks: Task[], timedout, settled, unrecoverable;
4136
const start = Date.now();
4237
do {
4338
// To prevent high cpu usage
4439
await timers.setTimeout(interval);
4540
// Calculate timedout
4641
timedout = Date.now() - timeout > start;
4742

48-
serviceStateMap = new Map<string, string>();
4943
services = await dockerode.listServices({filters: {label: [`com.docker.stack.namespace=${appName}`]}});
5044
tasks = await dockerode.listTasks({filters: {"label": [`com.docker.stack.namespace=${appName}`]}}) as Task[];
5145

46+
settled = true;
47+
unrecoverable = false;
5248
for (const s of services) {
49+
const serviceName = s.Spec?.Name;
50+
assert(serviceName != null, "serviceName must be a string");
51+
const updateState = s.UpdateStatus?.State ?? "deployed";
52+
const stalled = updateState === "paused" || updateState.startsWith("rollback_");
5353
const runningTasks = tasks.filter((t) => t.Status.State === "running" && t.ServiceID === s.ID);
5454
const desiredReplicas = s.Spec?.Mode?.Replicated?.Replicas ?? 0;
55+
const state = !stalled && runningTasks.length < desiredReplicas ? `replicating ${runningTasks.length}/${desiredReplicas}` : updateState;
5556

56-
// Always check replica count first - compare running tasks against desired replicas
57-
if (runningTasks.length < desiredReplicas) {
58-
serviceStateMap.set(s.ID, "replicating");
59-
} else if (s.UpdateStatus?.State && ["updating", "paused", "rollback_started", "rollback_paused"].includes(s.UpdateStatus.State)) {
60-
serviceStateMap.set(s.ID, s.UpdateStatus.State);
61-
}
62-
}
63-
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);
57+
if (reportedStates.get(s.ID) !== state) {
58+
const errMsg = tasks.find((t) => t.ServiceID === s.ID && t.Status.Err)?.Status.Err;
59+
console.log(`${serviceName} is in ${state} state${errMsg ? ", error: '" + errMsg + "'" : ""}`);
60+
reportedStates.set(s.ID, state);
7161
}
72-
}
73-
firstCheck = false;
7462

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);
63+
if (stalled) {
64+
unrecoverable = true;
65+
} else if (!["deployed", "completed"].includes(state)) {
66+
settled = false;
8067
}
8168
}
69+
} while (!timedout && !unrecoverable && !settled);
8270

83-
const servicesUpdating = [...serviceStateMap.entries()];
84-
85-
if (servicesUpdating.length === 0) {
86-
stableStreak++;
87-
} else {
88-
stableStreak = 0;
89-
for (const [serviceId, state] of servicesUpdating) {
90-
const serviceName = services.find((s) => s.ID === serviceId)?.Spec?.Name;
91-
assert(serviceName != null, "serviceName must be a string");
92-
const errMsg = tasks.find((t) => t.ServiceID === serviceId && t.Status.Err)?.Status.Err;
93-
console.log(`${serviceName} is in ${state} state${errMsg ? ", error: '" + errMsg + "'" : ""}`);
94-
}
95-
}
96-
97-
bail = stableStreak >= stableChecks;
98-
} while (!timedout && !bail);
71+
if (unrecoverable) {
72+
console.error("This deployment will not complete");
73+
process.exit(1);
74+
}
9975

10076
if (timedout) {
10177
console.error("Reconciliation timed out");
@@ -117,11 +93,6 @@ export function builder (yargs: Argv) {
11793
description: "How often reconciliation should run",
11894
default: 5000,
11995
});
120-
yargs.positional("stableChecks", {
121-
type: "number",
122-
description: "Consecutive settled checks required before success",
123-
default: 3,
124-
});
12596
yargs.hide("help");
12697
yargs.hide("version");
12798
return yargs;

0 commit comments

Comments
 (0)