ExStorageService core

This umbrella app owns object metadata, local content-addressed storage, cluster placement, durability, repair, scrub, drain, and safe cleanup. It uses Concord/VSR for metadata and does not use Ecto or an external database.

Embedding

Add the versioned core package without the S3 or admin applications:

def deps do
[
{:ex_storage_service, "~> 0.6"}
]
end

Disable the default local instance when the host owns its supervision tree:

config :ex_storage_service,
data_root: "/var/lib/singularity/ess",
blob_root: "/var/lib/singularity/ess/cas",
tmp_root: "/var/lib/singularity/ess/cas/tmp",
ra_root: "/var/lib/singularity/ess/ra",
metadata_root: "/var/lib/singularity/ess/concord",
instance_config: [auto_start: false, web_enabled: false]
config :concord,
data_dir: "/var/lib/singularity/ess/concord",
vsr: [
group_id: :ex_storage_service_metadata,
replica_id: node(),
members: [%{id: node(), endpoint: node()}],
storage: :file,
bootstrap: false
]

Concord 3 requires the explicit ordered :members list even for a standalone singleton. The umbrella's config/runtime.exs derives this same VSR shape automatically; an embedding host must configure Concord itself.

Then supervise an instance with stable roots. Staging and ready roots must be on the same filesystem so publication can use an atomic rename.

instance_options = [
instance: :singularity,
auto_start: false,
web_enabled: false,
workers: %{
multipart_gc: false,
content_gc: false,
cas_gc: false,
packer: false,
lifecycle: false,
cross_cluster_replication: false,
repair: false,
scrub: false
}
]
children = [{ExStorageService, instance_options}]

For direct content-addressed storage, derive options from the validated instance context and use ExStorageService.BlobStore.LocalCAS:

{:ok, context} = ExStorageService.context(instance_options)
blob_options = ExStorageService.Context.blob_store_options(context)
{:ok, staged} = ExStorageService.BlobStore.LocalCAS.stage(data, blob_options)
# staged.hash is the lowercase SHA-256 digest; staged.size is the byte count.
{:ok, ready} = ExStorageService.BlobStore.LocalCAS.commit(staged, blob_options)
# Commit is put-if-absent: identical content reuses the verified ready blob.
{:ok, %{size: size}} =
ExStorageService.BlobStore.LocalCAS.stat(ready.hash, blob_options)
{:ok, {:file, path, offset, length}} =
ExStorageService.BlobStore.LocalCAS.open(ready.hash, {10, 20}, blob_options)
:ok = ExStorageService.BlobStore.LocalCAS.verify(ready.hash, blob_options)

Call LocalCAS.discard/2 to abort a staged write. LocalCAS.commit/2 is the finalization/pinning boundary: it syncs the staged file and atomically publishes the immutable SHA-256 path. A successful commit may be retried safely and never overwrites different content. LocalCAS.open/3 returns a bounded file source; consumers should stream it rather than load object-sized data into memory.

Ready blobs live below objects/sha256/<first-two-hex>/<remaining-hex> under the configured blob root. Staging paths are private implementation details. Preserve the entire data, blob, temporary, and Concord metadata roots together during backup and upgrades. Package updates preserve ready-blob paths; any future incompatible on-disk or metadata migration will require an explicit release-note and migration boundary rather than an automatic destructive startup migration. Concord/VSR remains shared infrastructure, so only one metadata instance is supported per BEAM.

Operator tasks

Run source-tree tasks from the repository root:

PAGER=cat MIX_ENV=prod mix ess.metadata.schema_status --replication-factor 2
PAGER=cat MIX_ENV=prod mix ess.metadata.migrate_v2 --replication-factor 2
PAGER=cat MIX_ENV=prod mix ess.cluster.bootstrap
PAGER=cat MIX_ENV=prod mix ess.cluster.status
PAGER=cat MIX_ENV=prod mix ess.blob.locate SHA256
PAGER=cat MIX_ENV=prod mix ess.blob.audit --limit 100
PAGER=cat MIX_ENV=prod mix ess.repair.plan --shard 00 --limit 100
PAGER=cat MIX_ENV=prod mix ess.repair.run --shard 00 --limit 100
PAGER=cat MIX_ENV=prod mix ess.node.drain NODE_ID
PAGER=cat MIX_ENV=prod mix ess.node.status NODE_ID --limit 100

Inventory and plan commands are read-only. Migration must run with object writers quiesced after a verified metadata and blob backup. Repair and audit commands operate on bounded pages; process every reported cursor and owned shard before treating the result as a cluster-wide preflight.

Release images do not contain Mix or source files. Use the release RPC equivalents and exact backup, migration, activation, and rollback sequence in the cluster upgrade runbook.