Problem
The orchestration executor's processEvent switch statement in orchestration-executor.ts handles all entity-related history event types except ENTITYUNLOCKSENT. When an orchestration acquires entity locks and releases them, the sidecar records EntityUnlockSent events in the history. On replay, these events fall through to the default case and are silently logged as unknown events.
This means the unlock actions created by exitCriticalSection() during replay are never cleaned up from _pendingActions. Since setComplete() intentionally preserves pending actions (for fire-and-forget actions like sendEvent), the stale unlock actions are included in the returned action list and re-sent to the sidecar on every replay.
Affected files:
packages/durabletask-js/src/worker/orchestration-executor.ts (lines 131–194, switch statement)
Root Cause
All other entity event types have dedicated handlers that call validateEntityAction() to match the event against the pending action and remove it from _pendingActions:
ENTITYOPERATIONCALLED → handleEntityOperationCalled
ENTITYOPERATIONSIGNALED → handleEntityOperationSignaled
ENTITYLOCKREQUESTED → handleEntityLockRequested
ENTITYOPERATIONCOMPLETED → handleEntityOperationCompleted
ENTITYOPERATIONFAILED → handleEntityOperationFailed
ENTITYLOCKGRANTED → handleEntityLockGranted
ENTITYUNLOCKSENT → ❌ missing
The exitCriticalSection() method creates one SendEntityMessageAction (unlock type) per locked entity. On replay, the corresponding EntityUnlockSent history events are not processed, so these actions accumulate.
Proposed Fix
Add a handleEntityUnlockSent method and case in the switch statement, following the same validateEntityAction pattern used by the other entity event handlers.
Impact
- Severity: Medium — Duplicate unlock messages sent to the sidecar on every orchestration replay involving entity locks. The sidecar may handle these gracefully, but it creates unnecessary network traffic and violates the replay invariant that stale actions should not be re-emitted.
- Scenarios affected: Any orchestration that uses
lockEntities() / critical sections with entity locking. The bug manifests on every subsequent replay after the initial execution that releases locks.
Problem
The orchestration executor's
processEventswitch statement inorchestration-executor.tshandles all entity-related history event types exceptENTITYUNLOCKSENT. When an orchestration acquires entity locks and releases them, the sidecar recordsEntityUnlockSentevents in the history. On replay, these events fall through to thedefaultcase and are silently logged as unknown events.This means the unlock actions created by
exitCriticalSection()during replay are never cleaned up from_pendingActions. SincesetComplete()intentionally preserves pending actions (for fire-and-forget actions likesendEvent), the stale unlock actions are included in the returned action list and re-sent to the sidecar on every replay.Affected files:
packages/durabletask-js/src/worker/orchestration-executor.ts(lines 131–194, switch statement)Root Cause
All other entity event types have dedicated handlers that call
validateEntityAction()to match the event against the pending action and remove it from_pendingActions:ENTITYOPERATIONCALLED→handleEntityOperationCalledENTITYOPERATIONSIGNALED→handleEntityOperationSignaledENTITYLOCKREQUESTED→handleEntityLockRequestedENTITYOPERATIONCOMPLETED→handleEntityOperationCompletedENTITYOPERATIONFAILED→handleEntityOperationFailedENTITYLOCKGRANTED→handleEntityLockGrantedENTITYUNLOCKSENT→ ❌ missingThe
exitCriticalSection()method creates oneSendEntityMessageAction(unlock type) per locked entity. On replay, the correspondingEntityUnlockSenthistory events are not processed, so these actions accumulate.Proposed Fix
Add a
handleEntityUnlockSentmethod and case in the switch statement, following the samevalidateEntityActionpattern used by the other entity event handlers.Impact
lockEntities()/ critical sections with entity locking. The bug manifests on every subsequent replay after the initial execution that releases locks.