Skip to content

Commit c5722a2

Browse files
authored
fix: set instanceId on ActivityResponse in failure path (#122)
1 parent a629b19 commit c5722a2

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

packages/durabletask-js/src/worker/task-hub-grpc-worker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,7 @@ export class TaskHubGrpcWorker {
780780
const failureDetails = pbh.newFailureDetails(error);
781781

782782
res = new pb.ActivityResponse();
783+
res.setInstanceid(instanceId);
783784
res.setTaskid(req.getTaskid());
784785
res.setCompletiontoken(completionToken);
785786
res.setFailuredetails(failureDetails);
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* Tests that the TaskHubGrpcWorker correctly sets instanceId on ActivityResponse
6+
* in both success and failure paths.
7+
*
8+
* This validates the fix for a bug where the activity failure path omitted
9+
* the instanceId field from the ActivityResponse, deviating from the .NET SDK
10+
* behavior and potentially causing issues with sidecar response routing.
11+
*/
12+
13+
import * as pb from "../src/proto/orchestrator_service_pb";
14+
import * as stubs from "../src/proto/orchestrator_service_grpc_pb";
15+
import { TaskHubGrpcWorker } from "../src/worker/task-hub-grpc-worker";
16+
import { NoOpLogger } from "../src/types/logger.type";
17+
import { ActivityContext } from "../src/task/context/activity-context";
18+
import { StringValue } from "google-protobuf/google/protobuf/wrappers_pb";
19+
20+
const TEST_INSTANCE_ID = "test-instance-123";
21+
const TEST_TASK_ID = 42;
22+
const COMPLETION_TOKEN = "test-completion-token";
23+
24+
describe("Worker Activity Response", () => {
25+
/**
26+
* Creates a mock gRPC stub that captures the ActivityResponse passed to
27+
* completeActivityTask.
28+
*/
29+
function createMockStub(): {
30+
stub: stubs.TaskHubSidecarServiceClient;
31+
capturedResponse: pb.ActivityResponse | null;
32+
} {
33+
let capturedResponse: pb.ActivityResponse | null = null;
34+
35+
const stub = {
36+
completeActivityTask: (
37+
response: pb.ActivityResponse,
38+
metadata: any,
39+
callback: (err: any, res: any) => void,
40+
) => {
41+
capturedResponse = response;
42+
callback(null, {});
43+
},
44+
} as unknown as stubs.TaskHubSidecarServiceClient;
45+
46+
return {
47+
stub,
48+
get capturedResponse() {
49+
return capturedResponse;
50+
},
51+
};
52+
}
53+
54+
/**
55+
* Creates a minimal ActivityRequest for testing.
56+
*/
57+
function createActivityRequest(name: string, input?: string): pb.ActivityRequest {
58+
const req = new pb.ActivityRequest();
59+
req.setName(name);
60+
req.setTaskid(TEST_TASK_ID);
61+
62+
const orchInstance = new pb.OrchestrationInstance();
63+
orchInstance.setInstanceid(TEST_INSTANCE_ID);
64+
req.setOrchestrationinstance(orchInstance);
65+
66+
if (input !== undefined) {
67+
const inputValue = new StringValue();
68+
inputValue.setValue(input);
69+
req.setInput(inputValue);
70+
}
71+
72+
return req;
73+
}
74+
75+
it("should set instanceId on ActivityResponse when activity succeeds", async () => {
76+
// Arrange
77+
const worker = new TaskHubGrpcWorker({
78+
logger: new NoOpLogger(),
79+
});
80+
81+
const successActivity = (_ctx: ActivityContext) => {
82+
return "success result";
83+
};
84+
85+
worker.addActivity(successActivity);
86+
87+
const mockStub = createMockStub();
88+
const req = createActivityRequest("successActivity", JSON.stringify("test-input"));
89+
90+
// Act - call the private method directly
91+
await (worker as any)._executeActivityInternal(req, COMPLETION_TOKEN, mockStub.stub);
92+
93+
// Assert
94+
expect(mockStub.capturedResponse).not.toBeNull();
95+
expect(mockStub.capturedResponse!.getInstanceid()).toBe(TEST_INSTANCE_ID);
96+
expect(mockStub.capturedResponse!.getTaskid()).toBe(TEST_TASK_ID);
97+
expect(mockStub.capturedResponse!.getFailuredetails()).toBeUndefined();
98+
});
99+
100+
it("should set instanceId on ActivityResponse when activity fails", async () => {
101+
// Arrange
102+
const worker = new TaskHubGrpcWorker({
103+
logger: new NoOpLogger(),
104+
});
105+
106+
const failingActivity = (_ctx: ActivityContext) => {
107+
throw new Error("Activity execution failed");
108+
};
109+
110+
worker.addActivity(failingActivity);
111+
112+
const mockStub = createMockStub();
113+
const req = createActivityRequest("failingActivity");
114+
115+
// Act - call the private method directly
116+
await (worker as any)._executeActivityInternal(req, COMPLETION_TOKEN, mockStub.stub);
117+
118+
// Assert - the key assertion: instanceId MUST be set even on failure
119+
expect(mockStub.capturedResponse).not.toBeNull();
120+
expect(mockStub.capturedResponse!.getInstanceid()).toBe(TEST_INSTANCE_ID);
121+
expect(mockStub.capturedResponse!.getTaskid()).toBe(TEST_TASK_ID);
122+
expect(mockStub.capturedResponse!.getFailuredetails()).toBeDefined();
123+
expect(mockStub.capturedResponse!.getFailuredetails()!.getErrormessage()).toContain(
124+
"Activity execution failed",
125+
);
126+
});
127+
128+
it("should set instanceId on ActivityResponse when activity throws non-Error", async () => {
129+
// Arrange
130+
const worker = new TaskHubGrpcWorker({
131+
logger: new NoOpLogger(),
132+
});
133+
134+
const throwStringActivity = (_ctx: ActivityContext) => {
135+
throw "string error";
136+
};
137+
138+
worker.addActivity(throwStringActivity);
139+
140+
const mockStub = createMockStub();
141+
const req = createActivityRequest("throwStringActivity");
142+
143+
// Act
144+
await (worker as any)._executeActivityInternal(req, COMPLETION_TOKEN, mockStub.stub);
145+
146+
// Assert
147+
expect(mockStub.capturedResponse).not.toBeNull();
148+
expect(mockStub.capturedResponse!.getInstanceid()).toBe(TEST_INSTANCE_ID);
149+
expect(mockStub.capturedResponse!.getFailuredetails()).toBeDefined();
150+
});
151+
});

0 commit comments

Comments
 (0)