Skip to content

Commit 24ef86b

Browse files
authored
[wrangler] Handle dynamic retry delays in workflows instances describe (#15569)
1 parent 00cb411 commit 24ef86b

3 files changed

Lines changed: 167 additions & 12 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Fix `wrangler workflows instances describe` crashing on dynamic retry delays
6+
7+
The Workflows API serializes function retry delays as `"[dynamic]"`. The describe command previously parsed that as a duration, produced an Invalid Date, and threw `RangeError: Invalid time value` before printing remaining steps. It now renders `unknown (dynamic delay)` and also tolerates attempts whose `end` timestamp is missing.

packages/wrangler/src/__tests__/workflows.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,131 @@ describe("wrangler workflows", () => {
10581058
expect(output.steps[0].output).toEqual({});
10591059
expect(std.out).not.toContain("[...output truncated]");
10601060
});
1061+
1062+
it("should describe a waiting step with dynamic retry delay without crashing", async ({
1063+
expect,
1064+
}) => {
1065+
writeWranglerConfig();
1066+
const mockResponse = {
1067+
end: null,
1068+
output: null,
1069+
params: {},
1070+
queued: mockQueuedDate.toISOString(),
1071+
start: mockStartDate.toISOString(),
1072+
status: "running",
1073+
success: null,
1074+
trigger: {
1075+
source: "unknown",
1076+
},
1077+
versionId: "14707576-2549-4848-82ed-f68f8a1b47c7",
1078+
steps: [
1079+
{
1080+
attempts: [
1081+
{
1082+
end: mockEndDate.toISOString(),
1083+
error: {
1084+
message: "boom",
1085+
name: "Error",
1086+
},
1087+
start: mockStartDate.toISOString(),
1088+
success: false,
1089+
},
1090+
],
1091+
config: {
1092+
retries: {
1093+
backoff: "constant",
1094+
delay: "[dynamic]",
1095+
limit: 3,
1096+
},
1097+
timeout: "30 seconds",
1098+
},
1099+
name: "flaky",
1100+
output: null,
1101+
start: mockStartDate.toISOString(),
1102+
success: null,
1103+
type: "step",
1104+
},
1105+
],
1106+
};
1107+
1108+
msw.use(
1109+
http.get(
1110+
`*/accounts/:accountId/workflows/some-workflow/instances/:instanceId`,
1111+
async () => {
1112+
return HttpResponse.json({
1113+
success: true,
1114+
errors: [],
1115+
messages: [],
1116+
result: mockResponse,
1117+
});
1118+
}
1119+
)
1120+
);
1121+
1122+
await runWrangler(`workflows instances describe some-workflow bar`);
1123+
1124+
expect(std.out).toContain("Retries At: unknown (dynamic delay)");
1125+
expect(std.err).not.toContain("Invalid time value");
1126+
});
1127+
1128+
it("should describe a waiting step with a missing attempt end without crashing", async ({
1129+
expect,
1130+
}) => {
1131+
writeWranglerConfig();
1132+
msw.use(
1133+
http.get(
1134+
`*/accounts/:accountId/workflows/some-workflow/instances/:instanceId`,
1135+
async () => {
1136+
return HttpResponse.json({
1137+
success: true,
1138+
errors: [],
1139+
messages: [],
1140+
result: {
1141+
end: null,
1142+
output: null,
1143+
params: {},
1144+
queued: mockQueuedDate.toISOString(),
1145+
start: mockStartDate.toISOString(),
1146+
status: "running",
1147+
success: null,
1148+
trigger: { source: "unknown" },
1149+
versionId: "14707576-2549-4848-82ed-f68f8a1b47c7",
1150+
steps: [
1151+
{
1152+
attempts: [
1153+
{
1154+
end: null,
1155+
error: { message: "boom", name: "Error" },
1156+
start: mockStartDate.toISOString(),
1157+
success: false,
1158+
},
1159+
],
1160+
config: {
1161+
retries: {
1162+
backoff: "constant",
1163+
delay: "30 seconds",
1164+
limit: 3,
1165+
},
1166+
timeout: "30 seconds",
1167+
},
1168+
name: "flaky",
1169+
output: null,
1170+
start: mockStartDate.toISOString(),
1171+
success: null,
1172+
type: "step",
1173+
},
1174+
],
1175+
},
1176+
});
1177+
}
1178+
)
1179+
);
1180+
1181+
await runWrangler(`workflows instances describe some-workflow bar`);
1182+
1183+
expect(std.out).toContain("Retries At: unknown");
1184+
expect(std.err).not.toContain("Invalid time value");
1185+
});
10611186
});
10621187

10631188
describe("instances send-event", () => {

packages/wrangler/src/workflows/commands/instances/describe.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import assert from "node:assert";
21
import { logRaw } from "@cloudflare/cli-shared-helpers";
32
import { red, white } from "@cloudflare/cli-shared-helpers/colors";
43
import {
@@ -226,19 +225,22 @@ function logStep(
226225

227226
if (step.success === null) {
228227
const latestAttempt = step.attempts.at(-1);
229-
let delay = step.config.retries.delay;
230228
if (latestAttempt !== undefined && latestAttempt.success === false) {
231-
assert(
232-
latestAttempt.end,
233-
"end date always exists in the API for completed attempts"
234-
);
235-
const endDate = new Date(latestAttempt.end);
236-
if (typeof delay === "string") {
237-
delay = ms(delay);
229+
const retryDelayMs = parseRetryDelayMs(step.config.retries.delay);
230+
if (latestAttempt.end == null) {
231+
formattedStep["Retries At"] = "unknown";
232+
} else if (retryDelayMs == null) {
233+
formattedStep["Retries At"] = "unknown (dynamic delay)";
234+
} else {
235+
const retryDate = addMilliseconds(
236+
new Date(latestAttempt.end),
237+
retryDelayMs
238+
);
239+
if (!Number.isNaN(retryDate.getTime())) {
240+
formattedStep["Retries At"] =
241+
`${retryDate.toLocaleString()} (in ${formatDistanceToNowStrict(retryDate)} from now)`;
242+
}
238243
}
239-
const retryDate = addMilliseconds(endDate, delay);
240-
formattedStep["Retries At"] =
241-
`${retryDate.toLocaleString()} (in ${formatDistanceToNowStrict(retryDate)} from now)`;
242244
}
243245
}
244246
}
@@ -300,6 +302,27 @@ function logStep(
300302
}
301303
}
302304

305+
const DYNAMIC_RETRY_DELAY = "[dynamic]";
306+
307+
function parseRetryDelayMs(delay: unknown): number | null {
308+
if (delay === DYNAMIC_RETRY_DELAY) {
309+
return null;
310+
}
311+
312+
if (typeof delay === "number") {
313+
return Number.isFinite(delay) ? delay : null;
314+
}
315+
316+
if (typeof delay === "string") {
317+
const parsed = ms(delay);
318+
return typeof parsed === "number" && Number.isFinite(parsed)
319+
? parsed
320+
: null;
321+
}
322+
323+
return null;
324+
}
325+
303326
function getLastSuccessfulStep(logs: InstanceStatusAndLogs): string | null {
304327
let lastSuccessfulStepName: string | null = null;
305328

0 commit comments

Comments
 (0)