Skip to content

[durable-functions v4] Investigate restoring the v3 entity-lock API (context.df.lock / isLocked) vs documented migration #317

Description

@YunchuWang

Follow-up from PR #282 (review comment #17, P1)

PR #282 (Add Functions gRPC core helpers) added the new packages/azure-functions-durable compat package (npm durable-functions v4), built on the @microsoft/durabletask-js gRPC core. In review, @andystaples flagged (P1) that the package removed the v3 entity-lock API without a bridge:

  • context.df.lock(...entityIds) — v3 yields a DurableLock
  • context.df.isLocked(...) — returns boolean
  • types DurableLock, LockState, LockingRulesViolationError

This was resolved in #282 as a documented breaking change (CHANGELOG migration to the core-native API), not restored. This issue tracks whether the v3 surface can actually be brought back for drop-in compatibility, or whether the documented migration is the permanent answer.

Why it wasn't restored in #282

Core already exposes the same capability, with a different shape:

v3 (durable-functions) core (@microsoft/durabletask-js)
yield context.df.lock(...ids) -> DurableLock context.entities.lockEntities(...ids) -> Task<LockHandle> (handle.release())
context.df.isLocked() context.entities.isInCriticalSection()

The hard part is lock(): v3 orchestrators yield it and receive a DurableLock. Core's lockEntities() returns a Task<LockHandle>. To preserve the v3 yield shape we would need to remap Task<LockHandle> -> Task<DurableLock>, but core's Task<T> has no .then / .map / derived-task primitive (packages/durabletask-js/src/task/task.ts). Adding one touches the replay engine, so it is determinism-sensitive and was intentionally out of scope for #282.

Investigation asks

  1. isLocked() alias (likely cheap): can context.df.isLocked() be implemented directly on top of isInCriticalSection() in the compat layer with no engine change? Confirm the semantics match (any-lock-held vs specific-entity query).
  2. lock() bridge (the real question):
    • (a) Can a compat-layer wrapper adapt LockHandle -> DurableLock (yield shape + .release() semantics) without an engine-level Task.map, e.g. by post-processing the resolved handle replay-safely? Determine whether the yield contract allows adapting the resolved value deterministically.
    • (b) If not, is a replay-deterministic Task.map / derived-task primitive feasible in core? Scope the engine + determinism risk. (This is broader than compat and would benefit other consumers.)
  3. Type re-exports: if lock() is bridged, re-export compat DurableLock / LockState and map core lock-rule violations to LockingRulesViolationError.

Acceptance

Links

Follow-up to PR #282 review comment #17.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions