Liking cljdoc? Tell your friends :D

lite.bridge

Lite-internal glue between the user's ClientAdapter and the jepsen.client/Client the generator interpreter drives. Users never see jepsen.client/Client; they only write a ClientAdapter and a handler.

Like the adapter itself, this is generic: it wraps any ClientAdapter over any target-type's connection, and contains no workload or fault logic.

Lite-internal glue between the user's `ClientAdapter` and the
`jepsen.client/Client` the generator interpreter drives. Users never see
`jepsen.client/Client`; they only write a ClientAdapter and a handler.

Like the adapter itself, this is generic: it wraps *any* ClientAdapter over
*any* target-type's connection, and contains no workload or fault logic.
raw docstring

lite.client

The ClientAdapter abstraction: the protocol a user implements to bind Jepsen Lite to their target's protocol, plus the outcome signalling (fail! / info!) and the exception -> :type wrapping their handlers rely on.

Nothing in this namespace knows how the target is deployed (in-process, local-process, http, compose, ...). That is the other, orthogonal axis and it must never leak in here.

The ClientAdapter abstraction: the protocol a user implements to bind Jepsen
Lite to their target's *protocol*, plus the outcome signalling
(`fail!` / `info!`) and the exception -> `:type` wrapping their handlers rely
on.

Nothing in this namespace knows how the target is deployed (in-process,
local-process, http, compose, ...). That is the other, orthogonal axis and it
must never leak in here.
raw docstring

lite.core

The runner: config in, verdict out.

A run is Jepsen's own pipeline with the parts a Lite user shouldn't have to see kept internal -- generator -> the user's adapter (via the bridge) -> history -> the workload's checker.

The runner: config in, verdict out.

A run is Jepsen's own pipeline with the parts a Lite user shouldn't have to
see kept internal -- generator -> the user's adapter (via the bridge) ->
history -> the workload's checker.
raw docstring

lite.nemesis

Faults, the third axis. A user names an intent -- :crash, :pause, :partition -- and Lite picks the implementation that fits the target-type. Which faults are possible is a property of how the target is deployed, not of the workload or of the target's protocol.

Nemesis code never branches on the workload, and workloads never know a nemesis exists. The nemesis perturbs the target; the checkers simply observe what follows.

Faults, the third axis. A user names an *intent* -- `:crash`, `:pause`,
`:partition` -- and Lite picks the implementation that fits the target-type.
Which faults are possible is a property of how the target is deployed, not of
the workload or of the target's protocol.

Nemesis code never branches on the workload, and workloads never know a
nemesis exists. The nemesis perturbs the target; the checkers simply observe
what follows.
raw docstring

lite.target

Target-types: how the target under test is deployed, and therefore what its connection lifecycle looks like and which faults can be injected into it.

This is the second axis. The ClientAdapter binds Lite to the target's protocol and knows nothing about deployment; a target-type owns deployment and knows nothing about the protocol. Workloads know about neither.

Target-types: how the target under test is deployed, and therefore what its
connection lifecycle looks like and which faults can be injected into it.

This is the second axis. The ClientAdapter binds Lite to the target's
*protocol* and knows nothing about deployment; a target-type owns deployment
and knows nothing about the protocol. Workloads know about neither.
raw docstring

lite.target.compose

The :compose target-type: the target runs as docker-compose services that Lite brings up and takes down, with faults injected into the containers by Pumba.

This is the only target-type that can partition anything, and the reason is the same one that runs through the whole axis: a fault is possible when Lite owns the thing the fault happens to. :in-process owns an object, so it can destroy it. :local-process owns a process, so the kernel will kill or stop it. :compose owns a container -- and a container has a network interface of its own, which is what makes cutting it off meaningful. Lite cannot partition a process that reaches it over loopback, and it never pretends otherwise.

Faults are Pumba's, not Lite's:

crash pumba kill --signal SIGKILL, then the service is brought back up pause pumba pause --duration d (freezes the container's processes) partition pumba netem loss 100% (the container's traffic vanishes)

pause and partition heal themselves when their duration expires, so each is a single op rather than a start/stop pair. crash doesn't: something has to start the container again, and that something is Lite.

Config:

{:type :compose :file "examples/compose/docker-compose.yml" :service "kvs" ; which service to run and hit :container "jepsen-lite-kvs" ; its container_name, for Pumba :url "http://127.0.0.1:8080" ; its published endpoint :ready-timeout 120000 ; ms to wait for it to listen :fault-duration "5s" ; how long pause/partition last :pumba-image "ghcr.io/alexei-led/pumba" :tc-image "ghcr.io/alexei-led/pumba-alpine-nettools:latest"}

