Skip to content

fix: handle entity message actions in InMemoryOrchestrationBackend - #327

Open
YunchuWang wants to merge 5 commits into
mainfrom
copilot-finds/bug/in-memory-backend-missing-entity-action-handling
Open

fix: handle entity message actions in InMemoryOrchestrationBackend#327
YunchuWang wants to merge 5 commits into
mainfrom
copilot-finds/bug/in-memory-backend-missing-entity-action-handling

Conversation

@YunchuWang

@YunchuWang YunchuWang commented Jul 23, 2026

Copy link
Copy Markdown
Member

Fixes #209.

InMemoryOrchestrationBackend is public API (exported from src/index.ts) — it is what
consumers use to unit-test orchestrations without a sidecar. Its processAction switch
ends in default: throw new Error("Unknown orchestrator action type ..."), and none of
the four entity message actions were handled.

Taking the inventory of action builders in pb-helper.util.ts:

Builder Handled before this PR
newCompleteOrchestrationAction yes
newCreateTimerAction yes
newScheduleTaskAction yes
newCreateSubOrchestrationAction yes
newSendEventAction yes
newSendEntityMessageSignalAction no
newSendEntityMessageCallAction no
newSendEntityMessageLockAction no
newSendEntityMessageUnlockAction no

Before

Any orchestration calling ctx.entities.signalEntity(...) fails on this backend with:

Unknown orchestrator action type '8' for orchestration 'ea0678a9-...'.
This likely means the in-memory backend needs to be updated to handle a
newly introduced action type.

A raw enum ordinal, with nothing indicating entities are involved. In practice
signalEntity is untestable with the shipped test harness.

After

SENDENTITYMESSAGE dispatches on its four-way oneof, because the sub-types are not
equivalent:

  • signalEntity, unlock — fire-and-forget, no reply expected → no-op, orchestration
    proceeds normally.
  • callEntity, lockEntities — block awaiting a response that no entity worker exists
    to send here → throw an explanatory error naming the operation and pointing at the
    emulator/a real backend.

A blanket no-op across all four would be worse than the status quo: it converts today's
fast failure into a permanent hang for callEntity/lockEntities. The two fail-fast tests
cover exactly that — with a blanket no-op they hang to Jest's 5000 ms timeout; with the
dispatch they fail in ~6 ms.

Scope note

An earlier revision of this PR also handled TERMINATEORCHESTRATION. That has been removed
as unreachable:

  • No TerminateOrchestrationAction is constructed anywhere in src/ outside the generated
    proto, and pb-helper.util.ts has no builder for it.
  • OrchestrationContext exposes no terminate API — termination is client-side.
  • Its only non-proto reference is getActionSummary() in worker/index.ts, a logging
    helper that maps action types to display names.

By contrast REWINDORCHESTRATION, which this backend also handles, is constructed at
worker/rewind.ts:123. Handling exactly what is reachable is the existing convention.

Verification

  • Reverting the src change while keeping the new tests: 5 failed, 35 passed — the three
    signal tests report FAILED instead of COMPLETED, and the fail-fast tests surface the
    Unknown orchestrator action type '8' message.
  • With the change: 40/40 in in-memory-backend.spec.ts.
  • Full core unit suite: 1181 passing across 67 suites; unchanged before and after
    removing the terminate handler, confirming nothing exercised that path.
  • npm run build and eslint both clean.

…TYMESSAGE and TERMINATEORCHESTRATION action types

Add handling for SENDENTITYMESSAGE (case 8) and TERMINATEORCHESTRATION (case 7) action
types in InMemoryOrchestrationBackend.processAction(). Previously, orchestrations using
entity signals or terminate-orchestration actions would crash with 'Unexpected action type'
when run against the in-memory testing backend.

- SENDENTITYMESSAGE: No-op (entity signals are fire-and-forget; in-memory backend does not
  simulate entity workers)
- TERMINATEORCHESTRATION: Extracts target instance ID and delegates to existing terminate()
  method
- Adds 3 unit tests verifying entity signal orchestrations complete without crashing

Fixes #209

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 23, 2026 16:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes crashes in the in-memory testing backend by handling additional orchestrator action types that can be produced by entity interactions and orchestration termination.

