SmolBox.Store behaviour (SmolBox v0.1.0)

Copy Markdown View Source

Atomic host-store boundary for managed execution.

Every mutation is one transaction. Failures must roll back record and worker changes together. Reads are authoritative and must never translate unavailable, corrupt, or unknown-schema storage into not_found. There is no fallback store.

accept inserts only an initial validated record. An identical scoped fingerprint returns the existing record even when the pending queue is full; conflicting fingerprints fail. Queue limits count accepted work across this store namespace.

Worker leases enforce one active owner within a shared store. Claims and record versions fence subsequent writes, not already-sent HTTP. A new claim generation cannot authorize replay of dispatching/running/unknown work. Operators must map each physical worker to one stable ID and one shared store authority.

Reservation atomically checks the worker lease, execution claim, record version, queue deadline, and CPU/memory/disk/slot totals. It persists worker and opaque machine identity before creating a VM. Release requires completed cleanup and confirmed absence once a worker has been assigned. Unknown or failed-cleanup records retain reservations until this condition is met.

Due queries are bounded keyset scans ordered by {next_due_at_ms, scope, id}. A fresh process can restart at a nil cursor; notifications and PIDs are not state. Completed clean records are retained for identity lookup, not silently removed. Adapters must document retention and database/storage limits independently.

find_machine resolves a worker/name assignment through durable evidence. Its index is updated atomically with reservation and retained after cleanup. Names cannot be assigned to another execution on that worker. Missing, unavailable, corrupt, or incompletely migrated indexes must not be conflated. This lookup proves an assignment, not the current machine's incarnation or ownership.

Implementations may share the pure record operations, but must independently pass the adapter conformance suite and demonstrate their transaction semantics.

Summary

Types

capacity()

@type capacity() :: %{
  slots: pos_integer(),
  cpus: pos_integer(),
  memory_mb: pos_integer(),
  disk_gb: pos_integer()
}

context()

@type context() :: term()

cursor()

@type cursor() :: {non_neg_integer(), String.t(), String.t()} | nil

guard()

@type guard() :: %{
  owner: String.t(),
  generation: pos_integer(),
  version: pos_integer()
}

lease()

@type lease() :: %{
  owner: String.t(),
  generation: pos_integer(),
  until_ms: non_neg_integer()
}

resources()

@type resources() :: %{
  slots: non_neg_integer(),
  cpus: non_neg_integer(),
  memory_mb: non_neg_integer(),
  disk_gb: non_neg_integer()
}

result()

@type result() :: {:ok, SmolBox.Execution.t()} | {:error, SmolBox.Error.t()}

Callbacks

accept(context, t, pos_integer)

@callback accept(context(), SmolBox.Execution.t(), pos_integer()) ::
  {:ok, SmolBox.Execution.t(), :inserted | :existing}
  | {:error, SmolBox.Error.t()}

cancel(context, key, non_neg_integer)

@callback cancel(context(), SmolBox.Execution.key(), non_neg_integer()) :: result()

capabilities(context)

@callback capabilities(context()) ::
  {:ok, %{schema: 1, durable: boolean(), atomic: true}}
  | {:error, SmolBox.Error.t()}

claim(context, key, t, non_neg_integer, pos_integer)

claim_worker(context, t, t, non_neg_integer, pos_integer)

@callback claim_worker(
  context(),
  String.t(),
  String.t(),
  non_neg_integer(),
  pos_integer()
) ::
  {:ok, lease()} | {:error, SmolBox.Error.t()}

due(context, non_neg_integer, cursor, pos_integer)

@callback due(context(), non_neg_integer(), cursor(), pos_integer()) ::
  {:ok, [SmolBox.Execution.t()], cursor()} | {:error, SmolBox.Error.t()}

fetch(context, key)

@callback fetch(context(), SmolBox.Execution.key()) :: result()

find_machine(context, t, t)

@callback find_machine(context(), String.t(), String.t()) :: result()

release(context, key, guard, non_neg_integer)

@callback release(context(), SmolBox.Execution.key(), guard(), non_neg_integer()) ::
  result()

reserve(context, key, guard, tuple, non_neg_integer)

@callback reserve(
  context(),
  SmolBox.Execution.key(),
  guard(),
  {String.t(), String.t(), capacity()},
  non_neg_integer()
) :: result()

usage(context, t)

@callback usage(context(), String.t()) :: {:ok, resources()} | {:error, SmolBox.Error.t()}

write(context, key, guard, keyword, non_neg_integer)

@callback write(context(), SmolBox.Execution.key(), guard(), keyword(), non_neg_integer()) ::
  result()