Pumba runs as a container itself, with the docker socket mounted, so there is nothing to install beyond Docker. netem needs tc, which the target's own image is unlikely to have; --tc-image lends it one, which is why it is configured rather than assumed.

The `:compose` target-type: the target runs as docker-compose services that
Lite brings up and takes down, with faults injected into the containers by
Pumba.

This is the only target-type that can partition anything, and the reason is
the same one that runs through the whole axis: a fault is possible when Lite
owns the thing the fault happens to. `:in-process` owns an object, so it can
destroy it. `:local-process` owns a process, so the kernel will kill or stop
it. `:compose` owns a container -- and a container has a network interface of
its own, which is what makes cutting it off meaningful. Lite cannot partition
a process that reaches it over loopback, and it never pretends otherwise.

Faults are Pumba's, not Lite's:

  crash      pumba kill --signal SIGKILL, then the service is brought back up
  pause      pumba pause --duration d      (freezes the container's processes)
  partition  pumba netem loss 100%         (the container's traffic vanishes)

`pause` and `partition` heal themselves when their duration expires, so each
is a single op rather than a start/stop pair. `crash` doesn't: something has
to start the container again, and that something is Lite.

Config:

  {:type :compose
   :file      "examples/compose/docker-compose.yml"
   :service   "kvs"                      ; which service to run and hit
   :container "jepsen-lite-kvs"          ; its container_name, for Pumba
   :url       "http://127.0.0.1:8080"    ; its published endpoint
   :ready-timeout  120000                  ; ms to wait for it to listen
   :fault-duration "5s"                  ; how long pause/partition last
   :pumba-image "ghcr.io/alexei-led/pumba"
   :tc-image    "ghcr.io/alexei-led/pumba-alpine-nettools:latest"}

Pumba runs as a container itself, with the docker socket mounted, so there
is nothing to install beyond Docker. `netem` needs `tc`, which the target's
own image is unlikely to have; `--tc-image` lends it one, which is why it is
configured rather than assumed.
raw docstring

lite.target.endpoint

Where a target listens, and whether anything is there yet.

Shared by the target-types that reach the target over a socket -- :http, which connects to one somebody else started, and :local-process, which waits for one it started itself. Deliberately protocol-blind: a TCP connect is the most a deployment axis can honestly ask, since it knows the target's address but not its API, and has no business inventing a health endpoint the target may not have.

Where a target listens, and whether anything is there yet.

Shared by the target-types that reach the target over a socket -- `:http`,
which connects to one somebody else started, and `:local-process`, which
waits for one it started itself. Deliberately protocol-blind: a TCP connect
is the most a *deployment* axis can honestly ask, since it knows the
target's address but not its API, and has no business inventing a health
endpoint the target may not have.
raw docstring

lite.target.http

The :http target-type: the target is already running, outside Lite, and Lite connects to it over HTTP. That is the whole of the relationship.

Lite does not start, stop, restart or kill an :http target -- it doesn't own the process, and may not even be on the same machine as it. So there is no fault it can inject, which is why lite.nemesis/validity marks every intent ✗ for this row. What remains of the deployment axis is the connection, and that differs from :in-process in the way the Connection protocol anticipated: an in-process instance is one object every worker shares, while an external target hands each worker its own client.

Nothing here knows what the target's HTTP API looks like. Speaking the target's wire protocol is the ClientAdapter's job -- the other axis -- and the adapter carries whatever endpoint its handler needs. The :url here is a deployment fact, not a protocol one: where the target is supposed to be listening, so that Lite can say so plainly when nothing is.

Config:

{:type :http :url "http://localhost:8080" ; where the target is listening :connect-timeout 2000} ; ms to wait for it; optional

The `:http` target-type: the target is already running, outside Lite, and
Lite connects to it over HTTP. That is the whole of the relationship.

Lite does not start, stop, restart or kill an `:http` target -- it doesn't
own the process, and may not even be on the same machine as it. So there is
no fault it can inject, which is why `lite.nemesis/validity` marks every
intent ✗ for this row. What remains of the deployment axis is the
connection, and that differs from `:in-process` in the way the `Connection`
protocol anticipated: an in-process instance is one object every worker
shares, while an external target hands each worker its own client.

Nothing here knows what the target's HTTP API looks like. Speaking the
target's wire protocol is the ClientAdapter's job -- the other axis -- and
the adapter carries whatever endpoint its handler needs. The `:url` here is
a deployment fact, not a protocol one: where the target is supposed to be
listening, so that Lite can say so plainly when nothing is.

Config:

  {:type :http
   :url  "http://localhost:8080"  ; where the target is listening
   :connect-timeout 2000}          ; ms to wait for it; optional
raw docstring

lite.target.in-process

The :in-process target-type: the target runs inside Lite's own JVM, as an object the ClientAdapter opens.

There is one instance, and every worker shares it, the way real clients share one server. It lives in an atom rather than being captured by each worker, because the crash nemesis replaces it mid-run and clients must follow it to the new one.

The `:in-process` target-type: the target runs inside Lite's own JVM, as an
object the ClientAdapter opens.

There is one instance, and every worker shares it, the way real clients share
one server. It lives in an atom rather than being captured by each worker,
because the crash nemesis replaces it mid-run and clients must follow it to
the new one.
raw docstring

lite.target.local-process

The :local-process target-type: the target runs as a separate OS process on this machine, and Lite runs it -- starts it, holds its handle, and can signal it.

This is the first target-type where Lite owns something the operating system will kill for it, and that is the whole point of it. :in-process can simulate a crash by destroying an object and building another, which exercises a target's recovery code but not the process boundary; :http can't crash anything at all. Here crash is SIGKILL -- a real kill -9, with no chance to flush, close files or run shutdown hooks -- followed by a real restart that has to find its data on disk. pause is SIGSTOP and SIGCONT: the process is still there, still holding its port, and simply stops answering.

What it does not test: loss of writes that reached the OS but were never fsynced. SIGKILL kills the process, not the kernel, so the page cache is still written back. It tests process death, restart and recovery -- real and worth testing -- and it catches data buffered in the target's own memory. True power-loss testing needs a fault injector below the filesystem.

Config:

{:type :local-process :command ["java" "-cp" ... "-m" "my.driver" "--port" "8080"] :url "http://127.0.0.1:8080" ; how to tell it has come up :dir "/tmp/target" ; working directory; optional :env {"FOO" "bar"} ; extra environment; optional :log "/tmp/target.log" ; where its output goes; optional :ready-timeout 30000} ; ms to wait for it to listen

:command must be the target's own process, not a shell that launches it: Lite signals the process it started, and a shell wrapper would take the signal instead of the target.

The `:local-process` target-type: the target runs as a separate OS process
on this machine, and Lite runs it -- starts it, holds its handle, and can
signal it.

This is the first target-type where Lite owns something the operating system
will kill for it, and that is the whole point of it. `:in-process` can
simulate a crash by destroying an object and building another, which
exercises a target's recovery code but not the process boundary; `:http`
can't crash anything at all. Here `crash` is `SIGKILL` -- a real `kill -9`,
with no chance to flush, close files or run shutdown hooks -- followed by a
real restart that has to find its data on disk. `pause` is `SIGSTOP` and
`SIGCONT`: the process is still there, still holding its port, and simply
stops answering.

What it does *not* test: loss of writes that reached the OS but were never
fsynced. SIGKILL kills the process, not the kernel, so the page cache is
still written back. It tests process death, restart and recovery -- real and
worth testing -- and it catches data buffered in the *target's own* memory.
True power-loss testing needs a fault injector below the filesystem.

Config:

  {:type :local-process
   :command ["java" "-cp" ... "-m" "my.driver" "--port" "8080"]
   :url     "http://127.0.0.1:8080"  ; how to tell it has come up
   :dir     "/tmp/target"            ; working directory; optional
   :env     {"FOO" "bar"}          ; extra environment; optional
   :log     "/tmp/target.log"        ; where its output goes; optional
   :ready-timeout 30000}              ; ms to wait for it to listen

`:command` must be the target's own process, not a shell that launches it:
Lite signals the process it started, and a shell wrapper would take the
signal instead of the target.
raw docstring

lite.workload

Workloads: what ops to run, and what counts as correct.

A workload is a map:

:generator the op stream :final-generator optional; ops to run once the op stream is done and any time limit has expired, such as a final read :checker the verdict over the resulting history :wrap-client optional (fn [client] client'), for workloads whose ops need translating before the user's handler sees them :test-opts optional extra test-map entries the workload's own generator or checker reads (bank's accounts and starting total) :concurrency how many workers this workload wants, if it cares

Each workload also documents the handler contract it expects -- which :f values arrive, and what the handler should return -- in its own namespace.

Workloads know nothing about target-type or faults; those are separate axes.

Workloads: what ops to run, and what counts as correct.

A workload is a map:

  :generator    the op stream
  :final-generator  optional; ops to run once the op stream is done and any
                time limit has expired, such as a final read
  :checker      the verdict over the resulting history
  :wrap-client  optional (fn [client] client'), for workloads whose ops need
                translating before the user's handler sees them
  :test-opts    optional extra test-map entries the workload's own generator
                or checker reads (bank's accounts and starting total)
  :concurrency  how many workers this workload wants, if it cares

Each workload also documents the handler contract it expects -- which `:f`
values arrive, and what the handler should return -- in its own namespace.

Workloads know nothing about target-type or faults; those are separate axes.
raw docstring

lite.workload.bank

The :bank workload: move money between accounts and check that none is created or destroyed.

Generator, defaults and checker come from jepsen.tests.bank. This is the only workload that requires multi-key atomic transactions: a transfer debits one account and credits another, and a read that catches a target halfway through sees a total that doesn't add up. A target without atomic multi-key updates cannot meaningfully run :bank.

Handler contract

:init value is {account balance} -> set those balances. Sent once, before any transfers, to open the accounts. :transfer value is {:from a, :to b, :amount n} -> atomically debit from and credit to. If the transfer is rejected -- say the balance is too low -- call lite.client/fail!. :read value is nil -> return a map of every account to its balance.

The workload chooses the accounts and the starting total, and seeds them itself through the same handler as every other op -- the target starts empty and knows nothing about banking. Reads must cover all the accounts, and (unless :negative-balances?) balances may not go negative.

The `:bank` workload: move money between accounts and check that none is
created or destroyed.

Generator, defaults and checker come from `jepsen.tests.bank`. This is the
only workload that requires **multi-key atomic transactions**: a transfer
debits one account and credits another, and a read that catches a target
halfway through sees a total that doesn't add up. A target without atomic
multi-key updates cannot meaningfully run `:bank`.

## Handler contract

  :init      value is {account balance} -> set those balances. Sent once,
             before any transfers, to open the accounts.
  :transfer  value is {:from a, :to b, :amount n} -> atomically debit
             `from` and credit `to`. If the transfer is rejected -- say the
             balance is too low -- call `lite.client/fail!`.
  :read      value is nil -> return a map of every account to its balance.

The workload chooses the accounts and the starting total, and seeds them
itself through the same handler as every other op -- the target starts empty
and knows nothing about banking. Reads must cover all the accounts, and
(unless `:negative-balances?`) balances may not go negative.
raw docstring

lite.workload.counter

The :counter workload: increment a counter and read it back.

Deliberately the weak one. The checker only verifies each read falls between the sum of acknowledged increments and the sum of attempted ones, so it catches gross errors -- lost or duplicated increments -- and proves little else.

Handler contract

:add value is a positive integer -> increment the counter by it :read value is nil -> return the counter's current value

Reads must return an integer, and the counter only ever goes up.

The `:counter` workload: increment a counter and read it back.

Deliberately the weak one. The checker only verifies each read falls between
the sum of acknowledged increments and the sum of attempted ones, so it
catches gross errors -- lost or duplicated increments -- and proves little
else.

## Handler contract

  :add   value is a positive integer -> increment the counter by it
  :read  value is nil                -> return the counter's current value

Reads must return an integer, and the counter only ever goes up.
raw docstring

lite.workload.register

The :register workload: independent linearizable registers.

Generators come from jepsen.tests.linearizable-register; the verdict comes from Knossos via jepsen.checker/linearizable. Lite supplies neither -- it only wires them to the user's handler.

Handler contract

Ops arrive with :f and a :value payload for one logical register:

:read value is nil -> return the register's current value :write value is v -> set the register to v; return normally :cas value is [old new] -> if the register holds old, set it to new; otherwise call lite.client/fail!

The workload runs many independent registers so each checked history stays short (linearizability checking is NP-hard). Which register an op addresses is on the op as :key; a target that holds a single register can ignore it.

The `:register` workload: independent linearizable registers.

Generators come from `jepsen.tests.linearizable-register`; the verdict comes
from Knossos via `jepsen.checker/linearizable`. Lite supplies neither -- it
only wires them to the user's handler.

## Handler contract

Ops arrive with `:f` and a `:value` payload for one logical register:

  :read   value is nil        -> return the register's current value
  :write  value is v          -> set the register to v; return normally
  :cas    value is [old new]  -> if the register holds `old`, set it to
                                 `new`; otherwise call `lite.client/fail!`

The workload runs many independent registers so each checked history stays
short (linearizability checking is NP-hard). Which register an op addresses
is on the op as `:key`; a target that holds a single register can ignore it.
raw docstring

lite.workload.set

The :set workload: add distinct elements, then read them all back.

The lightest workload -- it asks nothing of the target beyond adds and a read, and catches lost writes and phantom elements.

Handler contract

:add value is one element -> add it to the collection; return normally :read value is nil -> return the whole current collection

The shape matters: a long run of adds, then a single final read. The checker compares that read against the adds it acknowledged.

The `:set` workload: add distinct elements, then read them all back.

The lightest workload -- it asks nothing of the target beyond adds and a
read, and catches lost writes and phantom elements.

## Handler contract

  :add   value is one element -> add it to the collection; return normally
  :read  value is nil        -> return the whole current collection

The shape matters: a long run of adds, then a single final read. The checker
compares that read against the adds it acknowledged.
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