fabric/store/backend
The store port: what a backend implements to keep Fabric’s runs.
Most applications use fabric/store.in_memory, fabric/store.directory
or fabric_postgres; this module is for writing another backend, and
fabric/store/conformance checks one.
A run is one encoded record (versioned JSON, see fabric docs) with a
revision. A backend supplies three functions over encoded records, and
Fabric writes only through insert and compare_and_set, so two owners
can never both advance the same revision.
The backend contract:
get(run)returns the latest committed record with its revision, orNotFound.insert(run, record)storesrecordas revision 1, or returnsAlreadyExistswhen the run exists.compare_and_set(run, expected, record)storesrecordas revisionexpected + 1if and only ifexpectedis the current revision, as one atomic step; otherwise it returnsConflict(current)(orNotFound). Two concurrent calls with the sameexpectedmust not both succeed, also across processes and machines that share the backend.- Any other failure is
Unavailable(reason). A backend function that crashes is treated asUnavailable. After anUnavailablewrite, Fabric reads the run back: finding exactly the record it wrote at the revision it wrote confirms the write. Every record Fabric writes carries a fresh write token, so another writer’s record is never mistaken for this one. A write that is still unconfirmed staysUnavailable: its outcome is unknown, since the backend may still perform it later.
Leases. A leased backend (LeasedBackend) keeps, next to each run’s
record, a lease: an owner and an expiry judged by the backend’s own
clock. Several nodes sharing one database use it to agree on which
node drives a run; the lease never authorizes a write, which the
revision alone decides. Its contract, over the one above:
get(run)returns the record, its revision, and itsHolder:Free, orHeld(owner, live),livewhile the expiry is ahead.insert(run, record, lease)stores revision 1 with the lease of aClaimorSeize(held by its owner for itsttl), and none for aHoldorRelease.compare_and_set(run, expected, record, lease)checks the revision first (NotFound,Conflict(current)), then the lease condition in the same atomic step:Hold(owner)only whileownerholds the lease (live or expired) and leaves it unchanged;Claim(owner, ttl)only while the lease is free, held byowner, or expired;SeizeandReleasewhoever holds it. A refused condition isLeaseRefused(holder)and writes nothing. After the write, aClaimorSeizeowner holds the lease forttland aReleasefrees it.renew(owner, runs, ttl)extends, tottlfrom now, the lease of each ofrunsthatownerholds while it is still live, and returns those runs. An expired lease is not renewed, so a renewal sent before a handoff (which leaves the lease expired) and applied after it does not make the lease live again. It changes no revision.claim_expired(owner, ttl, limit)claims forownerup tolimitruns whose lease is held and expired, and returns them. It changes no revision, and concurrent calls never return the same run.claim_ready(owner, ttl, limit)claims free runs whose validated idle dependency is unseen/changed or whose scheduled observation is due. Save the observation key, dependency revision and backend claim time together with the lease; a concurrent child write remains discoverable. A scheduled wait is first due immediately, then after its saved interval from the last ready claim. Preserve its key/time across same-key writes and metadata refresh. Only the backend’s clock determines eligibility. An absoluteAt(due)wait is eligible when backend UTC milliseconds reachdue. Claiming it does not consume it: after release it remains eligible until the execution leaves that wait. Never treat it as a polling interval.PollandChangedcan also carry an absolute deadline. Either the ordinary trigger or that deadline makes the wait eligible, independently. Claims are disjoint and bounded, change no execution bytes/revisions or retention ages, and refuse stale source revisions/projection versions. Usefabric/store/discoveryto derive metadata from supported records.now()returns UTC Unix milliseconds from the same backend clock used for leases and discovery. Reading it changes no stored data. Clock errors propagate; leased stores never substitute a node’s local time. Clock corrections may move time backward, so samples need not be monotonic.
fabric/store/conformance.checks checks a leased backend against this
contract; fabric/store/conformance.memory is one kept in memory, for
tests.
Types
Who holds a run’s lease in a leased backend.
pub type Holder {
Free
Held(owner: String, live: Bool)
}
Constructors
-
Free -
Held(owner: String, live: Bool)live: the lease’s expiry, judged by the backend’s clock, is still ahead.
What a write in a leased backend does to the run’s lease, in the same
atomic step as the record (see Leases). ttl is in milliseconds, from
the backend’s clock.
pub type Lease {
Hold(owner: String)
Claim(owner: String, ttl: Int)
Seize(owner: String, ttl: Int)
Release
}
Constructors
-
Hold(owner: String)Only while
ownerholds the lease, live or expired; the lease is unchanged. -
Claim(owner: String, ttl: Int)Only while the lease is free, held by
owner, or expired;ownerthen holds it forttl. -
Seize(owner: String, ttl: Int)Whoever holds the lease;
ownerthen holds it forttl. -
ReleaseWhoever holds the lease; it is then free.
The functions of a leased backend over encoded records (see Leases).
pub type LeasedBackend {
LeasedBackend(
now: fn() -> Result(Int, StoreError),
get: fn(String) -> Result(Current, StoreError),
insert: fn(String, String, Lease) -> Result(Nil, StoreError),
compare_and_set: fn(String, Int, String, Lease) -> Result(
Nil,
StoreError,
),
renew: fn(String, List(String), Int) -> Result(
List(String),
StoreError,
),
claim_expired: fn(String, Int, Int) -> Result(
List(String),
StoreError,
),
claim_ready: fn(String, Int, Int) -> Result(
List(String),
StoreError,
),
)
}
Constructors
-
LeasedBackend( now: fn() -> Result(Int, StoreError), get: fn(String) -> Result(Current, StoreError), insert: fn(String, String, Lease) -> Result(Nil, StoreError), compare_and_set: fn(String, Int, String, Lease) -> Result( Nil, StoreError, ), renew: fn(String, List(String), Int) -> Result( List(String), StoreError, ), claim_expired: fn(String, Int, Int) -> Result( List(String), StoreError, ), claim_ready: fn(String, Int, Int) -> Result( List(String), StoreError, ), )Arguments
- now
-
UTC Unix milliseconds from the backend’s lease/discovery clock.
- insert
-
insert(run, record, lease). - compare_and_set
-
compare_and_set(run, expected, record, lease). - renew
-
renew(owner, runs, ttl): returns the runs renewed, those whose leaseownerholds live. - claim_expired
-
claim_expired(owner, ttl, limit): returns the runs claimed. - claim_ready
-
Claims free runs with a changed dependency, due observation or deadline. Atomically save the observed key, dependency revision and backend claim time with its lease. Claims change no execution revision or retention age.
What a backend function reports. Unavailable is any failure other than
the outcomes the contract names; its outcome is unknown for a write.
pub type StoreError {
NotFound
AlreadyExists
Conflict(current: Int)
LeaseRefused(holder: Holder)
Unavailable(reason: String)
}
Constructors
-
NotFound -
AlreadyExists -
Conflict(current: Int)The expected revision is no longer current.
-
LeaseRefused(holder: Holder)A leased backend refused the write’s lease condition (see Leases):
holderis the run’s lease as the backend found it. Nothing was written. -
Unavailable(reason: String)