Repository navigation
persist: port Azure blob backend to the official Azure SDK - #39293
Conversation
7362d5d to
2995050
Compare
2995050 to
0c720be
Compare
Replace the unmaintained azure_storage and azure_storage_blobs 0.21 crates with azure_storage_blob 1.1, azure_core 1.1, and azure_identity 1.0. The new azure_core implements its HTTP transport for reqwest 0.13, so persist depends on reqwest 0.13 through a separate workspace alias while the rest of the tree stays on 0.12. The new azure_identity has no default credential chain, so persist builds the chain the old DefaultAzureCredential used: environment credentials, managed identity with a 1s timeout, and the Azure CLI. The new SDK also drops shared key authorization, so the Azurite path signs an account SAS with the emulator's well-known key. Azurite runs with --skipApiVersionCheck because the SDK sends a newer service version than the pinned Azurite release accepts. Blob names keep the old SDK's leading and trailing slash trimming, so existing data stays readable. Uploads stay single Put Blob requests. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Azure SDK sends service API version 2026-04-06, which Azurite 3.37.0 accepts, so the emulator no longer needs --skipApiVersionCheck. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
0c720be to
3f484b1
Compare
QA LLM Review1. MEDIUM -- Dropped Azure gets keep their ranged downloads running in detached SDK tasks
For blobs larger than one 1 MiB partition, Details
So every hedge race on a part over 1 MiB leaves the losing leg with up to Possible fixes:
|
The Azure SDK's partitioned download reads every range after the first in spawned tasks that dropping the download does not abort. HedgedBlob cancels the losing leg of a race by dropping its get, so those requests kept running outside the hedge and fetch budgets. Each range is now its own single-partition download, which the SDK reads without spawning, and persist drives up to eight of them at once, pinned to the first range's etag. Co-Authored-By: Claude Opus 5.5 <[email protected]>
mtabebe
left a comment
There was a problem hiding this comment.
code looks good.
One minor question, and another more detailed one that is really work for the persist team to consider in testing infrastructure.
| const EMULATOR_ACCOUNT: &str = "devstoreaccount1"; | ||
|
|
||
| /// The publicly documented account key of the Azurite emulator's | ||
| /// [EMULATOR_ACCOUNT]. | ||
| const EMULATOR_ACCOUNT_KEY: &str = | ||
| "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; |
There was a problem hiding this comment.
Just wanted to say that this really confused me why these were in the code, but these are not secrets.
I do wonder if this will trip up any automation ...
There was a problem hiding this comment.
Not a secret: it's Azurite's built-in, publicly documented key for devstoreaccount1. The repo's trufflehog lint (check-trufflehog.sh, which scans all tracked files) passes with it in place. Added a NOTE: to the doc comment saying so.
(Posted by Claude Code.)
There was a problem hiding this comment.
Cool, I realized it was the publicly documented key. It just took me a minute to figure it out
| let etag = first.properties.etag.clone(); | ||
| let partition_size = u64::cast_from(GET_PARTITION_SIZE.get()); | ||
| let offsets = | ||
| (partition_size..content_length.unwrap_or(0)).step_by(GET_PARTITION_SIZE.get()); |
There was a problem hiding this comment.
What happens if we end up in the or(0) case from download_total_len? What are the offsets that we will read?
It feels like we will not download any ranges.
I am curious if we want to be explicit about the none case of how we handle download_total_len?
There was a problem hiding this comment.
Good catch, fixed in f44c7f9. A missing length now fails the get instead of silently returning only the first partition. Looking at it also showed that a response ignoring the range would have been read twice, so the remaining ranges now start after the bytes actually received. Both cases have mock-transport tests that fail against the previous code.
(Posted by Claude Code.)
| /// dropping a get must drop every request it has in flight. | ||
| #[mz_ore::test(tokio::test)] | ||
| #[cfg_attr(miri, ignore)] // too slow | ||
| async fn azure_blob_get_drop_cancels_requests() { |
There was a problem hiding this comment.
Thank you for the new tests
| /// Returns a client for the blob at `path`. | ||
| fn blob_client(&self, path: &str) -> azure_storage_blob::BlobClient { | ||
| // Blobs are stored under their path without leading and trailing | ||
| // slashes, which `azure_storage_blobs` 0.21 stripped when building |
There was a problem hiding this comment.
Do you know if we have upgrade tests that would catch if we broke this?
My concern here is:
- old version writes on the old SDK
- new version reads on the new SDK
Then, there is a mismatch in prefix/label and now we can't read the data that was previously written.
FWIW: I had my agent try and write a reproduction and it passed so I am satisfied with the code here.
I don't think anything needs to change in this PR, so this is more we working through my thought process.
I think what we would want is an upgrade test with platform checks where we had different flags on the old/new version?
There was a problem hiding this comment.
No CI job covers that today. The Azurite platform checks in release qualification only run the restart scenario, and the Nightly Azurite jobs don't upgrade. The platform-checks composition supports --features=azurite, so an upgrade scenario on Azurite would be the natural follow-up. Blob names keep the old SDK's slash trimming, so objects written by the old SDK map to the same URLs.
(Posted by Claude Code.)
There was a problem hiding this comment.
I had some code on #36216 to cover the change / missing Azurite tests if and when anybody needs it
There was a problem hiding this comment.
Thanks. #39306 added Nightly upgrade checks on Azurite (UpgradeEntireMz and UpgradeEntireMzFromLatestSelfManaged with --features=azurite). They start on the old SDK and upgrade to the new one, which covers the old-write/new-read case.
(Posted by Claude Code.)
A first range response without a length left the get unable to tell which ranges remain, and it returned only the first partition. Fail such gets instead. The remaining ranges now start after the bytes the first response delivered, so a response that ignores the requested range is not read twice. Also spell out that the emulator account key is not a secret. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Thanks for the review! |
Azurite platform checks only run the restart scenario in release qualification, so no CI job catches an upgrade that can no longer read blobs an earlier release wrote to Azure blob storage. This adds two Nightly steps that run the whole-Mz upgrade scenarios with `--features=azurite`: `UpgradeEntireMz` from the last release, and `UpgradeEntireMzFromLatestSelfManaged`, which is the upgrade path Azure deployments take. `StartMz` already passes the Azurite blob store to every version it starts, so the scenarios need no changes. Follow-up to the review of #39293, which moves persist's Azure backend to the new Azure SDK. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
|
Thank you @antiguru! |
This replaces the abandoned
azure_storage/azure_storage_blobs0.21 crates withazure_storage_blob1.1,azure_core1.1, andazure_identity1.0. It removeshttp-typesand the advisory ignores for RUSTSEC-2026-0174 and RUSTSEC-2026-0275 (SEC-619), which were only needed for the old crates. This unblocks the Azure half of #39023.The new
azure_coreimplements its transport for reqwest 0.13 only. Persist uses it through areqwest-0-13workspace alias because the iceberg fork's public API still takes reqwest 0.12 types, which blocks a workspace-wide bump.The following behavior carries over from the old SDK:
ClientAssertionCredential.azure_identityremovedDefaultAzureCredential, so persist rebuilds its order: environment (static federated token, client secret), managed identity with a 1s timeout, then Azure CLI.The new SDK has no shared key auth. The Azurite path signs an account SAS with the emulator's well-known key instead. This upgrades Azurite to 3.37.0 because the SDK sends API version 2026-04-06, which 3.33.0 rejects.
This adds
azure_blob_multi_range_get, which covers gets that span several partitions, listing sizes, and delete.🤖 Generated with Claude Code