Changes:

  • Handle SENDENTITYMESSAGE actions in InMemoryOrchestrationBackend.processAction() (acknowledged/no-op).
  • Handle TERMINATEORCHESTRATION actions by terminating the targeted instance.
  • Add unit tests to verify entity signal (fire-and-forget) orchestrations don’t crash when run against the in-memory backend.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/durabletask-js/src/testing/in-memory-backend.ts Adds handling for SENDENTITYMESSAGE and TERMINATEORCHESTRATION orchestrator actions.
packages/durabletask-js/test/in-memory-backend.spec.ts Adds regression tests ensuring entity signaling doesn’t crash the in-memory backend.
Comments suppressed due to low confidence (1)

packages/durabletask-js/src/testing/in-memory-backend.ts:471

  • TERMINATEORCHESTRATION handling is newly added but isn't covered by a unit test (the existing termination test exercises external termination via the test client, not an OrchestratorAction of type TERMINATEORCHESTRATION). Adding a regression test would help ensure this action type doesn't reintroduce crashes.
      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 +654 to +658
try {
this.terminate(targetInstanceId, output);
} catch {
// Target instance may not exist or already terminated - ignore
}
Comment on lines +467 to +471
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;
…tity-action-handling

Conflicts came from main adding REWINDORCHESTRATION handling (#296) to the same processAction switch and the same method region, plus independent additive tests in the spec. Kept all three action cases (SENDENTITYMESSAGE, TERMINATEORCHESTRATION, REWINDORCHESTRATION), both new private methods, and both sets of tests.
Copilot AI review requested due to automatic review settings July 27, 2026 18:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

packages/durabletask-js/src/testing/in-memory-backend.ts:755

  • processTerminateOrchestrationAction wraps terminate() in a broad try/catch, but terminate() only throws on a missing instance. Catching everything can hide unexpected errors if terminate() changes in the future; it’s safer to check existence up-front and call terminate() without swallowing exceptions.
  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 +551 to +555
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;
SENDENTITYMESSAGE is a four-way oneof, but it was handled with a single blanket no-op. signalEntity and the critical-section unlock are fire-and-forget, so dropping them is correct. callEntity and lockEntities await an EntityOperationCompleted / EntityLockGranted response that this backend has no entity worker to produce, so the no-op turned a fast 'unknown action type' failure into a silent hang until the caller's timeout.

Now branches on EntitymessagetypeCase: signal and unlock stay no-ops, call and lock throw an explanatory error naming the operation and pointing at a real backend or the emulator, and an unrecognized case throws like the surrounding action switch does. The worker already converts a throw here into a FAILED orchestration with the message in failureDetails. Added tests for both paths; without the fix they hang until the Jest timeout (5002 ms), with it they fail in 6 ms.
Copilot AI review requested due to automatic review settings July 27, 2026 19:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/durabletask-js/src/testing/in-memory-backend.ts:549

  • TERMINATEORCHESTRATION handling was added here, but the test suite changes only cover SENDENTITYMESSAGE (entity signaling/call/lock). Consider adding a regression test that exercises a TERMINATEORCHESTRATION action end-to-end (or via a small backend-level simulation) to ensure terminating a target instance works and doesn’t regress.
      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;

YunchuWang and others added 2 commits July 28, 2026 16:42
The JS SDK never emits a TerminateOrchestrationAction, so the handler added
alongside the entity-message support could not be reached:

  - No construction site exists anywhere in src/ (outside generated proto).
    pb-helper.util.ts has builders for complete, timer, scheduleTask,
    subOrchestration, sendEvent and the four entity messages, but none for
    terminate.
  - OrchestrationContext exposes no terminate API; termination is client-side.
  - The only non-proto reference is getActionSummary() in worker/index.ts,
    which merely maps action types to display names for logging.

By contrast REWINDORCHESTRATION, which the backend also handles, is genuinely
constructed at worker/rewind.ts:123 - so handling exactly what is reachable is
the existing convention here.

The removed handler was also untested and swallowed all errors from terminate().

Full unit suite is unchanged at 1181 passing across 67 suites, confirming
nothing exercised this path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5aaa70df-fae3-4570-aabe-0fc96cb869bc
Copilot AI review requested due to automatic review settings July 28, 2026 23:46
@YunchuWang YunchuWang changed the title fix: handle SENDENTITYMESSAGE and TERMINATEORCHESTRATION in InMemoryOrchestrationBackend fix: handle entity message actions in InMemoryOrchestrationBackend Jul 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[copilot-finds] Bug: InMemoryOrchestrationBackend crashes on SENDENTITYMESSAGE and TERMINATEORCHESTRATION action types

3 participants