Software Transactional Memory

mino has refs, dosync, alter, commute, ensure, ref-set, io!, and watches plus validators on refs. The Clojure-level surface matches canon for any program that does not depend on the items below. Underneath, mino uses single-version optimistic locking with a global commit lock, not MVCC with per-ref read/write locks. The trade-off is deliberate: mino's typical workload is a small ref set and a handful of worker threads, often single-threaded. The simpler machinery costs nothing on the single-threaded fast path and stays comprehensible at a glance.

What works the same

If you know JVM Clojure, the surface is identical for everyday code:

(let [counter (ref 0)
      total   (ref 0)]
  (dotimes [_ 5]
    (dosync
      (alter counter inc)
      (alter total + @counter)))
  [@counter @total])
;=> [5 15]

How mino's STM differs underneath

These are documented deviations from JVM Clojure's clojure.lang.LockingTransaction. They are listed in one place at the top of src/prim/stm.c in the mino source tree.

Single-version optimistic locking

mino keeps one committed value per ref, not an MVCC history ring. Long-running readers competing with sustained writer pressure may exhaust the retry cap (10000) where JVM would serve an older snapshot from history. ref-min-history, ref-max-history, and ref-history-count exist as stubs returning 0 / 10 / 0 for source compatibility, but there is no history to introspect.

Global commit lock

mino serializes all commits behind one mutex (S->stm.commit_lock). Coarser than JVM's per-ref read/write locks but simpler, and on a single thread the lock is skipped entirely. Reads outside a transaction are an atomic load; reads inside a transaction touch only per-thread state.

No barging

JVM's older-tx-bumps-younger-tx mechanism is intentionally absent. Every retry restarts the body from scratch, so the retry cap is the only bound.

No mid-body retry

JVM detects conflicts as soon as a read observes a stale version. mino only checks the read set at commit time. Wasted work is bounded by the retry cap.

Print form

(pr-str r) produces #ref[ID VAL] where ID is a monotonic per-state counter. JVM prints #object[clojure.lang.Ref 0x... {:status :ready, :val ...}], a JVM-specific shape. mino's form is deliberately simpler and not pretending to be a JVM class.

Embedded use

STM is opt-in via mino_install(S, env, MINO_CAP_STM). The standalone ./mino binary calls mino_install_all, which installs it; embedders using only mino_env_new_default (the sandbox preset) stay opt-out. Anything a Clojure programmer can do, a C host can do via the mirroring mino_tx_* API:

The C entries share their core implementation with the Clojure-side primitives via internal tx_*_core helpers, so the two surfaces cannot drift. A nested mino_tx_run or dosync inside an outer transaction is absorbed into the outer's tx_state_t; only the outermost runner owns the setjmp / retry frame. See the Embedding Guide for how to compose this with watches, futures, and the host's own thread pool.

Cross-state ref defense

JVM Clojure has one global transactional surface. mino supports many mino_state in a single host process, so a host that accidentally passes a ref allocated in one state to another state's mino_tx_* entries would silently mutate the foreign heap. To prevent that, every ref records its allocating state at construction time, and every public C entry checks it; a mismatch throws eval/state MST007 ("ref from foreign state").

Agents

mino ships agents with async dispatch: agent, send, send-off, await, await-for, agent-error, restart-agent, shutdown-agents, release-pending-sends, and the error-mode / error-handler surface. send enqueues an action onto a per-state run-queue (POOLED for send, SOLO for send-off) and returns the agent immediately; a worker thread per pool drains its queue and runs each action under state_lock. await and await-for block until every named agent's in-flight count reaches zero (await-for returns false on timeout).

Thread budget. Each pool's worker counts against thread_limit (the embedder thread does not). Default is 1 in embedded use; standalone ./mino bumps to cpu_count after install. send / send-off throw MTH001 when the host hasn't granted enough thread budget -- the same shape future / promise / thread already use. Each pool's worker exits when its run-queue drains so it doesn't keep thread_count > 0 indefinitely; the next send into that pool re-spawns. mino's per-state eval lock still serializes one action at a time across both pools, so the user-visible behavior is identical to a single queue today; the split is the seam for a future SOLO-yields-eval-lock-during-blocking-IO design. Embedders that want both pools alive concurrently must raise the thread limit to at least 2; mixing with futures or host threads requires correspondingly more.

Failure handling. Action throws and watch throws are both captured into agent-error via mino_pcall -- a thrown watch does not abort sibling watches or propagate to the caller of send, matching JVM canon. With an error-handler installed, the action throw routes through the handler and the agent stays clean (no agent-error latch). restart-agent accepts trailing :clear-actions true to drop every queued action for that agent. send-via is intentionally deferred (no public Executor type).

Lifecycle. shutdown-agents flips an agents-shutdown flag, signals both pool workers to drain and exit, and pthread_joins each. Subsequent send / send-off throw MST008. Calling shutdown-agents from inside an action body (self-join) throws MST002 instead of deadlocking. mino_state_free quiesces both pools before heap teardown so a worker can't run after free.

Embedder C-API. Host code can drive agents directly without going through the Clojure prim layer. mino_send / mino_send_off enqueue an action and return the agent immediately. mino_await / mino_await_for block until the named agents drain (NULL-terminated array). mino_agent_error reads the failure latch. mino_restart_agent clears it and resets the value, with optional clear-actions semantics. Each entry takes the same mino_lock perimeter mino_call uses, and the cross-state guard fires at the boundary -- passing an agent from another mino_state throws MST007 and returns NULL.

What still doesn't work