Object storage: PUT whole immutable objects, range GET one record,
DELETE for retention, LIST by prefix. The default is the local
filesystem; S3, GCS, Azure Blob and OCI are drop-in adapters.
This module is both the behaviour and the instance-scoped facade. Every call names a scope:
:segments— compacted hook segments (Ankusa.Storage): thestorage.blob_store, with every new key understorage.key_prefix. Nodes that share one bucket give themselves distinct prefixes ("node-a/"), because a segment key (seg/<first>-<last>) is only unique per node.:backup— store backups (Ankusa.Store.Backup): thebackup.blob_storewhen set, else the segment store, withstorage.key_prefix. A backup belongs to one node's store, like its segments.
Claim packs do not go through this facade: Ankusa.ClaimCheck.Stores
writes and reads them across claim_check.blob_stores, never prefixed.
The prefix is applied once, when a new object is named (object_key/3);
the full key is what the archive catalogue records, and put/get/
get_range/delete take full keys verbatim. Changing storage.key_prefix
therefore moves only new segments, never strands written ones.
Encryption at rest
The object-store adapters take an :sse option (Ankusa.BlobStore.SSE) that
asks the provider to encrypt every object a put/4 writes, with a KMS key or
the provider's own. It is per store, so it covers the segment store, the
backup store and every claim store as each is configured.
Transport
The HTTP adapters share their transport options (Ankusa.BlobStore.Options):
one deadline per operation, jittered retries that honour Retry-After
(Ankusa.BlobStore.Retry), a breaker per bucket (Ankusa.BlobStore.Breaker),
multipart uploads above a size, integrity checksums
(Ankusa.BlobStore.Checksum) and a connection pool per endpoint
(Ankusa.BlobStore.Pool).
Summary
Callbacks
Every key under prefix, sorted, across all of the store's result pages. A
page that fails fails the call: a partial listing is never presented as the
whole.
put/4, but only if no object exists under key: {:error, :already_exists}
when one does, with that object left exactly as it was. The adapters use the
store's own precondition (S3 and Azure If-None-Match: *, GCS
ifGenerationMatch=0, OCI if-none-match: *), so the check and the write are
one atomic request. Ankusa.ClaimCheck writes every pack with it: a reused
or colliding pack id then refuses instead of overwriting claims that are
already referenced.
Functions
Every key of this node's objects in scope under prefix (behind storage.key_prefix for :segments and :backup), as full keys.
The full key a new object in scope is written under: key behind
storage.key_prefix. The compactor names each segment with it and records
the result in the archive catalogue, so a later prefix change only moves new
segments; the ones already written are read back under the key they were
written with.
Settle an :already_exists: :ok when key already holds exactly data (a
conditional PUT that landed, whose answer was lost and which was then sent
again, finds its own object), else {:error, :already_exists}. A store that
cannot be read back is {:error, :already_exists} too: the caller mints a new
key rather than assume.
The second object store backups are also written to (backup.secondary),
under the same storage.key_prefix; nil when none is configured.
The {module, opts} a scope resolves to on instance.
Check the :sse option and the transport options (Ankusa.BlobStore.Options)
of every blob store the config names (storage.blob_store,
backup.blob_store, backup.secondary, claim_check.blob_stores).
Types
Callbacks
@callback get_range( instance :: atom(), key :: String.t(), offset :: non_neg_integer(), length :: pos_integer(), opts :: keyword() ) :: {:ok, binary()} | {:error, term()}
@callback list(instance :: atom(), prefix :: String.t(), opts :: keyword()) :: {:ok, [String.t()]} | {:error, term()}
Every key under prefix, sorted, across all of the store's result pages. A
page that fails fails the call: a partial listing is never presented as the
whole.
@callback put_if_absent( instance :: atom(), key :: String.t(), data :: iodata(), opts :: keyword() ) :: :ok | {:error, :already_exists} | {:error, term()}
put/4, but only if no object exists under key: {:error, :already_exists}
when one does, with that object left exactly as it was. The adapters use the
store's own precondition (S3 and Azure If-None-Match: *, GCS
ifGenerationMatch=0, OCI if-none-match: *), so the check and the write are
one atomic request. Ankusa.ClaimCheck writes every pack with it: a reused
or colliding pack id then refuses instead of overwriting claims that are
already referenced.
An object that exists with exactly the bytes being written is :ok
(same_object/5): a retried request whose first attempt landed is not a
collision with another writer.
Functions
@spec get_range(atom(), scope(), String.t(), non_neg_integer(), pos_integer()) :: {:ok, binary()} | {:error, term()}
Every key of this node's objects in scope under prefix (behind storage.key_prefix for :segments and :backup), as full keys.
The full key a new object in scope is written under: key behind
storage.key_prefix. The compactor names each segment with it and records
the result in the archive catalogue, so a later prefix change only moves new
segments; the ones already written are read back under the key they were
written with.
@spec same_object(module(), atom(), String.t(), iodata(), keyword()) :: :ok | {:error, :already_exists}
Settle an :already_exists: :ok when key already holds exactly data (a
conditional PUT that landed, whose answer was lost and which was then sent
again, finds its own object), else {:error, :already_exists}. A store that
cannot be read back is {:error, :already_exists} too: the caller mints a new
key rather than assume.
@spec secondary_config(Ankusa.Config.t()) :: {module(), keyword(), String.t()} | nil
The second object store backups are also written to (backup.secondary),
under the same storage.key_prefix; nil when none is configured.
The {module, opts} a scope resolves to on instance.
@spec validate_config!(Ankusa.Config.t()) :: :ok
Check the :sse option and the transport options (Ankusa.BlobStore.Options)
of every blob store the config names (storage.blob_store,
backup.blob_store, backup.secondary, claim_check.blob_stores).