Liking cljdoc? Tell your friends :D

dibs.core

Distributed scheduler lock built on Redis SET NX PX: instances call dibs on a job; the first one wins, everyone else skips.

Purpose

When the same scheduled job runs on N instances of a service, exactly one instance should execute it per tick. This namespace provides that guarantee as a higher-order function: the caller hands us a closure and we run it if — and only if — this process wins the distributed lock. This deliberately avoids the annotation + AOP approach because annotations cannot express runtime-computed lock keys, cannot return the job's result to the caller, and silently fail to apply on self-invocation since Spring proxies only intercept external calls.

Java surface

The namespace is AOT-compiled (:gen-class) into dibs.Dibs so that Kotlin/Java callers see a plain static-method API with zero Clojure in the call site:

execute(conn, lockName, atMostSec, atLeastSec, Runnable)  -> boolean
executeWithResult(conn, lockName, atMostSec, atLeastSec, Supplier) -> Optional<T>

execute returns whether the lock was won (and therefore whether the task ran). executeWithResult returns the task's value wrapped in Optional, or Optional.empty() when this instance was skipped. Note the ambiguity this implies: a Supplier that legitimately returns null is indistinguishable from a skip. Callers that care must use execute or return a sentinel.

Locking model

A lock is a single Redis key whose value is a UUID token unique to the holder. Three rules make it safe:

  1. Acquisition is SET key token NX PX ttl — one atomic command, so two instances can never both believe they won.
  2. The TTL (lockAtMostFor) means a crashed holder cannot wedge the lock forever; it self-heals when the TTL lapses.
  3. Release is token-checked in a Lua script, so a holder that outlived its own TTL cannot delete a lock that now belongs to someone else.

conn is either a Redis URI string ("redis://host:6379"), a Carmine conn map, or a java.util.Map treated as the Carmine :spec map (the shape a Kotlin caller naturally produces: mapOf("uri" to "redis://...")).

Distributed scheduler lock built on Redis SET NX PX: instances call dibs
on a job; the first one wins, everyone else skips.

## Purpose

When the same scheduled job runs on N instances of a service, exactly one
instance should execute it per tick. This namespace provides that guarantee
as a higher-order function: the caller hands us a closure and we run it
if — and only if — this process wins the distributed lock. This deliberately
avoids the annotation + AOP approach because annotations cannot express 
runtime-computed lock keys, cannot return the job's result to the caller,
and silently fail to apply on self-invocation since Spring proxies 
only intercept external calls.

## Java surface

The namespace is AOT-compiled (`:gen-class`) into `dibs.Dibs`
so that Kotlin/Java callers see a plain static-method API with zero Clojure
in the call site:

    execute(conn, lockName, atMostSec, atLeastSec, Runnable)  -> boolean
    executeWithResult(conn, lockName, atMostSec, atLeastSec, Supplier) -> Optional<T>

`execute` returns whether the lock was won (and therefore whether the task
ran). `executeWithResult` returns the task's value wrapped in Optional, or
Optional.empty() when this instance was skipped. Note the ambiguity this
implies: a Supplier that legitimately returns null is indistinguishable
from a skip. Callers that care must use `execute` or return a sentinel.

## Locking model

A lock is a single Redis key whose value is a UUID token unique to the
holder. Three rules make it safe:

1. Acquisition is `SET key token NX PX ttl` — one atomic command, so two
   instances can never both believe they won.
2. The TTL (lockAtMostFor) means a crashed holder cannot wedge the lock
   forever; it self-heals when the TTL lapses.
3. Release is token-checked in a Lua script, so a holder that outlived its
   own TTL cannot delete a lock that now belongs to someone else.

`conn` is either a Redis URI string ("redis://host:6379"), a Carmine
conn map, or a java.util.Map treated as the Carmine :spec map (the shape a
Kotlin caller naturally produces: mapOf("uri" to "redis://...")).
raw docstring

-executeclj

(-execute conn lock-name at-most-sec at-least-sec task)

-executeWithResultclj

(-executeWithResult conn lock-name at-most-sec at-least-sec task)

locked-callclj

(locked-call conn lock-name at-most-sec at-least-sec f)

