fix(s3): harden burst load and large-object clones - #175
Open
forhappy wants to merge 34 commits into
Open
Conversation
forhappy
force-pushed
the
codex/s3-gateway-admission
branch
from
September 10, 2026 13:36
21dfa3d to
9238adb
Compare
Plan LFS publication against each backend's object, part-size, and part-count limits. Persist multipart completion validators so repeated S3 ranges do not rehash the complete LFS object.
forhappy
force-pushed
the
codex/s3-gateway-admission
branch
from
September 10, 2026 20:48
f8878c1 to
9155707
Compare
forhappy
force-pushed
the
codex/s3-gateway-admission
branch
from
September 10, 2026 21:21
231158c to
bddf02e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
replace the process-wide fail-fast semaphore with bounded FIFO control, read, and transfer admission pools so uploads cannot starve metadata or range reads
retain read capacity for the lifetime of streamed GET responses, including disconnect cleanup
add cancellation-aware backoff for high-fanout multipart part registration and return empty listings for unknown or unborn refs
heap-pin canonical readability maintenance to prevent worker-stack exhaustion after write bursts
commit an exact same-directory Git LFS tracking rule atomically with every large S3 object pointer, including retry-safe rule convergence
use the upstream Git attribute parser and glob matcher so quoted, escaped, unset, and nested rules follow Git semantics
make eager Crab clones pull historical LFS pointers even when older commits lack matching attribute rules
plan final LFS publication against the physical backend: S3 and GCS stay within 10,000 5 GiB parts, Azure stays within 50,000 4,000 MiB blocks, and GCS rejects completed objects above 5 TiB before reading selected parts
stream adaptive provider parts from 8 MiB payload blocks and serialize parts above the 32 MiB retained-payload budget, avoiding concurrent multi-gigabyte payload retention
bind successful LFS uploads to the multipart completion validator so parallel S3 range readers fetch only their requested slices instead of rehashing the whole object per range
eliminate assembled-object scratch above 64 MiB by hashing frozen multipart parts once and replaying them through a backpressured, size-and-SHA-verified LFS byte stream
give every UploadPart transfer a unique immutable payload identity and reclaim superseded or rejected payloads without risking a concurrent winner
reserve every backend part transfer before upload, account retired replacement payloads in the same registration CAS, and retain terminal capacity until late writers are fenced and cleaned
surface an explicit
fsckplusmetadb ownerrecovery path when repositories written by older builds lack verified Git visibility evidencelet a fully validated self-contained visibility proof survive retirement of its preferred catalog checkpoint without masking corrupt catalog metadata
cancel only debounced readability work; once canonical publication starts, drain its manifest/catalog/visibility boundary and coalesce later writes into a follow-up pass
persist per-upload expiry and staging-byte budgets, coordinate a fixed CAS-fenced capacity-slot catalog across replicas, and reconcile expired, terminal, and pre-publication-crash sessions from a bounded background sweep
bind multipart completion to a deterministic ref-journal publication plan so immutable commit evidence recovers the original outcome even after a later write replaces the object
add a separate internal management listener with process liveness and fresh repository-view readiness, plus CLI probes and bounded failure responses
export private Prometheus metrics with fixed-cardinality HTTP, full response-stream, and admission-pressure series, without request or credential identities
classify aggregate multipart maintenance as successful or degraded, expose bounded action/failure reasons and staleness, and retain per-slot failures that were previously visible only in logs
account content-spool and Xet-reconstruction temporary files through their full ownership lifetime, with bounded pressure, throughput, and I/O-failure metrics
qualify a digest-pinned non-root gateway image under a read-only root filesystem with RustFS, SigV4 traffic, presigned/tampered requests, range reads, and graceful SIGTERM
add an isolated Docker Compose profile with persistent RustFS data, protected file-mounted gateway credentials, dropped capabilities, bounded scratch, explicit initialization, safe shutdown, and an opt-in volume teardown
qualify durable multipart recovery across forced Compose gateway replacement and verify the completed object byte-for-byte
add a fail-closed Helm chart for Kubernetes 1.29/EKS with digest-pinned images, multi-zone replicas, strict pod security, bounded scratch, private management probes, rolling-update safety, and a disruption budget
export bounded logical object-store request, duration, in-flight, read-byte, and successful-write-byte metrics across full GET/HEAD/range/PUT/delete/list/copy and multipart lifecycles
report scratch-filesystem total, free, and process-available bytes on every scrape, with fail-closed probe health that cannot retain stale capacity after mount loss
atomically reserve scratch for declared and streamed request spools, Xet ranges, and generated Git packs, retaining bounded headroom and returning retryable SlowDown before disk exhaustion
require one process-shared, explicitly placed, byte-bounded immutable-read cache; prove descriptor-relative writes before listening and mount cache separately from request scratch
export fixed-cardinality memory/local/service cache hit, miss, failure, verified-byte, persistence-failure, and catalog usage/health telemetry without repository, object, or credential identity
prevent protocol-library debug/trace events from exposing signed requests, signature material, or raw malformed request bodies, and enforce the boundary even under
RUST_LOG=traceship opt-in, release-scoped PodMonitor and PrometheusRule resources with 14 alerts covering maintenance stalls, request/backend failures, admission saturation, and scratch/cache pressure
Failure evidence
before admission and maintenance fixes, write bursts could fail fast, 64-part completion exhausted state CAS retries, and canonical readability maintenance overflowed a Tokio worker stack
before the listing fix, ListObjectsV2 below an unborn ref returned InternalError
before the large-object fix, gateway-authored large objects cloned as LFS pointer text unless users ran
crab lfs pullmanuallythe previous hand parser misread quoted attribute patterns, causing special-character filenames to be cleaned through Crab/Xet instead of LFS
an unrooted basename rule also affected same-named descendants; generated rules now use
/filenamewithin the nearest attributes directoryfixed 8 MiB LFS backend parts exceeded the S3/GCS 10,000-part ceiling above about 80 GiB; a universal 5 GiB plan could also exceed the Azure 4,000 MiB block ceiling
fresh streamed LFS uploads did not persist their completion validator; a 320 MiB AWS high-level download issued 40 ranges and redundantly read and hashed about 12.8 GiB from the backend, taking 178.17 seconds
multipart completion previously assembled every selected part into a full-object local spool, making node scratch capacity an accidental object-size ceiling
deterministic part payload paths made same-ETag concurrent cleanup unsafe, while replacements accumulated every older backend payload
a process exit after backend upload but before part registration could leave unaccounted bytes, while an exit after replacement registration but before deletion could orphan the retired winner
the reused live repository emitted a generic missing-visibility warning after every write because older qualification generations predated the current proof contract
repaired historical proofs were rediscovered on every
fsckbecause reads preferred catalog proofs whose exact SlateDB checkpoints had retireda staggered signed-write sequence exposed that cancelling active readability maintenance could advance a manifest generation before its catalog-bound visibility proof was complete
active multipart sessions and registered staging bytes had no cross-process bound; abandoned Open uploads retained their slot and payloads indefinitely, and multipart listing scanned all historical state
the first live expiry/restart probe found that ListParts still exposed an Aborted session after its payload cleanup; Open-only loads now make every terminal session return NoSuchUpload
current-object attributes alone could not prove a successful multipart completion after a later overwrite; a retry could either remain fenced forever or evaluate its original condition against the later object
per-slot multipart reconciliation errors were logged and then folded into a nominally successful sweep, leaving operators unable to alert on stalled cleanup
content spools and Xet range reconstructions consumed local scratch without exposing live ownership, write rate, or destination-I/O failures; request-count admission alone could not diagnose volume pressure
backend storage calls previously exposed no latency, outcome, in-flight, or byte telemetry, leaving provider saturation, credential failures, and slow range streams indistinguishable from gateway pressure
scratch ownership metrics did not reveal unowned files, mount loss, or actual free capacity; a failed external filesystem probe could also leave stale healthy values in a dashboard
request-count admission and telemetry did not prevent concurrent spools, reconstruction, and Git-pack preparation from racing to exhaust scratch; reservation now serializes the capacity snapshot with in-process claims
the read-only image resolved the unlimited default cache beneath /var/lib/crab, so cache publications silently fell back to repeated provider reads; startup now rejects a missing or unwritable bounded cache mount
bounded cache placement alone did not reveal whether traffic hit memory, local disk, or a cache service, whether local persistence failed, or whether retained catalog accounting was healthy; the gateway now exports those signals without identity labels or payload scans
the pinned S3 protocol library logs complete signed requests and signature material at debug level and raw malformed XML bodies at error level; an operator enabling broad trace logging could expose reusable authorization material
gateway metrics existed without a chart-owned scrape target or executable alert semantics, so saturation, scratch/cache failure, backend authorization failure, and stalled multipart maintenance could remain dashboard-only signals
Verification
cargo test -p crab-storage --locked: 196 passed plus 2 credential-redaction integration testscargo test -p crab-lfs --locked: 56 passedcargo test -p crab-s3-gateway --locked: 110 passedcargo test -p crab-cache --locked --lib --features local-cache: 228 passedcargo test -p crab-cache-store --locked: 46 passedcargo test -p crab-cache-store --locked --features remote-client: 71 passed, including exact local-miss/service-range-hit observationscargo test -p crab-read --locked: 163 passedcargo test -p crab-metadata --locked --all-features: 368 passed, 1 ignored large-repository stress testcargo test -p crab-remote --test publication --features publication --locked: 13 passedcargo test -p crab-metadata --test plan_receipt_recovery --features storage --locked: 1 passedcargo test -p crab-git --locked: 266 passed, 1 ignored microbenchmarktargeted Crab eager-clone and explicit-repository LFS checkout tests passed
cargo clippy -p crab-read -p crab-s3-gateway --all-targets --locked -- -D warningscargo build --release -p crab-s3-gateway --lockedcargo fmt --all -- --checkandgit diff --checkmake architecture-check: 144 direct workspace dependency edges admitted; fixes the prior Multi-crate guardrails failureweb production build passed; link checker validated 398 HTML pages and 4,307 fragments
live RustFS-backed gateway: 64 simultaneous 5 MiB parts completed a 335,544,320-byte object while 75 concurrent HEAD/range probes succeeded
final-tree clone proof: 335,544,320-byte PUT, byte-identical 128-byte range, rooted escaped attribute rule, and fresh eager clone with SHA-256
9942003e84c1648820149cb7b82869eb1e6515ddd04951bd2c69f9273b09c053provider-aware live proof: fresh 335,544,320-byte random multipart upload, exact 1 KiB range at offset 256 MiB, no leaked scratch request directory, and full SHA-256
ac25495163208b6f448d266c595b4950b4fc08eaee2a1b5fde96e972bcf4c468bounded-scratch live proof: a brand-new 335,544,320-byte content hash completed from four durable 80 MiB parts in 56.64 seconds with only 6,317 bytes of peak completion scratch sampled every 5 ms; full download matched SHA-256
1ae20c0d8241a61aa2bef60b580be773bc594ccd8f28ad3924faf4b11fc083c2and a 4 KiB late range matched exactlyconcurrent completion proof: four new 83,886,080-byte objects completed simultaneously on one branch in 17.60 seconds with 7,482 bytes aggregate peak completion scratch and 222,560 KiB peak gateway RSS; all four full SHA-256 downloads and four late ranges matched
the rebuilt gateway retained the 320 MiB object across process restart; an 8 KiB range near 300 MiB matched in 0.515 seconds
the same AWS 40-range full download fell from 178.17 seconds before validator persistence to 2.05 seconds after it, with an identical full-object hash
boundary tests prove maximum-size plans remain below each provider part-size and part-count limit; live GCS/Azure transport qualification still requires CI credentials
multipart replacement unit proof: 32 concurrent writes to one part number leave exactly the catalog winner as the only backend payload
multipart crash proofs: in-flight bytes are durably quota-bound, a frozen session retains capacity until late cleanup, expired transfers wait through provider drain, and retired replacement payloads survive process exit for reconciler deletion
live RustFS replacement proof: backend payload count stayed at one across replacement, completion returned the replacement bytes with matching SHA-256, and terminal cleanup reduced the part prefix to zero objects
live signed replacement proof on the final binary: SigV4 create/upload/list/complete/GET replaced a 5 MiB part and reconstructed 6,684,672 exact bytes with SHA-256
404e6a006b7718f4c4a1616d45f6421d9ac17aef7625b59657cf2598bab1a884live visibility upgrade proof: a fresh repository published catalog-bound proofs at generations 1 and 2 without warnings;
crab fsck --repairbackfilled ten historical proofs in the reused repository,crab metadb owner --oncepublished generation 25, and a signed write/delete advanced through generations 26 and 27 without the warninglive historical-proof proof: before the fix, a repair reported 24 repaired and one failure but an immediate scan rediscovered 25 retired-checkpoint warnings; the fixed binary scans the same long-lived repository with
errors: 0,repair_failures: 0, andpassed: truelive maintenance-race proof: after repairing the reused repository at generation 29, six staggered signed PUT/readbacks and six staggered signed DELETEs completed while canonical maintenance ran; the fixed gateway emitted no maintenance warning and the current generation retained a valid visibility proof
repository maintenance proof:
crab repackreduced the live repository from 15 active packs to 1 (6,305,434 → 6,298,270 bytes); afterward a 1 MiB range and the full 6 MiB multipart object still matched the independent source bytes64 simultaneous session creates against one shared store admitted exactly 8 configured capacity slots and rejected 56
live two-process capacity proof: two uploads admitted, the third returned SlowDown, a session created on one replica aborted through the other, and the released generation was reused
live 10 MiB staging-budget proof: an 8 MiB part remained authoritative, a second 4 MiB part returned SlowDown, ListParts stayed at one, and only one backend payload remained
live restart/expiry proof: the session was durably Aborted, active listing returned zero, ListParts returned NoSuchUpload, and the backend payload prefix was empty
live historical-completion recovery proof: after a 6 MiB multipart publish was overwritten by a newer object, a fault-injected Completing record recovered from its immutable publication-plan receipt on restart, released its slot, removed all staged payloads, returned the original ETag on retry, and preserved the newer object byte-for-byte
live signed-request proof: AWS SigV4 header auth and presigned GET/PUT succeeded, a presigned range GET returned the exact 65,536-byte slice, and tampering with either a signed query or signed Content-Type returned
403 SignatureDoesNotMatchlive management-plane proof: the initialized long-lived repository returns
200for liveness/readiness, an uninitialized repository remains live but returns readiness503withRetry-After: 5, the S3 listener rejects/livezas unsigned traffic, and SIGTERM exits zerolive metrics proof on the rebuilt binary: signed list/PUT/HEAD/full GET/range GET/DELETE traffic produced nonzero HTTP duration and admission series; the scrape contained no bucket, key, principal, access key, or secret; management POST returned
405and the S3 listener required a signaturelive maintenance metrics proof: a clean RustFS-backed cycle reported success, duration, a nonzero last-success timestamp, seven bounded zero-valued failure reasons and no identity; a second fault-injected backend with a corrupt capacity record reported one degraded cycle, one reconciliation error, and no false last-success timestamp
packaged-image backend telemetry CI passed at commit
b7e4778f9d4: the clean non-root/read-only image produced positive logical backend read/write counters under signed list/PUT/full GET/range GET/delete traffic, released all 11 in-flight operation gauges, retained identity-free labels, rejected tampered/wrong-secret/unsigned requests, shut down cleanly, and recovered multipart state across Compose replacement in S3 gateway run 34523590765packaged-image metrics CI passed at commit
5782ad33ca4: the digest-pinned non-root/read-only image passed signed list/PUT/full GET/range GET/delete; valid and tampered presigned requests; wrong-secret and unsigned rejection; positive content-spool writes; fully released scratch gauges; zero bounded scratch I/O failures; identity-leak assertions; graceful termination; and Compose replacement recovery in S3 gateway run 34519602863packaged-image CI passed at commit
c7a1da7819a: digest-pinned build, non-root/read-only runtime, uninitialized readiness rejection, RustFS initialization, SigV4 PUT/GET/range/presign/tamper rejection, container health and graceful SIGTERMCompose startup, initialization, signed discovery, multipart recovery across forced gateway replacement, exact assembled-byte verification and shutdown passed in CI at
0d05f21f027Helm fail-closed values validation, manifest rendering, private management-port assertion, and strict Kubernetes 1.29 schema validation passed in CI at
a1a07cb01a5the packaged Linux image accepted a Kubernetes-style root-owned, process-group-readable credential (
root:10001, mode0440), then passed initialization, signed traffic, graceful shutdown, and Compose replacement recovery in S3 gateway run 34509816529backend observation proof covers routed and staged stores, high-level and resumable multipart, native multi-range reads, partial-body cancellation, fixed failure classes, 99 pre-registered bounded outcome series, workflow lint, strict clippy, and a release build
scratch-filesystem proof covers live total/free/available capacity, mount-loss zeroing, explicit probe health and failure counting, workflow lint, strict clippy, all 104 gateway tests, and a release build
scratch-admission unit proof covers bounded headroom, exact and incremental claims, reservation release, pre-body rejection for declared sizes, bounded rejection reasons, retry headers, and conservative Git-pack peak estimates
bounded-cache proof covers private descriptor-relative publish/sync/removal with no retained probe, explicit 64 MiB–1 PiB config validation, a fixed-cardinality configured-limit metric, Compose rendering, strict Helm lint, and strict Kubernetes 1.29 schema validation
packaged-image bounded-cache CI passed at commit
bddf02ee812: the non-root/read-only image rejected startup without its writable cache volume, created a private cache root with the configured 128 MiB ceiling, passed signed SigV4 and presigned traffic plus tamper/wrong-secret/unsigned rejection, preserved exact range reads, shut down gracefully, and recovered multipart state across Compose replacement in S3 gateway run 34531745429packaged-image cache-telemetry CI passed at commit
337e341c5f0: the non-root/read-only image passed signed SigV4 and presigned traffic plus tamper/wrong-secret/unsigned rejection; exported all nine fixed cache source/outcome series and three source-byte series; reported healthy catalog accounting with zero probe and persistence failures; exposed no bucket, key, principal, access key, or secret; shut down gracefully; and recovered multipart state across Compose replacement in S3 gateway run 34534587779packaged-image signed-request log-boundary CI passed at commit
3334726b62f: underRUST_LOG=trace, real AWS SigV4 header and presigned GET traffic, tampered presigned requests, wrong-secret and unsigned rejection, and exact ranges completed without emitting credential syntax, secrets, or reusable signatures; graceful shutdown and Compose multipart recovery also passed in S3 gateway run 34537164160packaged-image and Helm monitoring CI passed at commit
a77a297e7c1: Prometheus Operator resources remain absent by default, opt-in rendering preserves the private management port, strict Kubernetes 1.29 validation passes, and pinnedpromtoolsyntax plus healthy/faulting semantics prove all 14 alerts; the rebuilt non-root/read-only image also passed signed/presigned/tampered traffic, exact range reads, credential-safe trace logging, graceful shutdown, and Compose multipart recovery in S3 gateway run 34538754642packaged-image scratch-admission CI passed at commit
9155707507f: a signed sparse 500 MiB PutObject against the gateway 512 MiB tmpfs returned SlowDown before body materialization, published no object, incremented only the exhausted rejection reason, and released pending reservations to zero; signed and presigned traffic, tamper rejection, exact ranges, graceful shutdown, and Compose multipart recovery also passed in S3 gateway run 34528570397packaged-image scratch-capacity CI passed at commit
f8878c1584e: the non-root/read-only gateway reported positive total/free/available bytes and healthy zero-failure probe state from its 512 MiB tmpfs, then passed signed header and presigned traffic, tamper/wrong-secret/unsigned rejection, exact ranges, graceful termination, and Compose multipart recovery in S3 gateway run 34525303204Deliberate remaining profile limits
The gateway remains the documented frozen S3 compatibility profile rather than every AWS S3 control-plane feature. Gateway-authored large payloads use verified Git LFS storage; existing Crab/Xet pointer objects retain chunk-aware range reconstruction and Xet deduplication. Moving S3 uploads themselves onto Xet would be a separate storage-format migration requiring atomic pointer publication, GC reachability, clone/hydration, and SDK compatibility proof.
Each PutObject, UploadPart, and copied source range still uses scratch proportional to that individual request body or range, with concurrency bounded by admission. Large multipart completion no longer uses scratch proportional to the assembled object: it validates frozen durable parts, then replays new content through verified LFS publication. This costs a second backend read for a new content hash; already verified LFS content skips the replay. A future single-pass design would need provider-portable content-addressed promotion with the same integrity, retry, GC, and restart guarantees.
Open uploads now have a persisted absolute expiry, registered-byte quota, distributed active-session bound, and a separate transient quota shared by in-flight transfers and retired replacement payloads. Every backend part write is preceded by a durable reservation; replacement registration moves the old payload into durable retirement in the same CAS that selects the winner. Terminal sessions retain their capacity slot until pending writers clean themselves, while the reconciler reclaims crashed transfers only after the persisted expiry plus a ten-minute provider-drain grace period. This bounds physical staging to twice the configured per-upload budget without leaving crash-created bytes unaccounted.
A
Completingsession is intentionally never expired because its frozen part set may already have published the object. New completions persist the planned result and bind their commit to an immutable publication-plan receipt, so reconciliation remains exact after later overwrites; current-object attributes retain compatibility for older records. An intent with no provable committed result remains fenced because absence of a receipt cannot prove rejection. Terminal lifecycle records and publication-plan evidence are retained for idempotent retries, so retention-safe compaction remains future lifecycle work.The private management listener now exposes per-process request/admission saturation, aggregate multipart-reconciler health, lifecycle-owned content scratch, actual scratch-filesystem total/free/available capacity, pending reservations, retained headroom, and bounded capacity-rejection reasons with fixed labels. Logical backend latency, outcome, in-flight, and byte breakdowns are also exported with fixed labels. Provider wire-attempt telemetry, an explicit application-level Kubernetes policy cap, dashboards, and live alert delivery remain explicit production-observability work. The Helm chart now provides opt-in, release-scoped scrape and alert resources with 14 syntax- and semantics-tested rules. Cache-layer hit/miss/failure and verified-byte counters plus read-only catalog usage/health gauges are implemented with fixed labels; live Prometheus selection and Alertmanager receiver delivery remain unqualified. Kubelet remains authoritative when an
emptyDir.sizeLimitis not exposed as a filesystem quota.SlowDownunder a bounded saturated queue is intentional S3 backpressure, not silent loss. Every gateway-generated response now includesRetry-After: 1; clients should still use standard exponential backoff with jitter, while operators size per-instance transfer capacity and scale replicas from admission pressure. High Git-pack counts are handled outside request acknowledgement: continuously supervise onecrab metadb ownerper repository for bounded geometric repack and monitor its candidate/action fields, withcrab repackas the explicit catch-up command for an already fragmented repository.