S3 multipart scale implementation plan
Goal
Raise the bounded default multipart part size used by S3ObjectStore and
reject an input stream before it can issue S3 part number 10,001. The result
must preserve one-pass direct streaming, incremental SHA-256, and the existing
abort-on-failure path.
Baseline before #150
crates/lake-objects/src/s3.rsownsS3ObjectStore, reads fixedMULTIPART_PART_BYTESchunks in source order, and admits a finite window ofUploadPartrequests withpart_number: i32.- The prior
checked_addprotected only integer overflow, whereas S3 accepts part numbers only from 1 through 10,000. A 5 MiB part size therefore reaches an invalid S3 request after roughly 48.8 GiB. ObjectErrorincrates/lake-objects/src/lib.rsis the crate’s public Snafu error surface. Preserve its redaction and typed-error conventions.- Existing
crates/lake-objects/tests/s3_localstack.rsowns real S3 protocol coverage. Do not require a 625 GiB test fixture; use a unit-level numeric boundary test for this change.
Scope
Modify only:
crates/lake-objects/src/lib.rscrates/lake-objects/src/checkpoint.rscrates/lake-objects/src/s3.rscrates/lake-objects/tests/s3_localstack.rscrates/lake-objects/AGENT.mdREADME.mddocs/design/managed-objects.mdspecs/issue-150-s3-multipart-scale.spec.mdverification/issue-150-s3-multipart-scale.md
Do not change the managed object trait, DataLocation, Query, Metasrv, SDK
API, S3 credentials, or object persistence layout. Do not pre-read the source
or retain more than the existing finite part-request window plus small read
buffer.
Steps
- Add a unit test in
s3.rsthat proves part 10,000 is valid and asking for a successor returns a typedObjectErrorbefore any request construction. Runcargo test -p lake-objects multipart_part_number_limit_rejects_10001st_part; it must initially fail because no boundary helper/error exists. - Define the S3-specific typed error in
ObjectError. Ins3.rs, replace the fixed 5 MiB constant with a documented 64 MiB default and centralize the1..=10_000check. Apply that check only after reading another non-empty part and before constructing its upload future, so exactly 10,000 parts still complete successfully. Reuse the existingupload_nonemptyerror path so it aborts the multipart upload; the non-recoverable resumable path must abort and remove its checkpoint too. - Update reader allocation to use the new bounded part size, leaving the 64 KiB read buffer and existing source-order bounded request window unchanged.
- Preserve V1 checkpoint compatibility by accepting only the former 5 MiB
and new 64 MiB persisted sizes. On recovery, use the checkpoint’s accepted
size to rehash completed parts and partition the remaining pipeline; new
checkpoints continue to record 64 MiB. Add unit coverage for legacy
acceptance and unknown-size rejection, plus a production-helper regression
that drives remaining
5 MiB + 1 Binput as[5 MiB, 1 B]. Keep the LocalStack recovery case with one completed 5 MiB part and no remaining part to upload. - Update
lake-objects/AGENT.md, README, and managed-object design text to state the 64 MiB bound, the approximate default part-count capacity, and the deliberately typed ceiling for unknown-length streams and the V1 checkpoint compatibility boundary. - Run
mise run fmt, the bound test,cargo test -p lake-objects,mise run spec-lifecycle specs/issue-150-s3-multipart-scale.spec.md, andmise run gate. Record exact outcomes in the verification document.
Done criteria
- The 10,001st-part boundary has a unit test bound by the task spec.
- The code never hands S3 a part number outside 1..=10,000.
- A stream with exactly 10,000 parts completes the numeric boundary path.
- A valid 5 MiB V1 checkpoint resumes with its original partitioning, while an
unrecognized persisted part size is rejected before upload work begins; the
remaining pipeline emits
[5 MiB, 1 B], not one 64 MiB-default part. - Object bytes remain direct SDK-to-S3 streaming with bounded memory.
- Existing LocalStack multipart/abort tests remain enabled and pass when the integration environment is present.
Stop conditions
- Stop if the AWS Rust SDK rejects a 64 MiB
ByteStreamor an existing S3 contract requires the 5 MiB value exactly. - Stop if enforcing the boundary requires changing Query, Metasrv, the SDK
public API, or a persisted
DataLocationrepresentation. - Stop if an exact 10,000-part completion needs a multi-gigabyte fixture; test the pure numeric boundary instead and report the limitation.