Skip to content

Commit 05ab1da

Browse files
authored
Fix falsy values (0, empty string, false, null) silently dropped during serialization (#138)
1 parent 0c36ca9 commit 05ab1da

4 files changed

Lines changed: 401 additions & 6 deletions

File tree

packages/durabletask-js/src/worker/activity-executor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class ActivityExecutor {
4444
}
4545

4646
// Return the output
47-
const encodedOutput = activityOutput ? JSON.stringify(activityOutput) : undefined;
47+
const encodedOutput = activityOutput !== undefined ? JSON.stringify(activityOutput) : undefined;
4848

4949
// Log activity completion (EventId 604)
5050
WorkerLogs.activityCompleted(this._logger, orchestrationId, name);

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
211211

212212
let resultJson;
213213

214-
if (result) {
214+
if (result !== undefined) {
215215
resultJson = isResultEncoded ? result : JSON.stringify(result);
216216
}
217217

@@ -256,7 +256,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
256256
// replayed when the new instance starts
257257
for (const [eventName, values] of Object.entries(this._receivedEvents)) {
258258
for (const eventValue of values) {
259-
const encodedValue = eventValue ? JSON.stringify(eventValue) : undefined;
259+
const encodedValue = eventValue !== undefined ? JSON.stringify(eventValue) : undefined;
260260
carryoverEvents.push(ph.newEventRaisedEvent(eventName, encodedValue));
261261
}
262262
}
@@ -265,7 +265,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
265265
const action = ph.newCompleteOrchestrationAction(
266266
this.nextSequenceNumber(),
267267
pb.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW,
268-
this._newInput ? JSON.stringify(this._newInput) : undefined,
268+
this._newInput !== undefined ? JSON.stringify(this._newInput) : undefined,
269269
undefined,
270270
carryoverEvents,
271271
);
@@ -316,7 +316,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
316316
): Task<TOutput> {
317317
const id = this.nextSequenceNumber();
318318
const name = typeof activity === "string" ? activity : getName(activity);
319-
const encodedInput = input ? JSON.stringify(input) : undefined;
319+
const encodedInput = input !== undefined ? JSON.stringify(input) : undefined;
320320
const action = ph.newScheduleTaskAction(id, name, encodedInput, options?.tags, options?.version);
321321
this._pendingActions[action.getId()] = action;
322322

@@ -347,7 +347,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
347347
instanceId = `${this._instanceId}:${instanceIdSuffix}`;
348348
}
349349

350-
const encodedInput = input ? JSON.stringify(input) : undefined;
350+
const encodedInput = input !== undefined ? JSON.stringify(input) : undefined;
351351
const action = ph.newCreateSubOrchestrationAction(id, name, instanceId, encodedInput, options?.tags, options?.version);
352352
this._pendingActions[action.getId()] = action;
353353

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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 } 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+
import { ActivityContext } from "../src/task/context/activity-context";
15+
import { ActivityExecutor } from "../src/worker/activity-executor";
16+
17+
const testLogger = new NoOpLogger();
18+
const TEST_INSTANCE_ID = "falsy-test-instance";
19+
20+
describe("Falsy input serialization", () => {
21+
describe("callActivity with falsy inputs", () => {
22+
it.each([
23+
{ input: 0, label: "zero" },
24+
{ input: "", label: "empty string" },
25+
{ input: false, label: "false" },
26+
{ input: null, label: "null" },
27+
])("should correctly serialize $label as activity input", async ({ input }) => {
28+
const myActivity = async (_ctx: ActivityContext, actInput: any) => actInput;
29+
30+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
31+
const result = yield ctx.callActivity(myActivity, input as any);
32+
return result;
33+
};
34+
35+
const registry = new Registry();
36+
const orchestratorName = registry.addOrchestrator(orchestrator);
37+
registry.addActivity(myActivity);
38+
39+
const newEvents = [
40+
newOrchestratorStartedEvent(new Date()),
41+
newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID),
42+
];
43+
44+
const executor = new OrchestrationExecutor(registry, testLogger);
45+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
46+
47+
// Should have a ScheduleTask action with the serialized input
48+
const scheduleAction = result.actions.find((a) => a.hasScheduletask());
49+
expect(scheduleAction).toBeDefined();
50+
const inputValue = scheduleAction!.getScheduletask()!.getInput();
51+
expect(inputValue).toBeDefined();
52+
expect(inputValue!.getValue()).toEqual(JSON.stringify(input));
53+
});
54+
});
55+
56+
describe("callSubOrchestrator with falsy inputs", () => {
57+
it.each([
58+
{ input: 0, label: "zero" },
59+
{ input: "", label: "empty string" },
60+
{ input: false, label: "false" },
61+
{ input: null, label: "null" },
62+
])("should correctly serialize $label as sub-orchestration input", async ({ input }) => {
63+
const subOrchestrator: TOrchestrator = async (_ctx: OrchestrationContext, subInput: any) => {
64+
return subInput;
65+
};
66+
67+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
68+
const result = yield ctx.callSubOrchestrator(subOrchestrator, input as any);
69+
return result;
70+
};
71+
72+
const registry = new Registry();
73+
const orchestratorName = registry.addOrchestrator(orchestrator);
74+
registry.addOrchestrator(subOrchestrator);
75+
76+
const newEvents = [
77+
newOrchestratorStartedEvent(new Date()),
78+
newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID),
79+
];
80+
81+
const executor = new OrchestrationExecutor(registry, testLogger);
82+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
83+
84+
// Should have a CreateSubOrchestration action with the serialized input
85+
const subOrchAction = result.actions.find((a) => a.hasCreatesuborchestration());
86+
expect(subOrchAction).toBeDefined();
87+
const inputValue = subOrchAction!.getCreatesuborchestration()!.getInput();
88+
expect(inputValue).toBeDefined();
89+
expect(inputValue!.getValue()).toEqual(JSON.stringify(input));
90+
});
91+
});
92+
93+
describe("orchestration completion with falsy results", () => {
94+
it.each([
95+
{ result: 0, label: "zero" },
96+
{ result: "", label: "empty string" },
97+
{ result: false, label: "false" },
98+
{ result: null, label: "null" },
99+
])("should correctly serialize $label as orchestration result", async ({ result }) => {
100+
const orchestrator: TOrchestrator = async (_ctx: OrchestrationContext) => {
101+
return result;
102+
};
103+
104+
const registry = new Registry();
105+
const orchestratorName = registry.addOrchestrator(orchestrator);
106+
107+
const newEvents = [
108+
newOrchestratorStartedEvent(new Date()),
109+
newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID),
110+
];
111+
112+
const executor = new OrchestrationExecutor(registry, testLogger);
113+
const execResult = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
114+
115+
expect(execResult.actions.length).toEqual(1);
116+
const completeAction = execResult.actions[0].getCompleteorchestration();
117+
expect(completeAction).toBeDefined();
118+
expect(completeAction!.getOrchestrationstatus()).toEqual(
119+
pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED,
120+
);
121+
const resultValue = completeAction!.getResult();
122+
expect(resultValue).toBeDefined();
123+
expect(resultValue!.getValue()).toEqual(JSON.stringify(result));
124+
});
125+
});
126+
127+
describe("continueAsNew with falsy input", () => {
128+
it("should correctly serialize zero as continue-as-new input", async () => {
129+
const orchestrator: TOrchestrator = async (ctx: OrchestrationContext, input: any): Promise<any> => {
130+
if (input === 0) {
131+
ctx.continueAsNew(0, false);
132+
return;
133+
}
134+
return input;
135+
};
136+
137+
const registry = new Registry();
138+
const orchestratorName = registry.addOrchestrator(orchestrator);
139+
140+
const newEvents = [
141+
newOrchestratorStartedEvent(new Date()),
142+
newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID, JSON.stringify(0)),
143+
];
144+
145+
const executor = new OrchestrationExecutor(registry, testLogger);
146+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
147+
148+
expect(result.actions.length).toEqual(1);
149+
const completeAction = result.actions[0].getCompleteorchestration();
150+
expect(completeAction).toBeDefined();
151+
expect(completeAction!.getOrchestrationstatus()).toEqual(
152+
pb.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW,
153+
);
154+
const resultValue = completeAction!.getResult();
155+
expect(resultValue).toBeDefined();
156+
expect(resultValue!.getValue()).toEqual(JSON.stringify(0));
157+
});
158+
});
159+
160+
describe("undefined inputs are still treated as no input", () => {
161+
it("should not set input when activity input is undefined", async () => {
162+
const myActivity = async (_ctx: ActivityContext) => "done";
163+
164+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
165+
const result = yield ctx.callActivity(myActivity);
166+
return result;
167+
};
168+
169+
const registry = new Registry();
170+
const orchestratorName = registry.addOrchestrator(orchestrator);
171+
registry.addActivity(myActivity);
172+
173+
const newEvents = [
174+
newOrchestratorStartedEvent(new Date()),
175+
newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID),
176+
];
177+
178+
const executor = new OrchestrationExecutor(registry, testLogger);
179+
const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents);
180+
181+
const scheduleAction = result.actions.find((a) => a.hasScheduletask());
182+
expect(scheduleAction).toBeDefined();
183+
// Input should be undefined when not provided
184+
const inputValue = scheduleAction!.getScheduletask()!.getInput();
185+
expect(inputValue).toBeUndefined();
186+
});
187+
});
188+
189+
describe("activity output with falsy values", () => {
190+
it.each([
191+
{ output: 0, label: "zero" },
192+
{ output: "", label: "empty string" },
193+
{ output: false, label: "false" },
194+
{ output: null, label: "null" },
195+
])("should correctly serialize $label as activity output", async ({ output }) => {
196+
const myActivity = async (_ctx: ActivityContext) => output;
197+
198+
const registry = new Registry();
199+
registry.addActivity(myActivity);
200+
201+
const executor = new ActivityExecutor(registry, testLogger);
202+
const result = await executor.execute(TEST_INSTANCE_ID, "myActivity", 1);
203+
204+
expect(result).toEqual(JSON.stringify(output));
205+
});
206+
207+
it("should return undefined when activity output is undefined", async () => {
208+
const myActivity = async (_ctx: ActivityContext) => undefined;
209+
210+
const registry = new Registry();
211+
registry.addActivity(myActivity);
212+
213+
const executor = new ActivityExecutor(registry, testLogger);
214+
const result = await executor.execute(TEST_INSTANCE_ID, "myActivity", 1);
215+
216+
expect(result).toBeUndefined();
217+
});
218+
});
219+
});

0 commit comments

Comments
 (0)