Core concepts
Repository internals
Lexicon discovery, inclusion proofs, and CAR encoding, decoding, and import staging.
Lexicon namespace discovery#
Atoll.Lexicon.Authority.discover/2 maps an NSID namespace to its repository DID.
For edu.university.dept.lab.blogging.getBlogPost, it queries exactly
_lexicon.blogging.lab.dept.university.edu. It removes only the final name segment,
reverses the authority labels, and normalizes only those labels to lowercase. The
case-sensitive schema name is preserved in the resulting record URI:
at://DID/com.atproto.lexicon.schema/NSID. This follows the
Lexicon publication/resolution rules
and NSID normalization rules.
There is no parent-domain search or handle/HTTPS fallback. Duplicate identical DID claims are accepted; conflicting valid claims fail. Invalid/unrelated TXT values are ignored within a bounded response: at most 32 records, 16 chunks per record, 255 bytes per chunk, 2052 bytes per joined record, and 16 KiB total. Overlong DNS names and reserved authority domains fail before lookup. The system DNS resolver has a three-second timeout; no additional application cache is used.
Authority.resolve/2 additionally forces fresh DID resolution to obtain the
repository signing key and PDS endpoint through the existing protected resolver.
The account handle need not match the namespace: DNS delegation establishes that
binding. This trusts the configured/system DNS resolution path; it does not add
DNSSEC validation. Discovering a repository does not yet authenticate a schema.
Tests use mock DNS/HTTP.
Atoll.Lexicon.Fetcher.fetch/2 performs that discovery and fetches
com.atproto.repo.getRecord from the resolved HTTPS PDS. It pins a public IP while
preserving TLS hostname verification and the Host header, including nondefault
ports. It sends no account credentials, follows no redirects, performs no automatic
retries, and accepts at most 256 KiB of uncompressed response JSON. Duplicate JSON
keys and nesting deeper than 64 levels are rejected.
The returned URI must exactly match the delegated DID, schema collection, and
normalized NSID. The value must declare the schema record type, Lexicon version 1,
matching id, and nonempty named definitions. Its canonical DAG-CBOR SHA-256 CID
must match the response CID. Success returns the document and its DID/URI/CID
provenance without installing it. Before returning success, it also requests
com.atproto.sync.getRecord from the same PDS, bounded to 2 MiB, and verifies the
CAR's signed commit with the freshly resolved account key. The MST inclusion path
must identify the same schema CID as the JSON response. Invalid, missing,
wrongly signed, or mismatched proofs fail without an unsigned fallback. A record
change between the two reads can therefore fail the request; no automatic retry
is made. Returned provenance includes the signed commit CID and revision.
This still trusts DNS and DID resolution to identify the publisher and signing key. A valid older commit signed by that key can prove historical inclusion; the proof does not independently establish the latest revision. The PDS's current-record response supplies the freshness assertion. Signing-key rotation during retrieval may cause verification to fail until a new request resolves the updated identity.
Atoll.Lexicon.Catalog.resolve/2 follows external references through the same
independent discovery/fetch process for each namespace. It supports the record
schema language accepted by the operator catalog loader, strips the publication
$type envelope, and validates all definitions and reference targets before
returning a catalog. Missing dependencies, unsupported definitions, invalid union
targets, and unresolved fragments reject the whole result. Cycles are permitted;
each namespace is fetched at most once. Bundled schemas take precedence and are
never fetched remotely or replaced.
One resolution permits at most 16 remote documents and 1 MiB of aggregate re-encoded schema JSON, in addition to the fetcher's per-response limit. A 30-second elapsed-time budget is checked between fetches and before accepting the completed catalog. An in-flight DNS/HTTP operation retains its individual timeout, so this is not a hard 30-second cancellation deadline. Results contain the validated catalog plus DID/URI/CID/commit/revision provenance for each remote document; no global configuration, cache, or database state changes. Transport/clock options are trusted test hooks, never request parameters.
With ATOLL_NETWORK_LEXICONS=true, authenticated create/put/applyWrites requests
resolve unknown collections before opening the write transaction. A batch shares
one catalog budget across its collections and dependencies. Bundled and
operator-configured schemas retain precedence. validate: false skips discovery;
known local collections and delete operations also need no network lookup.
Required validation rejects unavailable or unsupported remote catalogs. Optimistic
validation proceeds with unknown status when catalog resolution fails, while
still enforcing known local schemas. A successfully resolved record schema is
enforced in either mode, and invalid records are rejected before mutation.
Catalog resolution is all-or-nothing: a failed batch dependency discards the
remote catalog for that request. Authorization is checked before discovery and
again under the repository lock, so session revocation during lookup prevents the
write. The remote catalog is a per-request snapshot; no schema cache is installed.
Internal write APIs and CAR import retain their existing data-integrity policy.
Verifying record inclusion proofs#
Atoll.MST.Proof.verify/3 checks a partial search path against a supplied root and
returns its record CID, or nil for a proven absence. Missing path blocks are
errors, not evidence of absence. Visited blocks must match their DAG-CBOR CIDs,
use canonical encoding and key-prefix compression, sort keys within the inherited
subtree bounds, and follow the SHA-256-derived tree levels without skipping empty
intermediate nodes. Limits are 129 visited nodes, 1 MiB per node, 2 MiB of retained
path-node bytes and 10,000 entries per node. Sibling subtrees need not be supplied; the verifier does not claim to
validate their structure or the complete repository.
Atoll.MST.Proof.fetch/4 performs the same validation through a CID reader that
returns {:ok, bytes}. It returns only the visited blocks and the record CID (or
nil for absence), and never requests sibling subtrees. The default retained-node
budget is 2 MiB; trusted internal callers can set max_bytes between 1 byte and
64 MiB. Crossing that budget stops traversal with mst_proof_too_large. Missing,
corrupt or malformed selected nodes fail rather than becoming absence proofs.
com.atproto.sync.getRecord now uses this loader against the verified current
commit's signed root, with the repository head locked throughout export. It checks
the commit revision and the requested record-index entry against the proof, then
loads only the selected record block. It no longer reconstructs the complete MST
from every index row. Unrelated index rows or sibling blocks are not audited by
this endpoint; full export retains its whole-index consistency check. Selected
index inconsistencies still fail closed. Inactive repositories remain unavailable.
Tests compare fetched paths with constructed canonical trees, enforce exact byte
budgets, and distinguish unrelated damage from selected-path corruption.
Whole-tree traversal is available for streamed HTTP exports as described below.
Signing-key rotation and recovery also validate the stored tree against the streamed
record index. Recovery reads and verifies distinct record bodies one at a time
before repairing custody, including same-key restoration. Changed-key transitions
stage authenticated tree/record CIDs in batches of 256 in a transaction-local
PostgreSQL temporary table, deduplicate there, and construct the retained revision
array inside PostgreSQL. They do not copy potentially damaged old membership
indexes or accumulate tree/membership maps in Elixir. The database role needs the
TEMP privilege. Temporary tables are dropped after insertion or at transaction
end; quota checks, custody changes and sync events remain atomic. PostgreSQL still
materializes the revision array and its indexes, and large repositories incur
multiple traversal passes while holding the write lock.
Staged imports validate canonical MST nodes and each record through bounded
traversal, then replay lazy record/CID streams during atomic publication. Record
rows insert in batches of 1,000; revision membership uses the same PostgreSQL
staging mechanism as key transitions. Migration re-signing replaces the commit
without collecting reachable CIDs. Validation still exhausts every branch before
publication, verifies record hashes/types/data-model limits, and rejects missing
blocks even when public storage contains them. Repeated traversal trades extra
reads for bounded tree metadata. The stage must remain open through publication.
Import blob-reference reconciliation marks imported rows with the advancing revision, including surviving path/CID pairs. PostgreSQL finds previously referenced owned blobs absent from the new revision, queues byte cleanup in batches of 256, and deletes stale ownership and reference rows in the publication transaction. Moving a reference preserves ownership; uploads never referenced by the old repository remain staged. Other accounts' ownership is preserved, and physical byte deletion remains the cleanup worker's responsibility. Rollback restores the previous references and ownership without leaving cleanup jobs.
Staged HTTP imports now keep the archive, CID/offset index and revision membership
on disk or in PostgreSQL, with bounded application traversal and publication batches.
The buffered Snapshot.decode/5 API collects its archive, records and blocks, with
a default 64 MiB retained-output accounting budget. Trusted callers may override
it with max_buffer_bytes:. Each expanded record path is charged for its bytes,
CID and a 128-byte map-entry allowance; each unique reachable block is charged for
its bytes, CID and a 96-byte allowance. Shared record blocks count once, while
every referencing record path counts separately. Unreferenced input blocks are
discarded, and reachable CIDs are consumed incrementally without building a list.
Exceeding the output budget returns {:error, :car_too_large} without a partial
snapshot. The buffered input still has its independent 64 MiB CAR/100,000-section
limits; input buffers and traversal state can coexist with output maps, so this
accounting budget is not an exact heap-size limit. Snapshot.from_stage/4 keeps
its streamed interface and does not collect these output maps.
Ordinary record mutations now load previous values only for their at most 200
changed paths, persist their changes transactionally, and feed a 128-row sorted
record cursor to MST.Builder.build/3. Completed canonical nodes are emitted to
block storage immediately. The builder keeps pending ancestor entries under a
16 MiB accounting budget, with 10,000 entries/1 MiB per node, 100,000 emitted nodes
and one million input records. It preserves exact prefix compression and empty
intermediate levels. Invalid/unsorted input, exhausted budgets or failed writes
abort construction; partial emissions require a transaction or staging. Final
revision membership is built through bounded traversal and PostgreSQL staging.
Quota, blob-reference, head and event failures roll the entire mutation back.
Writes still rebuild the whole tree and perform work proportional to record count;
this is not incremental path mutation. Pending-byte accounting is not a precise
BEAM heap measurement. Public batch/body limits still bound prepared records, and
the database materializes revision arrays. MST.load/3 now uses the canonical
traversal directly instead of collecting records and rebuilding the tree. It
returns the same fully buffered tree, but caps retained metadata at 64 MiB by
default (max_bytes: overrides this for trusted callers). Accounting charges
each encoded node plus its CID and a 96-byte map-entry allowance, and each expanded
record path plus its CID and a 128-byte allowance. Expanded paths count even when
prefix compression makes the encoded tree small. This is an accounting budget,
not an exact heap-size limit; traversal's independent 16 MiB pending budget and
node/depth/count limits also apply. Exhausting the retained budget returns
{:error, :mst_too_large} without a partial tree or further block reads. Invalid
or noncanonical trees return {:error, :invalid_mst}. The loader accepts either a
block map or a reader callback and never reads record bodies or unrelated blocks.
Callers needing a stream should use MST.Traversal.stream/3 directly.
MST.new/2, put/4 and delete/3 apply the same default 64 MiB retained-metadata
accounting limit and max_bytes: option. Construction checks the record map's
expanded paths and CIDs before allocating sorted construction entries, then
charges encoded nodes as they are emitted. It also enforces one million records,
100,000 unique nodes, 10,000 entries per node and a 1 MiB encoded-node limit.
Exhaustion returns {:error, :mst_too_large} without a partial tree; invalid
paths/CIDs return {:error, :invalid_mst}. Mutation failures leave the input tree
unchanged. Options apply to each call, rather than being stored in the tree.
These helpers remain fully buffered: caller-owned inputs, previous tree versions,
sorting lists and encoding buffers can coexist with the output, so accounting is
not a promise of a 64 MiB total process heap. The separate streaming builder remains
the production write path and the buffered constructor remains an independent
canonical reference implementation.
describeRepo streams its complete collection array instead of materializing all
names in the application. PostgreSQL deduplicates and sorts names in bytewise
order; a cursor fetches 128 names at a time and JSON encoding uses batches of 128.
The DID document and reciprocal handle lookup resolve before opening this cursor.
The repository is rechecked as active and held with a shared head lock during
enumeration, with a 60-second transaction timeout. Client cancellation stops
enumeration and closes the cursor; failures after headers have been sent abort
the response rather than returning a truncated but valid JSON collection array.
No collection names are silently omitted and there is no pagination change.
Trusted callers can use Repositories.stream_collections/2 or
Repositories.Description.stream/3 to consume the cursor within its owning
transaction. Buffered Repositories.collections/2 retains a default 64 MiB
accounting budget (max_bytes:), charging each name's bytes plus a 64-byte list
entry allowance. Description.get/2 exposes this as max_collection_bytes:.
Overflow returns {:error, :repository_metadata_too_large} without a partial
list. These budgets do not constrain PostgreSQL's distinct/sort work or promise
an exact process heap size; the HTTP endpoint uses streaming rather than imposing
the buffered helper's collection limit.
The metadata-memory checklist covers the audited repository write, staged import, full/differential export, historical read, record/block proof, key-transition and collection-inventory paths, plus explicitly budgeted buffered helpers. Streaming does not make full-tree operations constant-time or eliminate database revision arrays and indexes. Callers of streaming helpers must preserve their lazy contract.
Atoll.MST.Editor.apply/4 edits a caller-authenticated partial tree using up to
200 {:put, path, cid} / {:delete, path} operations. It lazily reads search paths
and split/merge boundaries, preserves unvisited subtrees as opaque CID links,
re-encodes canonical prefixes, and creates/prunes empty intermediate/root nodes
as needed. Results contain the new root, fetched original nodes and generated
nodes; nothing is persisted. An encoded-byte accounting limit (default 16 MiB,
including a per-map-entry allowance) bounds both node caches. Per-node size/entry
limits and key-derived levels constrain decoding. This is not a precise heap
measurement or a full-tree audit; the caller must authenticate the starting root
and validate the intended operation semantics.
Tests compare hundreds of edits with independent full canonical reconstruction, exercise root-height changes, and replay a mixed batch's inverse using only its fetched boundary proof. Missing required nodes, bad CIDs, malformed operations, and exhausted budgets fail closed. This editor is the foundation for compact inductive commit proofs and is now used by firehose encoding.
Repositories.CommitProof.build/4 reverses up to 200 unique-path operations over
the new tree and requires the resulting root to match prevData (or the canonical
empty root for the initial commit). It separately proves each operation's new CID
or deletion absence against the original new tree, preventing false operation CIDs
from passing merely because reversal restores the old root. It returns only the
original nodes fetched for search and split/merge boundaries; generated inverse
nodes are not emitted. The retained-node editor budget is 16 MiB, and unchanged
subtrees are neither read nor audited.
Commit-event encoding also checks the previous commit's DID and revision against
the event's since, adds hash-verified created/updated records of at most one
million bytes, and emits a CAR capped at two million bytes. Oversized proofs or
editor-budget exhaustion retain the existing commit-only #sync fallback. Invalid
operations, mismatched roots or missing/corrupt required nodes fail closed. Empty
commits need only the root proof; deletion proofs include required merge/split
boundaries. Historical replay uses immutable retained blocks, not the current head.
Tests reverse emitted partial slices, reject tampered operation metadata, and show
that a mixed edit in a 2,000-record repository uses less than one tenth of its full
export. Eight offline cases have additionally been verified by the independent
TypeScript @atproto/repo 0.8.10 implementation: it reconstructs prior roots from
Atoll's exact partial proof slices, including a maximum mixed batch. Normal tests
pin the reference roots and verified slices without requiring Node. See
test/fixtures/mst/README.md for provenance, scope and
reproduction commands. Signed event transport and live relay/client interoperability
remain separate pending suites.
Atoll.Repositories.RecordProof.verify/5 accepts a CAR of at most 2 MiB, an
expected DID/path, and a trusted signing curve/public key. It checks the first CAR
root's version-3 commit signature, follows the MST proof to the requested record,
and returns that record plus its CID, commit CID, and revision. The record block
must be present, hash correctly, fit the 1,000,000-byte record limit, and declare
the requested collection. Wrong keys, mismatched DIDs, absent records, truncated
proofs, and tampered blocks fail. Both k256 and P-256 are supported.
The caller must authenticate the signing key and establish freshness separately. A valid older signed commit can still prove historical inclusion; this API neither resolves identity nor proves that the commit is the latest. Network Lexicon retrieval invokes this verifier and requires its record CID to match the JSON response. The structure follows the repository specification.
Streaming CAR encoding#
Atoll.CAR.encode_stream/2 returns {:ok, enumerable} of binary CARv1 chunks:
first the header, then one framed section per supplied {cid, bytes} block. It
preserves source order (including duplicate sections), so a caller can place the
commit first without sorting or collecting the archive. CID hashes and the 2 MiB
section limit are checked before each block is emitted. The header retains the
buffered codec's limits; the streamed archive has no aggregate 64 MiB cap.
The source must be lazy to keep memory bounded. Consumer cancellation or a block
validation exception releases an upstream Stream.resource, including resources
owned by a database stream. Invalid roots fail before enumeration. Invalid blocks
raise ArgumentError during enumeration; callers must abort the transfer because
earlier chunks may already have been sent. They must also provide snapshot
consistency, authorization, and transfer-duration limits. Tests consume a stream
larger than 64 MiB without collecting it and check resource cleanup on cancellation
and validation failure. com.atproto.sync.getRepo now uses this encoder through
Atoll.Repositories.stream_export/4. It verifies the signed snapshot before sending
headers, then sends the commit first, MST nodes, and record bodies read individually
from PostgreSQL. Incremental exports omit blocks owned by the retained since
revision; unknown revisions still receive a full export. Client chunk-write failure
halts enumeration and releases the transaction. Corruption found after headers
aborts the stream rather than attempting a JSON error response.
A shared repository-head lock prevents mutation, deletion, or revision compaction
from invalidating the snapshot while the callback consumes it. Export credentials
are rechecked after acquiring the snapshot lock. The transaction has a 60-second
timeout; slow readers hold a database connection and can delay repository writes.
Snapshot validation uses Atoll.MST.Traversal.stream/3 to read the stored signed
tree without rebuilding it or retaining a complete record map. Canonical nodes
are checked with the same node validator used by individual proofs: CID hashes,
encoding, prefix compression, strict key ranges, hash-derived levels and required
intermediate nodes. Every signed record entry is compared with the database index
in bytewise order using a 128-row cursor. Missing/extra index entries, wrong CIDs
and corrupt stored nodes reject the snapshot before the consumer sends headers.
The previous ability to reconstruct a missing stored node from the index is not
used by streamed exports.
Traversal retains pending branches, with defaults of 129 levels, 100,000 nodes,
1,000,000 records and a 16 MiB metadata accounting budget. Each node is limited to
1 MiB and 10,000 entries. The accounting budget charges serialized node bytes,
expanded key bytes and a fixed per-entry allowance; it is not an exact BEAM heap
measurement. Completed branches release their charge, so total tree size need not
fit this budget. Trusted internal callers can adjust traversal count/accounting
limits; malformed or over-budget traversal raises Atoll.MST.TraversalError.
After validation, delivery walks the nodes again and streams distinct record CIDs and bodies from PostgreSQL one row at a time. Incremental exports pin the selected revision and query membership in PostgreSQL instead of loading its entire block array into application memory. This adds node reads and, for incremental exports, membership queries; normalized per-revision membership and higher-throughput traversal remain possible optimizations. Full exports still inspect all metadata before sending, and large repositories can reach the transaction deadline.
The complete archive, record/CID map and whole MST are not accumulated by this
HTTP export path. The buffered Repositories.export/3 API consumes the same
validated stream, rejecting archives above 64 MiB or 100,000 block sections while
collecting chunks. It retains the encoded output but does not reconstruct a whole
MST, record map or revision membership set. Its CAR now uses the stream's block
order (commit first), and corrupt stored nodes fail rather than being rebuilt from
the record index. Lazy corruption becomes an error result without returning partial
bytes. Legacy buffered snapshot/MST APIs retain the metadata costs described above. The streaming callback must finish
consuming the enumerable before returning. The legacy CAR.decode/1 and import_archive/3 APIs remain buffered;
HTTP imports use incremental decoding and staging. Tests compare full and incremental block sets with the buffered codec,
exercise cancellation/corruption, and stream a repository larger than 64 MiB.
Incremental CAR decoding#
Atoll.CAR.Decoder accepts arbitrary binary chunks through feed/4, emitting a
validated header followed by hash-verified blocks to a synchronous callback. The
callback returns {:cont, accumulator} to continue or {:halt, accumulator} to
cancel. A partial varint or section is retained between calls. finish/1 must
succeed at end of input; otherwise the archive is truncated. Duplicate blocks are
emitted and counted, leaving deduplication to the consumer.
The decoder buffers one section in 4 KiB segments: headers are limited to 64 KiB
and block sections to 2 MiB. Fragmented input never requires rebuilding the full
section on each feed. Default archive limits are 1 GiB and 1,000,000 block sections;
new/1 accepts explicit max_bytes and max_blocks limits. Input validation and
consumer cancellation stop processing immediately. Tests cover every split point
in a sample CAR, byte-at-a-time input, malformed/truncated framing, hash failures,
limits, cancellation, and decoding over 64 MiB without accumulating output.
Callbacks can observe valid blocks before a later archive error. Import consumers must stage those blocks and publish nothing until final framing, repository signature, MST completeness, ownership, and quota validation all succeed. This codec performs no database writes and does not authenticate repositories. Public HTTP imports use this decoder through private staging before transactional publication.
Private CAR staging#
Atoll.CAR.Stage.with_chunks/3 consumes an enumerable of binary chunks with the
incremental decoder, writing each unique verified block to a request-private
temporary file. A second private file stores its CID-to-offset/length index;
only roots, decoder buffers and fixed index metadata remain in application memory. The consuming callback is invoked only after
Decoder.finish/1 succeeds. Stage.read/2 reads and rechecks a staged block's hash
while inside that callback. Duplicate sections still count against decoder limits
but do not consume additional staging space.
StageIndex uses 48-byte slots containing a CID, byte offset and length. The
index reserves two slots per permitted block section (96,000,000 logical bytes at
the default million-section limit). Sparse-file support can reduce physical disk
allocation; capacity planning must allow the full logical size plus block bodies.
Each upload gets a random HMAC-SHA256 key to prevent clients from preparing a
chosen collision cluster. Linear probing wraps at the end of the file and stops
after at most 128 slot reads. Probe exhaustion or index I/O failure rejects staging;
it never falls back to an unbounded scan or in-memory map. Reads reject invalid
slots, out-of-range offsets/lengths, and bytes that fail the requested CID hash.
Duplicate blocks occupy one data/index entry but count toward the decoder limit.
The random staging directory has mode 0700 and is removed, with both files closed,
after normal return, invalid/truncated input, limit rejection, or exceptions.
No data is inserted into public block storage. Disk/creation failures produce a
staging error. Callers can choose a trusted directory and decoder byte/block
limits; defaults use the system temporary directory and the decoder's 1 GiB /
1,000,000-section limits. These options must not come from client request input.
A VM or host crash can leave private temporary files behind, so operational
stale-file cleanup and disk-capacity planning remain necessary when operating public
streaming imports. This stage validates transport integrity only;
repository signatures, complete MST membership, account authorization, and quotas
must still pass before publication. Public HTTP imports now use this staging path.
Atoll.Repositories.Snapshot.from_stage/4 validates a staged repository using the
same signature, five-minute future-revision bound, bounded canonical MST traversal,
complete reachable-block membership, and record data-model checks as the buffered
snapshot decoder. It returns repository metadata, replayable record and reachable
CID streams, and a read_block callback instead of maps of records or block bodies.
Unreferenced staged blocks are omitted; shared record CIDs can occur repeatedly
in the CID stream and are deduplicated during revision staging. Records are checked individually
against the 1,000,000-byte limit and their collection's $type.
The callback remains valid only inside Stage.with_chunks/3; publishing imports
must finish all staged reads there. Validation does not retain a reconstructed
MST, record map or in-memory staging index. This validator does not publish
blocks or update accounts, blob references, quotas, or event streams. The HTTP importer uses this validator before staged publication.
Atoll.Repositories.import_staged/3 publishes a stage inside its owning callback.
It authenticates the management session, validates the staged snapshot, then
rechecks authorization and the captured repository head/key under the normal
mutation locks. Record bodies are read individually for blob-reference indexing
and block insertion. Records, retained-revision membership, quota enforcement,
and the sync event commit atomically; a failure rolls them all back. Only reachable
blocks are inserted. Identical retries remain idempotent.
Migration imports retain their existing policy: a deactivated account may validate
against its recorded source key and re-sign with the local repository key. The
source commit is replaced in the published block set, while its CID/revision are
retained in migration metadata. The destination stays deactivated. Tests cover
staged publication, unreachable-block exclusion, quota rollback including blob
references, and cross-curve migration re-signing/retries. Public HTTP imports now use a stateful bounded reader with
Stage.with_reader/4, retaining the updated connection through completion or
failure. Tests include an HTTP upload larger than 64 MiB with duplicate sections.
Staging files are owned by temporary supervised lease processes that monitor their
request owner. If the request crashes or is killed, the lease closes the file and
removes its private directory. Normal completion explicitly releases the lease;
it is not restarted after owner termination. ATOLL_IMPORT_CONCURRENCY sets the
maximum simultaneous staging leases per node (default 16, range 1–64). Admission
failure maps to HTTP 503 before reading the body. This bounds concurrent staging
reservations locally; it is independent of Redis/PostgreSQL request budgets and
does not reserve actual free disk space. With the 1 GiB per-upload cap, plan disk
capacity for the configured concurrency as well as metadata and filesystem overhead.
VM/host crashes or abrupt termination of a lease itself still require stale-file
cleanup. Tests kill a request process and await lease termination to verify cleanup,
and verify rejection/re-admission at the concurrency limit without sleeps.