Slap.KV (slap_kv v0.4.0)

Copy Markdown View Source

A durable key-value store on Slap.KV.Cluster. Rows are addressed by a partition and a key, both non-empty binaries: the partition places the row on a shard (Slap.KV.Cluster.shard_for(partition)), and within a partition rows are in key order, so a partition can be scanned. There are no scans across partitions.

Each row has a version, an integer that changes with every write to it. put/4 and delete/3 take if_version: to write only if the row is at that version (:absent for a row that must not exist), so a read-modify-write can detect a concurrent change.

A write returns once it is durable, and reads return only durable rows: a read never sees a write that could still be lost. Pass cluster: to use a module defined with use Slap.KV.Cluster, otp_app: :my_app. Run one KV cluster per VM.

A write may be applied after its caller has stopped waiting for it (see :timeout below). A write with a :deadline is not applied after it, and a read with consistency: :linearizable sees every write applied before it: together, they let a caller find out when a write it gave up on can no longer happen.

Errors shared by all calls

Invalid row data and scan bounds return {:error, {:bad_request, reason}}. Invalid control options (:consistency, :with_versions, :timeout, :cluster) and unknown option names raise ArgumentError.

  • {:error, :unavailable} - the shard is stopping, moving or has no owner, or a write failed. A write that fails this way may still have been applied: read the row to find out rather than retry blindly.
  • {:error, :timeout} - no reply within :timeout (default 30 s). A write may still be applied.
  • {:error, :deadline_exceeded} - a write's :deadline passed before it was applied. It was not, and will not be, applied.
  • {:error, {:bad_request, reason}} - invalid row data or scan bounds.

Summary

Types

The value and version returned by get/3.

A row returned by scan/2, with its key and optional version.

Functions

Deletes a row. Returns :ok once the deletion is durable, whether or not the row existed. With if_version: version the row must be at version, and with deadline: it must be applied by then (as for put/4).

Reads a row. Returns {:ok, %{value: value, version: version}}, or {:ok, nil} when there is none.

Writes a row. Returns {:ok, version} once it is durable.

Lists a partition's rows in key order. Returns {:ok, %{rows: [{key, value}], cursor: cursor}}, where cursor is nil after the last page. With with_versions: true, rows are {key, value, version}.

Types

entry()

@type entry() :: %{value: binary(), version: version()}

The value and version returned by get/3.

error()

@type error() ::
  :deadline_exceeded
  | :timeout
  | :unavailable
  | {:bad_request, term()}
  | {:conflict, version() | nil}

key()

@type key() :: binary()

page()

@type page() :: %{rows: [scan_entry()], cursor: binary() | nil}

partition()

@type partition() :: binary()

scan_entry()

@type scan_entry() :: {key(), binary()} | {key(), binary(), version()}

A row returned by scan/2, with its key and optional version.

version()

@type version() :: non_neg_integer()

Functions

delete(partition, key, opts \\ [])

@spec delete(partition(), key(), keyword()) :: :ok | {:error, error()}

Deletes a row. Returns :ok once the deletion is durable, whether or not the row existed. With if_version: version the row must be at version, and with deadline: it must be applied by then (as for put/4).

get(partition, key, opts \\ [])

@spec get(partition(), key(), keyword()) :: {:ok, entry() | nil} | {:error, error()}

Reads a row. Returns {:ok, %{value: value, version: version}}, or {:ok, nil} when there is none.

With consistency: :linearizable, the read goes through the row's partition writer, behind the writes it has received, and returns once every write it has applied is durable and a write of its own confirms that its node still owns the shard. Such a read sees every write that was applied before it, acknowledged or not; it costs a write. The default, :durable, reads what is durable without waiting, and may miss a write that is applied but not yet acknowledged.

put(partition, key, value, opts \\ [])

@spec put(partition(), key(), binary(), keyword()) ::
  {:ok, version()} | {:error, error()}

Writes a row. Returns {:ok, version} once it is durable.

With if_version: version the row must be at version, and with if_version: :absent there must be no row; otherwise the result is {:error, {:conflict, current}}, where current is the row's version, or nil if there is none.

With deadline: ms (a system time, System.os_time(:millisecond)) the write is applied only if the row's partition writer gets to it before then, by its node's clock; otherwise the result is {:error, :deadline_exceeded}. So once a clock reads ms plus the largest difference between nodes' clocks, the write can no longer be applied.

scan(partition, opts \\ [])

@spec scan(partition(), keyword()) :: {:ok, page()} | {:error, error()}

Lists a partition's rows in key order. Returns {:ok, %{rows: [{key, value}], cursor: cursor}}, where cursor is nil after the last page. With with_versions: true, rows are {key, value, version}.

Options:

  • :prefix - only keys that start with it.
  • :gte, :lt - only keys in this range.
  • :limit - rows per page, 1 to 1,000 (default 100).
  • :cursor - the previous page's cursor, to continue after it. An input of nil starts at the beginning of the selected range.
  • :with_versions - include each row's version (default false).

Each page is a separate read of the durable rows: rows written between pages appear in later pages if their keys come later. A binary cursor marks a key position and remains valid after that row is deleted. Only :cursor accepts nil; supplied :prefix, :gte, and :lt values must be binaries.