Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/durabletask-js/src/testing/in-memory-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,20 @@ export class InMemoryOrchestrationBackend {
case pb.OrchestratorAction.OrchestratoractiontypeCase.SENDEVENT:
this.processSendEventAction(action.getSendevent()!);
break;
case pb.OrchestratorAction.OrchestratoractiontypeCase.SENDENTITYMESSAGE:
// Entity message actions (signal, call, lock, unlock) are produced by
// orchestrations that interact with entities. The in-memory backend does
// not currently include a full entity processing runtime, so these
// actions are acknowledged but not executed. Signal (fire-and-forget)
// operations silently succeed; call/lock operations will cause the
// orchestration to wait indefinitely for a response that never arrives,
// which matches the expected behavior when no entity worker is available.
break;
case pb.OrchestratorAction.OrchestratoractiontypeCase.TERMINATEORCHESTRATION:
// Terminate-orchestration actions are used for recursive termination of
// sub-orchestrations. Process by terminating the target instance.
this.processTerminateOrchestrationAction(action);
break;
Comment on lines +467 to +471
default:
throw new Error(
`Unknown orchestrator action type '${actionType}' for orchestration '${instance.instanceId}'. ` +
Expand Down Expand Up @@ -624,6 +638,26 @@ export class InMemoryOrchestrationBackend {
});
}

private processTerminateOrchestrationAction(action: pb.OrchestratorAction): void {
const terminateAction = action.getTerminateorchestration();
if (!terminateAction) {
return;
}

const targetInstanceId = terminateAction.getInstanceid();
if (!targetInstanceId) {
return;
}

const output = terminateAction.getReason()?.getValue();

try {
this.terminate(targetInstanceId, output);
} catch {
// Target instance may not exist or already terminated - ignore
}
Comment on lines +654 to +658
}

private processSendEventAction(sendEvent: pb.SendEventAction): void {
const targetInstanceId = sendEvent.getInstance()?.getInstanceid();
const eventName = sendEvent.getName();
Expand Down
71 changes: 71 additions & 0 deletions packages/durabletask-js/test/in-memory-backend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
OrchestrationContext,
Task,
TOrchestrator,
EntityInstanceId,
} from "../src";

describe("In-Memory Backend", () => {
Expand Down Expand Up @@ -377,4 +378,74 @@ describe("In-Memory Backend", () => {
expect(state?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED);
expect(state?.serializedOutput).toEqual(JSON.stringify(42));
});

it("should not crash when orchestration signals an entity (fire-and-forget)", async () => {
const entityId = new EntityInstanceId("counter", "mykey");

const orchestrator: TOrchestrator = async (ctx: OrchestrationContext): Promise<any> => {
// Signal an entity (fire-and-forget). The in-memory backend doesn't
// process entities, but it must not crash on the SENDENTITYMESSAGE action.
ctx.entities.signalEntity(entityId, "add", 1);
return "signaled";
};

worker.addOrchestrator(orchestrator);
await worker.start();

const id = await client.scheduleNewOrchestration(orchestrator);
const state = await client.waitForOrchestrationCompletion(id, true, 10);

expect(state).toBeDefined();
expect(state?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED);
expect(state?.serializedOutput).toEqual(JSON.stringify("signaled"));
});

it("should not crash when orchestration signals multiple entities", async () => {
const entity1 = new EntityInstanceId("counter", "key1");
const entity2 = new EntityInstanceId("counter", "key2");

const orchestrator: TOrchestrator = async (ctx: OrchestrationContext): Promise<any> => {
ctx.entities.signalEntity(entity1, "add", 1);
ctx.entities.signalEntity(entity2, "add", 2);
return "done";
};

worker.addOrchestrator(orchestrator);
await worker.start();

const id = await client.scheduleNewOrchestration(orchestrator);
const state = await client.waitForOrchestrationCompletion(id, true, 10);

expect(state).toBeDefined();
expect(state?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED);
expect(state?.serializedOutput).toEqual(JSON.stringify("done"));
});

it("should not crash when orchestration signals entity then calls activity", async () => {
const entityId = new EntityInstanceId("counter", "mykey");

const doubleIt = async (_: ActivityContext, input: number) => {
return input * 2;
};

const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
// Signal an entity, then call an activity. The backend must handle
// the SENDENTITYMESSAGE action without crashing before the activity
// result can be processed.
ctx.entities.signalEntity(entityId, "add", 5);
const result: number = yield ctx.callActivity(doubleIt, 21);
return result;
};

worker.addOrchestrator(orchestrator);
worker.addActivity(doubleIt);
await worker.start();

const id = await client.scheduleNewOrchestration(orchestrator);
const state = await client.waitForOrchestrationCompletion(id, true, 10);

expect(state).toBeDefined();
expect(state?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED);
expect(state?.serializedOutput).toEqual(JSON.stringify(42));
});
});