Skip to content

Commit f91c8a9

Browse files
authored
fix: validate invalid createTimer inputs (#288)
1 parent 672a7ef commit f91c8a9

2 files changed

Lines changed: 301 additions & 5 deletions

File tree

packages/durabletask-js/src/worker/runtime-orchestration-context.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -309,13 +309,29 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
309309
createTimer(fireAt: number | Date): Task<any> {
310310
const id = this.nextSequenceNumber();
311311

312-
// If a number is passed, we use it as the number of seconds to wait
313-
// we use instanceof Date as number is not a native Javascript type
314-
if (!(fireAt instanceof Date)) {
315-
fireAt = new Date(this._currentUtcDatetime.getTime() + fireAt * 1000);
312+
let fireAtDate: Date;
313+
if (typeof fireAt === "number") {
314+
if (!Number.isFinite(fireAt)) {
315+
throw new Error(
316+
`createTimer requires a finite number (seconds) or a valid Date, but received ${String(fireAt)}`,
317+
);
318+
}
319+
fireAtDate = new Date(this._currentUtcDatetime.getTime() + fireAt * 1000);
320+
} else if (fireAt instanceof Date) {
321+
fireAtDate = fireAt;
322+
} else {
323+
throw new Error(
324+
`createTimer requires a finite number (seconds) or a valid Date, but received ${String(fireAt)}`,
325+
);
326+
}
327+
328+
if (Number.isNaN(fireAtDate.getTime())) {
329+
throw new Error(
330+
"createTimer received or produced an invalid Date (NaN timestamp)",
331+
);
316332
}
317333

318-
const action = ph.newCreateTimerAction(id, fireAt);
334+
const action = ph.newCreateTimerAction(id, fireAtDate);
319335
this._pendingActions[action.getId()] = action;
320336

321337
const timerTask = new CompletableTask();
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { OrchestrationContext } from "../src/task/context/orchestration-context";
5+
import {
6+
newExecutionStartedEvent,
7+
newOrchestratorStartedEvent,
8+
} from "../src/utils/pb-helper.util";
9+
import { OrchestrationExecutor, OrchestrationExecutionResult } from "../src/worker/orchestration-executor";
10+
import * as pb from "../src/proto/orchestrator_service_pb";
11+
import { Registry } from "../src/worker/registry";
12+
import { TOrchestrator } from "../src/types/orchestrator.type";
13+
import { NoOpLogger } from "../src/types/logger.type";
14+
15+
const testLogger = new NoOpLogger();
16+
const TEST_INSTANCE_ID = "test-createtimer-validation";
17+
18+
/**
19+
* Helper to extract the CompleteOrchestrationAction from an OrchestrationExecutionResult
20+
*/
21+
function getCompleteAction(
22+
result: OrchestrationExecutionResult,
23+
): pb.CompleteOrchestrationAction | undefined {
24+
const completeActions = result.actions.filter((a) => a.hasCompleteorchestration());
25+
expect(completeActions.length).toEqual(1);
26+
return completeActions[0].getCompleteorchestration() ?? undefined;
27+
}
28+
29+
describe("createTimer input validation", () => {
30+
it("should reject NaN as fireAt value", async () => {
31+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
32+
yield ctx.createTimer(NaN);
33+
};
34+
35+
const registry = new Registry();
36+
const name = registry.addOrchestrator(orchestrator);
37+
const startTime = new Date("2025-01-01T00:00:00Z");
38+
const newEvents = [
39+
newOrchestratorStartedEvent(startTime),
40+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
41+
];
42+
const executor = new OrchestrationExecutor(registry, testLogger);
43+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
44+
45+
const completeAction = getCompleteAction(result);
46+
expect(completeAction?.getOrchestrationstatus()).toEqual(
47+
pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED,
48+
);
49+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("createTimer");
50+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("NaN");
51+
});
52+
53+
it("should reject Infinity as fireAt value", async () => {
54+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
55+
yield ctx.createTimer(Infinity);
56+
};
57+
58+
const registry = new Registry();
59+
const name = registry.addOrchestrator(orchestrator);
60+
const startTime = new Date("2025-01-01T00:00:00Z");
61+
const newEvents = [
62+
newOrchestratorStartedEvent(startTime),
63+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
64+
];
65+
const executor = new OrchestrationExecutor(registry, testLogger);
66+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
67+
68+
const completeAction = getCompleteAction(result);
69+
expect(completeAction?.getOrchestrationstatus()).toEqual(
70+
pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED,
71+
);
72+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("createTimer");
73+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("Infinity");
74+
});
75+
76+
it("should reject negative Infinity as fireAt value", async () => {
77+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
78+
yield ctx.createTimer(-Infinity);
79+
};
80+
81+
const registry = new Registry();
82+
const name = registry.addOrchestrator(orchestrator);
83+
const startTime = new Date("2025-01-01T00:00:00Z");
84+
const newEvents = [
85+
newOrchestratorStartedEvent(startTime),
86+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
87+
];
88+
const executor = new OrchestrationExecutor(registry, testLogger);
89+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
90+
91+
const completeAction = getCompleteAction(result);
92+
expect(completeAction?.getOrchestrationstatus()).toEqual(
93+
pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED,
94+
);
95+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("createTimer");
96+
});
97+
98+
it("should reject an invalid Date object", async () => {
99+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
100+
yield ctx.createTimer(new Date("not a date"));
101+
};
102+
103+
const registry = new Registry();
104+
const name = registry.addOrchestrator(orchestrator);
105+
const startTime = new Date("2025-01-01T00:00:00Z");
106+
const newEvents = [
107+
newOrchestratorStartedEvent(startTime),
108+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
109+
];
110+
const executor = new OrchestrationExecutor(registry, testLogger);
111+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
112+
113+
const completeAction = getCompleteAction(result);
114+
expect(completeAction?.getOrchestrationstatus()).toEqual(
115+
pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED,
116+
);
117+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("createTimer");
118+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("invalid Date");
119+
});
120+
121+
it("should reject values that are neither numbers nor Date objects", async () => {
122+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
123+
yield ctx.createTimer("not a timer" as unknown as number);
124+
};
125+
126+
const registry = new Registry();
127+
const name = registry.addOrchestrator(orchestrator);
128+
const startTime = new Date("2025-01-01T00:00:00Z");
129+
const newEvents = [
130+
newOrchestratorStartedEvent(startTime),
131+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
132+
];
133+
const executor = new OrchestrationExecutor(registry, testLogger);
134+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
135+
136+
const completeAction = getCompleteAction(result);
137+
expect(completeAction?.getOrchestrationstatus()).toEqual(
138+
pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED,
139+
);
140+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("createTimer");
141+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("not a timer");
142+
});
143+
144+
it("should reject a finite number that produces an invalid Date", async () => {
145+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
146+
yield ctx.createTimer(Number.MAX_VALUE);
147+
};
148+
149+
const registry = new Registry();
150+
const name = registry.addOrchestrator(orchestrator);
151+
const startTime = new Date("2025-01-01T00:00:00Z");
152+
const newEvents = [
153+
newOrchestratorStartedEvent(startTime),
154+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
155+
];
156+
const executor = new OrchestrationExecutor(registry, testLogger);
157+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
158+
159+
const completeAction = getCompleteAction(result);
160+
expect(completeAction?.getOrchestrationstatus()).toEqual(
161+
pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED,
162+
);
163+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("createTimer");
164+
expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("invalid Date");
165+
});
166+
167+
it("should accept a valid positive number (seconds)", async () => {
168+
const delaySeconds = 30;
169+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
170+
yield ctx.createTimer(delaySeconds);
171+
};
172+
173+
const registry = new Registry();
174+
const name = registry.addOrchestrator(orchestrator);
175+
const startTime = new Date("2025-01-01T00:00:00Z");
176+
const expectedFireAt = new Date(startTime.getTime() + delaySeconds * 1000);
177+
const newEvents = [
178+
newOrchestratorStartedEvent(startTime),
179+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
180+
];
181+
const executor = new OrchestrationExecutor(registry, testLogger);
182+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
183+
184+
// Should have a createTimer action (not failed)
185+
const timerActions = result.actions.filter((a) => a.hasCreatetimer());
186+
expect(timerActions.length).toEqual(1);
187+
const fireAt = timerActions[0].getCreatetimer()?.getFireat()?.toDate();
188+
expect(fireAt?.getTime()).toEqual(expectedFireAt.getTime());
189+
});
190+
191+
it("should accept zero as a valid delay (fires immediately)", async () => {
192+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
193+
yield ctx.createTimer(0);
194+
};
195+
196+
const registry = new Registry();
197+
const name = registry.addOrchestrator(orchestrator);
198+
const startTime = new Date("2025-01-01T00:00:00Z");
199+
const newEvents = [
200+
newOrchestratorStartedEvent(startTime),
201+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
202+
];
203+
const executor = new OrchestrationExecutor(registry, testLogger);
204+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
205+
206+
// Zero is a valid delay — timer fires at the current orchestration time
207+
const timerActions = result.actions.filter((a) => a.hasCreatetimer());
208+
expect(timerActions.length).toEqual(1);
209+
const fireAt = timerActions[0].getCreatetimer()?.getFireat()?.toDate();
210+
expect(fireAt?.getTime()).toEqual(startTime.getTime());
211+
});
212+
213+
it("should accept a valid Date object", async () => {
214+
const futureDate = new Date("2025-06-15T12:00:00Z");
215+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
216+
yield ctx.createTimer(futureDate);
217+
};
218+
219+
const registry = new Registry();
220+
const name = registry.addOrchestrator(orchestrator);
221+
const startTime = new Date("2025-01-01T00:00:00Z");
222+
const newEvents = [
223+
newOrchestratorStartedEvent(startTime),
224+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
225+
];
226+
const executor = new OrchestrationExecutor(registry, testLogger);
227+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
228+
229+
const timerActions = result.actions.filter((a) => a.hasCreatetimer());
230+
expect(timerActions.length).toEqual(1);
231+
const fireAt = timerActions[0].getCreatetimer()?.getFireat()?.toDate();
232+
expect(fireAt?.getTime()).toEqual(futureDate.getTime());
233+
});
234+
235+
it("should accept a negative number (timer fires in the past, which the sidecar handles)", async () => {
236+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
237+
yield ctx.createTimer(-5);
238+
};
239+
240+
const registry = new Registry();
241+
const name = registry.addOrchestrator(orchestrator);
242+
const startTime = new Date("2025-01-01T00:00:00Z");
243+
const expectedFireAt = new Date(startTime.getTime() - 5000);
244+
const newEvents = [
245+
newOrchestratorStartedEvent(startTime),
246+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
247+
];
248+
const executor = new OrchestrationExecutor(registry, testLogger);
249+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
250+
251+
// Negative delays create a timer in the past — sidecar fires it immediately.
252+
// This is valid behavior for scenarios like conditional timer creation.
253+
const timerActions = result.actions.filter((a) => a.hasCreatetimer());
254+
expect(timerActions.length).toEqual(1);
255+
const fireAt = timerActions[0].getCreatetimer()?.getFireat()?.toDate();
256+
expect(fireAt?.getTime()).toEqual(expectedFireAt.getTime());
257+
});
258+
259+
it("should accept fractional seconds", async () => {
260+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext) {
261+
yield ctx.createTimer(1.5);
262+
};
263+
264+
const registry = new Registry();
265+
const name = registry.addOrchestrator(orchestrator);
266+
const startTime = new Date("2025-01-01T00:00:00Z");
267+
const expectedFireAt = new Date(startTime.getTime() + 1500);
268+
const newEvents = [
269+
newOrchestratorStartedEvent(startTime),
270+
newExecutionStartedEvent(name, TEST_INSTANCE_ID),
271+
];
272+
const executor = new OrchestrationExecutor(registry, testLogger);
273+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
274+
275+
const timerActions = result.actions.filter((a) => a.hasCreatetimer());
276+
expect(timerActions.length).toEqual(1);
277+
const fireAt = timerActions[0].getCreatetimer()?.getFireat()?.toDate();
278+
expect(fireAt?.getTime()).toEqual(expectedFireAt.getTime());
279+
});
280+
});

0 commit comments

Comments
 (0)