Repository navigation
Add BlobBatchClient for deleting blobs in one request - #82
Draft
kartik-joshi-01 wants to merge 2 commits into
Draft
kartik-joshi-01 wants to merge 2 commits into
kartik-joshi-01 wants to merge 2 commits into
Conversation
Blob Batch sub-requests are serialized into the batch body rather than sent through the HTTP client, so the shared key and Entra ID middleware now also sign a single request through RequestSigner. The client factory accepts that signer, so a client and the requests signed outside it share one token cache.
kartik-joshi-01
marked this pull request as draft
September 24, 2026 04:25
Member
|
Thanks. Will take some time to review this |
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.
Closes #81.
Batch delete, shaped after the .NET library. There's
deleteBlobs()for the everyday case andcreateBatch()/submitBatch()when blobs need their own options or conditions.Names are relative to the client: a blob name on a container client,
container/blobon a service client.BlobClientinstances work too, and theirsnapshot/versionidselectors are kept.What's in here
BlobBatchClientandBlobBatchunderSpecialized, withBlobBatchClientOptions(same idea asBlobLeaseClientOptions),BlobBatchExceptionandBlobBatchFailure.deleteBlobs()iscreateBatch()+deleteBlob()per name +submitBatch(), so there's one code path.getBlobBatchClient()onBlobContainerClient(posts to{container}?restype=container&comp=batch) and onBlobServiceClient(posts to/?comp=batch, sub-request paths are/{container}/{blob}). The scope is passed explicitly, not parsed from the URI.RequestSignerinterface. Each sub-request inside a batch needs its ownAuthorizationheader, and the signing logic lives in the two auth middleware, so they now implementsign()and__invokecalls it. The batch client hands the same middleware instance to the outer client and to the sub-requests, so one batch fetches an Entra token once. The signing code itself didn't change.How it behaves
Same rules as .NET's
BlobBatch: a batch belongs to the client scope that created it (same URI and credential, so a freshgetBlobBatchClient()is fine), can't be empty, can't be submitted twice, and can't be added to after submitting. Like .NET it's marked submitted before the request goes out, so a batch whose request failed can't be resubmitted either. Empty names are rejected, and on a service client a name has to becontainer/blob.A batch isn't atomic and the service gives no ordering guarantee. Each sub-request succeeds or fails on its own, and every failure comes back in one
BlobBatchException: aBlobBatchFailureper blob with its index, the input as you passed it, and aBlobStorageExceptionbuilt from the sub-response by the existing deserializer.BlobBatchExceptionextendsBlobStorageException(Java does the same), so existingcatchblocks still see partial failures. A 404 is a failure, same asBlobClient::delete()and the .NET README's troubleshooting example.If the service refuses the whole batch you get the usual
BlobStorageExceptionfrom the middleware. That includes the undocumented shape where the refusal comes back as a 202 with a single 400 part and no Content-ID; .NET special-cases it too, and both Azurite versions produce it when a sub-request path is malformed.Auth: shared key and Entra ID sign every sub-request; a SAS on the client URI is appended to every sub-request path. Per the spec the SAS needs
wfor the batch request itself anddfor each blob; the docs page says so. Sub-requests carryx-ms-datebut nox-ms-version, the outer request's applies. ABlobClientfrom a different account, endpoint or container is rejected when you add it.Snapshot and version selectors go out unencoded, as .NET sends them; Azurite (3.35 and 3.37) 404s the percent-encoded form. The response boundary comes from the response
Content-Type, because Azure usesbatchresponse_<id>while Azurite 3.35 echoes the request boundary.Two things I didn't copy from .NET:
submitBatch()returns nothing and always throws on a failure, because .NET'sthrowOnAnyFailure: falseonly makes sense with its lazy per-operationResponseobjects and we don't have those (Python raises by default as well). And the 256 limit is checked client-side; .NET has a TODO for it and leaves it to the service. The 4 MB body limit is left to the service.One caveat in the docblocks: the retry middleware may replay a batch whose response was lost, and the replay then reports
BlobNotFoundfor blobs the first attempt already deleted.Tests
Integration on Azurite: container and account scope, awkward names (spaces, unicode,
+, emoji), a missing blob and a leased blob failing while the rest get deleted, snapshot options for the whole list and per blob, deleting a selected snapshot, container SAS and account SAS withw+d. Functional tests onguzzlehttp/test-serverpin the wire format: the exact shared-key signature on each sub-request (known-answer, computed afterx-ms-dateis set), Bearer and SAS per sub-request, one token fetch per batch, per-blob options, parsing abatchresponse_body with 202/404/412, the single-400-part refusal, and a batch rejected as a whole. Unit tests for the batch rules, name validation, scope, the multipart parser, options inheritance and a known-answerRequestSignertest.The
coresuite astests.ymlruns it, on Azurite 3.37 with Guzzle 8: 489 passed, 28 skipped. The batch integration tests also pass on Azurite 3.35. PHPStan level 10 (no suppressions) and Pint are clean, and the storage-common commit passes on its own.Things I'd like your call on
azure-oss/storage-common: ^2.3forRequestSigner, since Common is at 2.2.1 with an unreleased Added entry. Change it if you version it differently.CredentialAuthorizationTest, and its role isn't assigned ininfra/, so I can't tell whether it may delete. Bearer signing of sub-requests is covered functionally. If the identity has a data-plane write role, say so and I'll add one.Functional/BlobClientTest.phprather than their own class:guzzlehttp/test-serveruses one fixed port, and underpest --parallela second test-server class stops the first one's server mid-run. Splitting them needs a per-worker port or a lock.AccessTierenum andBlobClient::setAccessTier()first, thenBlobBatch::setBlobAccessTier(),BlobBatchClient::setBlobsAccessTier()and the one-operation-type-per-batch rule from .NET. Happy to do that one next if you want it.