Liking cljdoc? Tell your friends :D

MachineLatch

Clojars Project cljdoc

A Clojure library providing a state machine latch with declarative transitions and await semantics. Designed for coordinating concurrent operations on virtual threads.

Requirements

  • JDK 25+ (recommended, LTS)
  • Clojure 1.12+

This library depends on co.multiply/scoped which uses Java's ScopedValue API, stable as of JDK 25.

Installation

;; deps.edn
co.multiply/machine-latch {:mvn/version "0.1.3"}

Why machine-latch?

Java's CountDownLatch handles binary state well, but multi-state coordination lands squarely on you. Once you need more than "waiting" and "done", you're managing atomics and condition variables yourself.

Phaser offers multi-phase coordination, but with a tradeoff: threads can only wait until the current phase passes. This means every transition wakes every waiter, even those waiting for later phases.

AtomicReference could hold the current state, but can't easily answer "have we reached or passed phase N?" Without imposing structure on state progression, you're back to manual bookkeeping.

machine-latch addresses all three by compiling states to integers and asserting unidirectional progression:

  • Targeted wake-ups: Threads waiting for phase 5 stay parked when you transition to phase 2. Only threads whose target state has been reached are woken.
  • Strict state machine contract: Define valid states and transitions upfront. Only declared transitions are allowed, and exactly one caller wins each transition via CAS.
(require '[co.multiply.machine-latch :as ml])

(def make-job-latch
  (ml/machine-latch-factory
    {:states      [:queued :running :complete]
     :transitions {:start  {:queued :running}
                   :finish {:running :complete}
                   :cancel {:queued  :complete
                            :running :complete}}}))

API

machine-latch-factory

Creates a factory function from a machine spec. The factory produces latch instances that share compiled transition logic.

(require '[co.multiply.machine-latch :as ml])

(def make-latch
  (ml/machine-latch-factory
    {:states      [:pending :running :done]
     :transitions {:start  {:pending :running}
                   :finish {:running :done}}}))

(def latch (make-latch))

Actions can have multiple source states:

(def make-latch
  (ml/machine-latch-factory
    {:states      [:pending :running :done :cancelled]
     :transitions {:start  {:pending :running}
                   :finish {:running :done}
                   :cancel {:pending :cancelled
                            :running :cancelled}}}))

transition!

Atomically attempt a state transition. Returns true if this thread won the transition, false otherwise.

(def latch (make-latch))

(ml/transition! latch :start)  ; => true (won the transition)
(ml/transition! latch :start)  ; => false (already past :pending)

(ml/get-state latch)           ; => :running

When transition! returns true, the calling thread "owns" that phase and should proceed with the associated work. When it returns false, another thread won (or the action isn't valid from the current state).

get-state

Returns the current state keyword. Non-blocking.

(ml/get-state latch)  ; => :running

at-or-past?

Non-blocking check: has the latch reached or passed a given state?

(ml/at-or-past? latch :pending)  ; => true (we're past it)
(ml/at-or-past? latch :running)  ; => true (we're at it)
(ml/at-or-past? latch :done)     ; => false (not yet)

await

Block until the latch reaches or passes a target state. Returns true when reached.

;; On a virtual thread:
(ml/await latch :done)  ; blocks until :done, returns true

Must be called from a virtual thread by default (see Platform Thread Protection).

await-millis / await-dur

Block with a timeout. Returns true if the state was reached, false if timed out.

(require '[co.multiply.machine-latch :as ml])
(import '[java.time Duration])

(ml/await-millis latch :done 5000)              ; 5 second timeout
(ml/await-dur latch :done (Duration/ofSeconds 5))  ; same, with Duration

Constraints

Validated at compile time:

  • States must be declared in order from initial to terminal
  • Transitions must only go forward in this order
  • The :states vector defines both valid states and their ordering
  • The machine must have exactly one terminal state

Execution Ownership

transition! grants execution ownership via CAS. When it returns true:

  • The caller "owns" the current phase's work
  • No other caller can win the same transition
  • The caller may proceed with phase-specific logic

When it returns false, another thread won (or the transition isn't valid from the current state). The losing thread should not proceed with that phase's work.

Efficiency

  • Pre-compilation: States are mapped to integers at factory creation. All runtime comparisons use int, not keyword lookup. Likewise, transitions are compiled and shared across all instances.
  • Targeted wake-ups: Only threads waiting for a reached-or-passed state are unparked. Threads waiting for later states remain parked without spurious wake-ups.

Platform Thread Protection

By default, await throws IllegalStateException if called from a platform thread. Parking platform threads is a bad idea, and is by default discouraged by this library. By throwing, you have an increased chance of discovering cases that might be heading toward threadpool starvation.

Disable via:

  • JVM property: -Dco.multiply.machine-latch.assert-virtual=false
  • At runtime: (ml/throw-on-platform-park! false)
  • Per-call: (scoping [ml/*assert-virtual* false] (ml/await latch :state)) *

* See scoping

Example: Job Coordination

A job that can be started, finished, or cancelled:

(require '[co.multiply.machine-latch :as ml])

(def make-job-latch
  (ml/machine-latch-factory
    {:states      [:queued :running :complete]
     :transitions {:start  {:queued :running}
                   :finish {:running :complete}
                   :cancel {:queued  :complete
                            :running :complete}}}))

(let [latch (make-job-latch)]
  ;; Spawn worker
  (Thread/startVirtualThread
    (fn []
      (when (ml/transition! latch :start)
        (Thread/sleep 5000)  ; simulate work
        (ml/transition! latch :finish))))

  ;; Wait for completion (on a virtual thread)
  (Thread/startVirtualThread
    (fn []
      (ml/await latch :complete)
      (println "Job complete!")))

  ;; Or cancel it
  (ml/transition! latch :cancel))

License

MIT License. Copyright (c) 2025 Multiply. See LICENSE.

Authored by @eneroth

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close