A Clojure library providing a state machine latch with declarative transitions and await semantics. Designed for coordinating concurrent operations on virtual threads.
This library depends on co.multiply/scoped which uses Java's ScopedValue API,
stable as of JDK 25.
;; deps.edn
co.multiply/machine-latch {:mvn/version "0.1.3"}
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:
(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}}}))
machine-latch-factoryCreates 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-stateReturns 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)
awaitBlock 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-durBlock 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
Validated at compile time:
:states vector defines both valid states and their orderingtransition! grants execution ownership via CAS. When it returns true:
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.
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:
-Dco.multiply.machine-latch.assert-virtual=false(ml/throw-on-platform-park! false)(scoping [ml/*assert-virtual* false] (ml/await latch :state)) ** See scoping
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))
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
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |