Repository navigation
feat(store): add begin_write, a write made through a grant, with S3 multipart parts - #150
Open
earakely-scale wants to merge 13 commits into
Open
earakely-scale wants to merge 13 commits into
earakely-scale wants to merge 13 commits into
Conversation
…t, with S3 multipart parts ObjectStore.begin_write(object_url, media_type, max_bytes, kinds, expires_in) returns a PendingWrite: the grant to hand over, complete(uploaded) to make the object, and abort(). Leaving its with block without completing aborts it. * The default wraps issue_write_grant in one URL: an http-put-parts grant of one part when the receiver takes parts, else an http-put. complete checks the object landed at the reported size, so every store that issues write grants offers begin_write unchanged. * S3ObjectStore takes parts as a multipart upload created with the media type and presigned per part (64 MiB, at most MAX_PARTS). complete lists the stored parts and finishes the upload only when they are parts 1..n of the reported size, else aborts. * A failure in complete is UploadFailedError (a ValueError), a store that will not start the upload is GrantUnavailableError, and an abort never raises over the error that triggered it. * The @Local router routes begin_write through its write fence; a staged store refuses it rather than forward it to the store it wraps. * S3 clients from from_config sign virtual-hosted URLs, which name the bucket's region: a bucket under a day old answers the global endpoint with a 307, which a grant must not follow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… into edgararakelyan/store-begin-write
…chable abort from hiding the failure complete() now refuses an upload the party reports over grant.max_bytes before checking the stored bytes; together with that check, nothing larger than the grant's limit becomes an object. Aborting an S3 upload also swallows botocore connection and timeout errors, logging them, so the error that caused the abort is the one raised. The PendingWrite contract now says what aborting a one-PUT write can do: its object lands with the PUT and stays, since object stores do not delete. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A botocore connection or timeout error starting an upload is now GrantUnavailableError, so the caller offers its fallback, and one listing or completing the parts is UploadFailedError, so the caller fails only that write. Before, these escaped as BotoCoreError. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… into edgararakelyan/store-begin-write
…anges The grant's URL count and the parts complete() expects come from the protocol's part_ranges, the same rule the uploader sends by, rather than a copy of it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s accepted Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… into edgararakelyan/store-begin-write
… UploadFailedError complete() promises UploadFailedError when the store cannot finish a write. The one-PUT write let a store's own error from reading the object's metadata escape instead (a denied HEAD on S3, a provider error on GCS), which a caller isolating failures per item did not catch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n an @Local run Virtual-hosted addressing applied to every endpoint, so an S3-compatible one set through AWS_ENDPOINT_URL_S3 (MinIO, say) was sent to bucket.minio:9000 instead of minio:9000/bucket. It now applies only when the client sends to AWS. begin_write joins the writes an @Local run must not make to the configured store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vateLink's included Regional virtual-hosted addressing now applies only on AWS's own S3 hosts, with no addressing style configured: a PrivateLink endpoint, also under amazonaws.com, keeps the path style it is addressed by, and an AWS profile's s3.addressing_style is no longer overridden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Merged the updated #149 in; no conflicts of its own. Two fixes in the follow-up commits:
|
This branch has not been deployed
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.
Stacked on #149 (which is on #147): merge those first, then this retargets to
main.Summary
A grant-issuing store can now take an object a remote party uploads in parallel parts. The issuer stays the one that makes, checks or discards the object.
ObjectStore.begin_write(object_url, *, media_type, max_bytes, kinds, expires_in=None)returns aPendingWrite:grant: theWriteObjectto hand over, of a kind inkinds, preferringhttp-put-parts;complete(uploaded): makes the object, or raisesUploadFailedErrorand aborts the write. It refuses a reported size overmax_bytes, then checks the stored bytes are the reported ones, so nothing over the limit becomes an object;abort(): idempotent. Leaving itswithblock without completing aborts. A staged upload (S3 multipart) is discarded. A one-PUT write's object lands as the PUT finishes and stays, because object stores do not delete. So the caller usesobject_urlonly aftercomplete, and begins each write at a fresh URL.The default wraps
issue_write_grantin one URL:http-put-partsgrant for a receiver that takes parts, else anhttp-put;max_single_upload_bytes;completechecks the object landed at the reported size.Every store that issues write grants therefore supports
begin_writewith no change, including out-of-tree ones.S3ObjectStoreparts:MAX_PARTS.part_bytesbut the last, totalling the reported size. Otherwise it aborts.ClientError, or a botocore connection or timeout error, starting the upload becomesGrantUnavailableError, which callers already read as "offer today's form";UploadFailedError, aValueError, so a caller that fails one item on bad input fails only that write;UploadFailedErrortoo.Wrappers:
LocalRunObjectStore.begin_writegoes through the same@localwrite fence asissue_write_grant.StagedObjectStorerefusesbegin_writeinstead of letting__getattr__hand the agent a grant on the store it wraps. A staged form comes later, with the agent issuers.Regional signing (affects every S3 grant on AWS). When its client sends to one of AWS's own S3 hosts with no addressing style configured,
S3ObjectStore.from_configsetsaddressing_style="virtual"(path-style for dotted bucket names), so presigned URLs name the bucket's region. botocore's default signs against the global endpoint, and a bucket less than a day old answers that with307 TemporaryRedirect; grant transfers deliberately don't follow redirects. Found running parts uploads against a fresh bucket.AWS_ENDPOINT_URL_S3, a PrivateLink endpoint), and an AWS profile'ss3.addressing_style, are left as boto3 resolves them.<bucket>.s3.amazonaws.comto<bucket>.s3.<region>.amazonaws.com. A sandbox egress allowlist written for the global host must name the regional one.The S3 write cuts and checks its parts with the protocol's
part_ranges, the rule the uploader sends by.Accepted limit. A one-PUT write's grant stays good until it expires, so the receiver, which chose the bytes anyway, could replace an accepted object until then. The S3 parts path cannot: completing it ends the upload, and a re-PUT of a part URL gets
NoSuchUpload(checked on a real bucket).issue_write_grantis unchanged, and nothing callsbegin_writeyet;snapshot_envadopts it next.Tests
pending_write_test.py, the default:aws/s3_parts_write_test.py, on moto:UploadFailedErrorraised;GRANT_CASESgains a completed object write, a misreported one refused, and an abandoned one leaving nothing. They run against the local grant server here; the S3 parts path was also checked by hand on a real bucket.begin_writeand fences it in an@localrun, and the staged store refuses it. The@localwrite-fence test listsbegin_writeamong the writes a run must not make to the configured store.Mutants, all killed:
ClientError.The unit tier passes.
🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge under its documented single-PUT limits.Summary
Adds
PendingWrite, a default single-PUT implementation, and S3 multipart uploads with checks before completion. The wrappers route or refuse the new operation.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[begin_write] --> B[Return upload grant] B --> C[Receiver uploads bytes] C --> D[complete] D --> E{Reported size within limit?} E -->|No| F[Abort write] E -->|Yes| G[Check stored size or parts] G -->|Mismatch or error| F G -->|Match| H[Accept object] H --> I[S3 completes multipart upload]Reviews (8) · Last reviewed commit: "fix(store): keep a configured endpoint's..." · Reviewed by Greptile