Run thunk f under the distributed lock lock-name. Returns [acquired? result]: [true (f)] if this call won the lock, [false nil] if another holder has it and f was skipped entirely.

This is the one place the acquire/execute/release lifecycle lives, so the ordering rules are worth spelling out:

  • Durations are validated before touching Redis. atLeastFor > atMostFor is a contradiction (the lock would be required to outlive its own expiry), and catching it here turns a subtle production timing bug into an immediate IllegalArgumentException at the call site.

  • The token is minted per call, not per process. Two ticks of the same job on the same instance are distinct holders; reusing a process-wide token would let tick N+1 release a lock that tick N still holds.

  • Release lives in finally so a throwing task cannot leak the lock until its TTL. The exception itself propagates to the caller untouched — swallowing it would make failing jobs look like successful ones to the scheduler.

  • Elapsed time is measured in milliseconds as longs. remaining-ms is clamped at zero because a task that ran longer than atLeastFor has already satisfied the minimum-hold requirement by simply existing.

Run thunk `f` under the distributed lock `lock-name`. Returns
[acquired? result]: [true (f)] if this call won the lock, [false nil] if
another holder has it and `f` was skipped entirely.

This is the one place the acquire/execute/release lifecycle lives, so the
ordering rules are worth spelling out:

- Durations are validated before touching Redis. atLeastFor > atMostFor
  is a contradiction (the lock would be required to outlive its own
  expiry), and catching it here turns a subtle production timing bug into
  an immediate IllegalArgumentException at the call site.

- The token is minted per *call*, not per process. Two ticks of the same
  job on the same instance are distinct holders; reusing a process-wide
  token would let tick N+1 release a lock that tick N still holds.

- Release lives in `finally` so a throwing task cannot leak the lock
  until its TTL. The exception itself propagates to the caller untouched
  — swallowing it would make failing jobs look like successful ones to
  the scheduler.

- Elapsed time is measured in milliseconds as longs. remaining-ms is
  clamped at zero because a task that ran longer than atLeastFor has
  already satisfied the minimum-hold requirement by simply existing.
raw docstring

release-lockclj

(release-lock conn lock-key token remaining-ms)

Release lock-key, but only if token still owns it. When remaining-ms is positive, the key is retained that long instead of deleted (the lockAtLeastFor window — see release-script for why). Returns true iff we still owned the lock; false means our TTL expired while the task ran and the key is either gone or someone else's now, in which case we deliberately touch nothing.

Release `lock-key`, but only if `token` still owns it. When
`remaining-ms` is positive, the key is retained that long instead of
deleted (the lockAtLeastFor window — see release-script for why).
Returns true iff we still owned the lock; false means our TTL expired
while the task ran and the key is either gone or someone else's now, in
which case we deliberately touch nothing.
raw docstring

try-acquire-lockclj

(try-acquire-lock conn lock-key token ttl-ms)

Attempt to acquire lock-key on behalf of token, expiring after ttl-ms. Returns true iff this call acquired the lock.

This is a single SET key token NX PX ttl — the NX makes acquisition atomic (no get-then-set race window), and attaching the TTL in the same command means there is no instant where the key exists without an expiry. A two-step SETNX + EXPIRE would leave exactly that gap: crash between the two commands and the lock is held forever.

The token matters as much as the TTL. Every acquisition writes a value unique to the acquiring call (a random UUID), which is what later lets release-lock distinguish "my lock" from "a lock someone else took after mine expired". Without it, this would be a lock that any process can release, which is barely a lock at all.

Attempt to acquire `lock-key` on behalf of `token`, expiring after
`ttl-ms`. Returns true iff this call acquired the lock.

This is a single `SET key token NX PX ttl` — the NX makes acquisition
atomic (no get-then-set race window), and attaching the TTL in the same
command means there is no instant where the key exists without an expiry.
A two-step SETNX + EXPIRE would leave exactly that gap: crash between the
two commands and the lock is held forever.

The token matters as much as the TTL. Every acquisition writes a value
unique to the acquiring call (a random UUID), which is what later lets
release-lock distinguish "my lock" from "a lock someone else took after
mine expired". Without it, this would be a lock that any process can
release, which is barely a lock at all.
raw docstring

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