Skip to content

neuodev/distributed-lock

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Distributed Lock (Semaphore) on Temporal

A distributed semaphore that coordinates access to a limited resource, built on Temporal Workflows in TypeScript.

The trick: a Workflow ID is the lock. Temporal refuses to run two Workflows with the same ID, so starting a Workflow named permit:{resource}:{slot} is an atomic "claim this slot" operation.

How it works

A semaphore of capacity N is just N candidate slots (0 … N-1). Acquiring the semaphore means winning the start race for any one slot.

  • permitSlotWorkflow (src/workflows/permit-slot.ts) — one running instance per held permit. It does nothing but wait on a release signal with a lease timeout, then exits (freeing the slot). It ends in one of three ways:
    1. release signal → returns "released"
    2. lease elapses → returns "lease-expired"
    3. parent closes → ParentClosePolicy.TERMINATE kills it
  • Semaphore (src/workflows/semaphore.ts) — the acquire logic. It shuffles the slots (to spread contention), tries to startChild each one, treats WorkflowExecutionAlreadyStartedError as "slot busy", and backs off when all slots are held. withPermit() wraps acquire/release in a try/finally.
  • exampleConsumer (src/workflows/example.ts) — a parent Workflow that acquires a permit, does its work, and releases. As the acquirer, it is the parent of the permit children it starts.

The acquirer must be a Temporal Workflow, because acquisition uses startChild. An external client could instead acquire via Client.workflow.start, at the cost of losing ParentClosePolicy.TERMINATE (crash cleanup would rely solely on the lease).

Safety: lease + fencing

Three layers free a slot if a holder goes silent: ParentClosePolicy.TERMINATE (parent died), the in-Workflow lease condition timeout, and workflowExecutionTimeout as a server-side backstop. For resources that can be corrupted by overlap, pass the permit's runId downstream as a fencing token and reject stale holders there.

Run it

Requires a local Temporal dev server:

temporal server start-dev          # http://localhost:8233 for the Web UI

pnpm install
pnpm worker                        # terminal 1: start the worker
pnpm start                         # terminal 2: run the demo (8 consumers, capacity 3)

The demo starts 8 consumers contending for a capacity-3 resource, so 5 queue until a permit frees up. Watch the permit:gpu-pool:* Workflows come and go in the Web UI.

Scripts

Command Description
pnpm worker Run the Worker (tsx)
pnpm start Run the demo client (tsx)
pnpm build Compile with tsc + tsc-alias (rewrites the @/* alias)

Credits

Design mirrors Temporal's Distributed Lock article, ported to TypeScript.

About

A distributed semaphore that coordinates access to a limited resource, built on Temporal Workflows in TypeScript.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors