Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/azure_blob_store.rs
Line
Count
Source
1
// Copyright 2024 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::pin::Pin;
16
use core::time::Duration;
17
use core::{cmp, mem};
18
use std::borrow::Cow;
19
use std::sync::Arc;
20
use std::time::Instant;
21
22
use async_trait::async_trait;
23
use azure_core::credentials::TokenCredential;
24
use azure_core::error::ErrorKind;
25
use azure_core::http::headers::{RETRY_AFTER, RETRY_AFTER_MS, X_MS_RETRY_AFTER_MS};
26
use azure_core::http::{RequestContent, RetryOptions, StatusCode, Transport, Url};
27
use azure_identity::WorkloadIdentityCredential;
28
use azure_storage_blob::clients::{BlobContainerClient, BlobContainerClientOptions};
29
use azure_storage_blob::models::{
30
    BlobClientDownloadOptions, BlobClientGetPropertiesResultHeaders, BlockListType,
31
    BlockLookupList, HttpRange, StorageErrorCode,
32
};
33
use futures::future::FusedFuture;
34
use futures::stream::{FuturesUnordered, unfold};
35
use futures::{FutureExt, StreamExt, TryStreamExt};
36
use nativelink_config::stores::ExperimentalAzureSpec;
37
use nativelink_error::{Code, Error, ResultExt, make_err};
38
use nativelink_metric::MetricsComponent;
39
use nativelink_util::adaptive_permits::{AdaptivePermit, AdaptivePermits};
40
use nativelink_util::buf_channel::{DropCloserReadHalf, DropCloserWriteHalf};
41
use nativelink_util::health_utils::{HealthRegistryBuilder, HealthStatus, HealthStatusIndicator};
42
use nativelink_util::instant_wrapper::InstantWrapper;
43
use nativelink_util::metrics::{record_object_store_request_wait, record_object_store_throttle};
44
use nativelink_util::retry::{Retrier, RetryResult};
45
use nativelink_util::store_trait::{
46
    RemoveCallback, StoreDriver, StoreKey, StoreOptimizations, UploadSizeInfo,
47
};
48
use nativelink_util::{background_spawn, fs};
49
use parking_lot::Mutex;
50
use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc};
51
use tokio::time::sleep;
52
use tracing::{Level, event};
53
54
use crate::cas_utils::is_zero_digest;
55
use crate::common_s3_utils::install_default_rustls_crypto_provider;
56
57
// Check the below doc for the limits specific to Azure.
58
// https://learn.microsoft.com/en-us/azure/storage/blobs/scalability-targets#scale-targets-for-blob-storage
59
60
// Maximum number of blocks in a block blob or append blob
61
const MAX_BLOCKS: usize = 50_000;
62
63
// Maximum size of a block in a block blob (4,000 MiB)
64
const MAX_BLOCK_SIZE: u64 = 4_000 * 1024 * 1024; // 4,000 MiB = 4 GiB
65
66
// Default block size for uploads (5 MiB)
67
const DEFAULT_BLOCK_SIZE: u64 = 5 * 1024 * 1024; // 5 MiB
68
69
// Default maximum retry buffer per request
70
const DEFAULT_MAX_RETRY_BUFFER_PER_REQUEST: usize = 5 * 1024 * 1024; // 5 MiB
71
72
// Default maximum number of concurrent uploads
73
const DEFAULT_MAX_CONCURRENT_UPLOADS: usize = 10;
74
75
// Default public Azure Blob Storage endpoint suffix.
76
const DEFAULT_BLOB_ENDPOINT_SUFFIX: &str = "blob.core.windows.net";
77
78
/// Successful reads in a row before the adaptive read limit grows by one.
79
const READ_LIMIT_GROW_AFTER: usize = 16;
80
81
/// Shortest time between two warnings from one store that the host ran out
82
/// of local ports.
83
const PORT_HINT_INTERVAL: Duration = Duration::from_mins(1);
84
85
#[derive(MetricsComponent)]
86
pub struct AzureBlobStore<NowFn> {
87
    client: Arc<BlobContainerClient>,
88
    now_fn: NowFn,
89
    #[metric(help = "The container name for the Azure store")]
90
    container: String,
91
    #[metric(help = "The blob prefix for the Azure store")]
92
    blob_prefix: String,
93
    retrier: Retrier,
94
    #[metric(help = "The number of seconds to consider an object expired")]
95
    consider_expired_after_s: i64,
96
    #[metric(help = "The number of bytes to buffer for retrying requests")]
97
    max_retry_buffer_per_request: usize,
98
    #[metric(help = "The number of concurrent uploads allowed")]
99
    max_concurrent_uploads: usize,
100
    throttle_retry_delay: Duration,
101
    throttle_retry_max_delay: Duration,
102
    /// Bounds `GET` and `HEAD` requests in flight; `None` leaves them
103
    /// unbounded. See `CommonObjectSpec::max_concurrent_reads`.
104
    read_permits: Option<Arc<AdaptivePermits>>,
105
    /// Bounds every request in flight, of any kind; `None` leaves them
106
    /// unbounded. See `CommonObjectSpec::max_concurrent_requests`.
107
    request_permits: Option<Arc<Semaphore>>,
108
    #[metric(help = "The most requests this store keeps in flight at once, 0 for unbounded")]
109
    max_concurrent_requests: usize,
110
    /// When this store last warned that the host ran out of local ports.
111
    last_port_hint: Mutex<Option<Instant>>,
112
}
113
114
impl<NowFn> core::fmt::Debug for AzureBlobStore<NowFn> {
115
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116
0
        f.debug_struct("AzureBlobStore")
117
0
            .field("container", &self.container)
118
0
            .field("blob_prefix", &self.blob_prefix)
119
0
            .field("consider_expired_after_s", &self.consider_expired_after_s)
120
0
            .finish_non_exhaustive()
121
0
    }
122
}
123
124
/// A request that cannot connect, or reads nothing for this long (response
125
/// headers included), fails and is retried. Without a bound, a request that
126
/// hangs keeps its permit forever, and `max_concurrent_requests` of them
127
/// wedge the store. The read timer runs only while the body is polled, so a
128
/// download that a slow reader holds back is not cut off. The values are the
129
/// Azure SDK's own defaults, which this store's client replaces.
130
const AZURE_CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
131
const AZURE_READ_TIMEOUT: Duration = Duration::from_mins(1);
132
133
/// How long a cancelled upload's cleanup waits before it looks at the block
134
/// list, so a block whose request was already in flight has landed and is
135
/// seen rather than raced.
136
const STAGED_BLOCK_DISCARD_DELAY: Duration = Duration::from_secs(5);
137
138
/// Discards the blocks an upload staged if the upload never commits, from a
139
/// background task in `Drop`: after an error, so the caller gets it without
140
/// waiting for request permits, and when the upload future is dropped (the
141
/// `ByteStream` it fed died and the server swept it), since a dropped future
142
/// runs no code of its own. Only a committed upload disarms it.
143
struct StagedBlocksGuard {
144
    client: Arc<BlobContainerClient>,
145
    requests: Option<Arc<Semaphore>>,
146
    blob_path: String,
147
    armed: bool,
148
}
149
150
impl StagedBlocksGuard {
151
3
    const fn disarm(&mut self) {
152
3
        self.armed = false;
153
3
    }
154
}
155
156
impl Drop for StagedBlocksGuard {
157
12
    fn drop(&mut self) {
158
12
        if !self.armed {
159
3
            return;
160
9
        }
161
9
        let client = Arc::clone(&self.client);
162
9
        let requests = self.requests.take();
163
9
        let blob_path = mem::take(&mut self.blob_path);
164
9
        background_spawn!("azure_discard_staged_blocks", async move 
{8
165
8
            sleep(STAGED_BLOCK_DISCARD_DELAY).await;
166
7
            discard_staged_blocks(&client, requests.as_ref(), &blob_path).await;
167
7
        });
168
12
    }
169
}
170
171
/// Best effort: drop the blocks an upload left staged under this name.
172
/// Azure keeps uncommitted blocks, and bills them, for a week before
173
/// collecting them, which adds up under an upload failure storm. A
174
/// committed blob under the same name (a re-upload of a key that already
175
/// exists) must survive: re-committing its own block list is what discards
176
/// the staged blocks next to it. With nothing committed the name holds only
177
/// staged blocks and deleting it removes them. Failure here is logged, never
178
/// returned: the upload has already failed, and the blocks are collected
179
/// eventually either way. Each request takes a permit from `requests`
180
/// like any other the store sends.
181
7
async fn discard_staged_blocks(
182
7
    client: &BlobContainerClient,
183
7
    requests: Option<&Arc<Semaphore>>,
184
7
    blob_path: &str,
185
7
) {
186
7
    let blob_client = client.blob_client(blob_path);
187
7
    let block_list_result = {
188
7
        let _request_permit = request_permit(requests).await;
189
7
        blob_client
190
7
            .block_blob_client()
191
7
            .get_block_list(BlockListType::All, None)
192
7
            .await
193
    };
194
7
    let block_list = match block_list_result {
195
7
        Ok(response) => match response.into_model() {
196
7
            Ok(block_list) => block_list,
197
0
            Err(err) => {
198
0
                event!(
199
0
                    Level::WARN,
200
                    ?err,
201
                    blob_path,
202
                    "Could not read the block list after a failed Azure upload"
203
                );
204
0
                return;
205
            }
206
        },
207
0
        Err(err) => {
208
0
            if err.http_status() != Some(StatusCode::NotFound) {
209
0
                event!(
210
0
                    Level::WARN,
211
                    ?err,
212
                    blob_path,
213
                    "Could not read the block list after a failed Azure upload"
214
                );
215
0
            }
216
0
            return;
217
        }
218
    };
219
7
    let uncommitted = block_list.uncommitted_blocks.unwrap_or_default();
220
7
    if uncommitted.is_empty() {
221
0
        return;
222
7
    }
223
7
    let committed: Vec<Vec<u8>> = block_list
224
7
        .committed_blocks
225
7
        .unwrap_or_default()
226
7
        .into_iter()
227
7
        .filter_map(|block| block.name)
228
7
        .collect();
229
7
    let _request_permit = request_permit(requests).await;
230
7
    let result = if committed.is_empty() {
231
4
        blob_client.delete(None).await.map(|_| ())
232
    } else {
233
3
        let keep = BlockLookupList {
234
3
            committed: Some(committed),
235
3
            ..Default::default()
236
3
        };
237
3
        match RequestContent::try_from(keep) {
238
3
            Ok(blocks) => blob_client
239
3
                .block_blob_client()
240
3
                .commit_block_list(blocks, None)
241
3
                .await
242
3
                .map(|_| ()),
243
0
            Err(err) => Err(err),
244
        }
245
    };
246
7
    match result {
247
7
        Ok(()) => event!(
248
7
            Level::INFO,
249
            blob_path,
250
7
            staged_blocks = uncommitted.len(),
251
            "Discarded the blocks a failed Azure upload left staged"
252
        ),
253
0
        Err(err) => event!(
254
0
            Level::WARN,
255
            ?err,
256
            blob_path,
257
0
            staged_blocks = uncommitted.len(),
258
            "Could not discard the blocks a failed Azure upload left staged"
259
        ),
260
    }
261
7
}
262
263
/// A permit from the store's request pool when requests are bounded,
264
/// counted on `store.object.request_waits` when none was free. Every path
265
/// takes it after the read permit and before the process-wide open-file
266
/// permit, so no two requests wait on each other's pools in opposite order.
267
291
async fn request_permit(requests: Option<&Arc<Semaphore>>) -> Option<OwnedSemaphorePermit> {
268
291
    let 
requests97
= requests
?194
;
269
97
    if requests.available_permits() == 0 {
270
77
        record_object_store_request_wait("azure");
271
77
    
}20
272
    Some(
273
97
        Arc::clone(requests)
274
97
            .acquire_owned()
275
97
            .await
276
97
            .expect("The request permit pool is never closed"),
277
    )
278
291
}
279
280
/// Whether `err` failed because this host had no local port left to
281
/// connect from: `connect()` answers `EADDRNOTAVAIL` when every source port
282
/// toward the destination is held, by a request in flight, an idle
283
/// connection kept for reuse, or a closed one still in `TIME_WAIT`.
284
32
pub fn is_out_of_local_ports(err: &(dyn core::error::Error + 'static)) -> bool {
285
32
    let mut next = Some(err);
286
68
    while let Some(
err43
) = next {
287
43
        if err
288
43
            .downcast_ref::<std::io::Error>()
289
43
            .is_some_and(|err| 
err.kind()8
==
std::io::ErrorKind::AddrNotAvailable8
)
290
        {
291
7
            return true;
292
36
        }
293
36
        next = err.source();
294
    }
295
25
    false
296
32
}
297
298
impl<I, NowFn> AzureBlobStore<NowFn>
299
where
300
    I: InstantWrapper,
301
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
302
{
303
0
    pub fn new(spec: &ExperimentalAzureSpec, now_fn: NowFn) -> Result<Arc<Self>, Error> {
304
0
        let jitter_fn = spec.common.retry.make_jitter_fn();
305
0
        let client = Self::build_container_client(spec)?;
306
0
        Self::new_with_client_and_jitter(spec, client, jitter_fn, now_fn)
307
0
    }
308
309
    /// Builds the container URL and selects the auth strategy:
310
    ///   * `sas_url` set    -> use it verbatim as the container URL with no credential.
311
    ///   * otherwise        -> `https://{account}.{endpoint}/{container}` authenticated with
312
    ///     Entra ID via Workload Identity (keyless).
313
0
    fn build_container_client(spec: &ExperimentalAzureSpec) -> Result<BlobContainerClient, Error> {
314
0
        let mut options = BlobContainerClientOptions::default();
315
0
        options.client_options.retry = RetryOptions::none();
316
        // Hand the SDK an HTTP client with an explicit rustls (ring) config.
317
0
        options.client_options.transport = Some(Self::build_http_transport()?);
318
319
0
        let (container_url, credential): (Url, Option<Arc<dyn TokenCredential>>) =
320
0
            if let Some(sas_url) = spec.sas_url.as_ref() {
321
0
                let url = Url::parse(sas_url)
322
0
                    .map_err(|e| make_err!(Code::InvalidArgument, "Invalid Azure sas_url: {e}"))?;
323
0
                (url, None)
324
            } else {
325
0
                let endpoint = spec.endpoint.clone().unwrap_or_else(|| {
326
0
                    format!(
327
                        "https://{}.{DEFAULT_BLOB_ENDPOINT_SUFFIX}",
328
                        spec.account_name
329
                    )
330
0
                });
331
0
                let mut url = Url::parse(&endpoint)
332
0
                    .map_err(|e| make_err!(Code::InvalidArgument, "Invalid Azure endpoint: {e}"))?;
333
0
                url.path_segments_mut()
334
0
                    .map_err(|()| {
335
0
                        make_err!(
336
0
                            Code::InvalidArgument,
337
                            "Azure endpoint is not a valid base URL: {endpoint}"
338
                        )
339
0
                    })?
340
0
                    .pop_if_empty()
341
0
                    .push(&spec.container);
342
0
                let credential: Arc<dyn TokenCredential> = WorkloadIdentityCredential::new(None)
343
0
                    .map_err(|e| {
344
0
                        make_err!(
345
0
                            Code::FailedPrecondition,
346
                            "Failed to create Azure Workload Identity credential: {e}"
347
                        )
348
0
                    })?;
349
0
                (url, Some(credential))
350
            };
351
352
0
        BlobContainerClient::new(container_url, credential, Some(options))
353
0
            .map_err(|e| make_err!(Code::Unavailable, "Failed to create Azure client: {e}"))
354
0
    }
355
356
    /// Builds an HTTP transport for the Azure SDK backed by a reqwest client with
357
    /// an explicit rustls config using `NativeLink`'s ring crypto provider, so the
358
    /// SDK never falls back to guessing a provider (which breaks HTTPS here).
359
0
    fn build_http_transport() -> Result<Transport, Error> {
360
0
        install_default_rustls_crypto_provider();
361
362
0
        let mut roots = rustls::RootCertStore::empty();
363
0
        roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
364
0
        let tls_config = rustls::ClientConfig::builder()
365
0
            .with_root_certificates(roots)
366
0
            .with_no_client_auth();
367
368
0
        let client = reqwest::Client::builder()
369
0
            .use_preconfigured_tls(tls_config)
370
0
            .connect_timeout(AZURE_CONNECT_TIMEOUT)
371
0
            .read_timeout(AZURE_READ_TIMEOUT)
372
0
            .build()
373
0
            .map_err(|e| make_err!(Code::Unavailable, "Failed to build Azure HTTP client: {e}"))?;
374
375
0
        Ok(Transport::new(Arc::new(client)))
376
0
    }
377
378
63
    pub fn new_with_client_and_jitter(
379
63
        spec: &ExperimentalAzureSpec,
380
63
        client: BlobContainerClient,
381
63
        jitter_fn: Arc<dyn Fn(Duration) -> Duration + Send + Sync>,
382
63
        now_fn: NowFn,
383
63
    ) -> Result<Arc<Self>, Error> {
384
63
        let throttle_retry_delay = Duration::try_from_secs_f32(spec.common.throttle_retry_delay_s)
385
63
            .map_err(|e| 
make_err!0
(
Code::InvalidArgument0
, "Invalid throttle_retry_delay_s: {e}"))
?0
;
386
63
        let throttle_retry_max_delay =
387
63
            Duration::try_from_secs_f32(spec.common.throttle_retry_max_delay_s).map_err(|e| 
{0
388
0
                make_err!(
389
0
                    Code::InvalidArgument,
390
                    "Invalid throttle_retry_max_delay_s: {e}"
391
                )
392
0
            })?;
393
63
        if throttle_retry_max_delay < throttle_retry_delay {
394
0
            return Err(make_err!(
395
0
                Code::InvalidArgument,
396
0
                "throttle_retry_max_delay_s ({}) is less than throttle_retry_delay_s ({})",
397
0
                spec.common.throttle_retry_max_delay_s,
398
0
                spec.common.throttle_retry_delay_s
399
0
            ));
400
63
        }
401
63
        let read_permits = (spec.common.max_concurrent_reads > 0).then(|| 
{11
402
11
            let max = spec.common.max_concurrent_reads;
403
11
            let (min, grow_after) = if spec.common.adaptive_read_concurrency {
404
2
                (
405
2
                    spec.common.min_concurrent_reads.min(max),
406
2
                    READ_LIMIT_GROW_AFTER,
407
2
                )
408
            } else {
409
9
                (max, 0)
410
            };
411
11
            AdaptivePermits::new("azure", max, min, max, grow_after)
412
11
        });
413
63
        let max_concurrent_requests = spec.common.max_concurrent_requests;
414
63
        let max_concurrent_reads = spec.common.max_concurrent_reads;
415
63
        if max_concurrent_requests > 0
416
8
            && (max_concurrent_reads == 0 || 
max_concurrent_reads >= max_concurrent_requests7
)
417
        {
418
3
            return Err(make_err!(
419
3
                Code::InvalidArgument,
420
3
                "max_concurrent_requests ({max_concurrent_requests}) needs max_concurrent_reads between 1 and {}: a download holds its request permit while its body streams, so without that gap downloads can take every permit and stall existence checks and uploads (max_concurrent_reads is {max_concurrent_reads})",
421
3
                max_concurrent_requests - 1
422
3
            ));
423
60
        }
424
60
        let request_permits = (max_concurrent_requests > 0)
425
60
            .then(|| 
Arc::new5
(
Semaphore::new5
(
max_concurrent_requests5
)));
426
60
        let throttle_retry_budget =
427
60
            Duration::try_from_secs_f32(spec.common.throttle_retry_budget_s).map_err(|e| 
{0
428
0
                make_err!(
429
0
                    Code::InvalidArgument,
430
                    "Invalid throttle_retry_budget_s: {e}"
431
                )
432
0
            })?;
433
60
        Ok(Arc::new(Self {
434
60
            client: Arc::new(client),
435
60
            now_fn,
436
60
            container: spec.container.clone(),
437
60
            blob_prefix: spec
438
60
                .common
439
60
                .key_prefix
440
60
                .as_ref()
441
60
                .unwrap_or(&String::new())
442
60
                .clone(),
443
60
            retrier: Retrier::new(
444
60
                Arc::new(|duration| 
Box::pin21
(
sleep21
(
duration21
))),
445
60
                jitter_fn,
446
60
                spec.common.retry.clone(),
447
            )
448
60
            .with_client("azure")
449
60
            .with_throttle_budget(throttle_retry_budget)
450
60
            .with_throttle_max_wait(throttle_retry_max_delay),
451
60
            consider_expired_after_s: i64::from(spec.common.consider_expired_after_s),
452
60
            max_retry_buffer_per_request: spec
453
60
                .common
454
60
                .max_retry_buffer_per_request
455
60
                .unwrap_or(DEFAULT_MAX_RETRY_BUFFER_PER_REQUEST),
456
60
            max_concurrent_uploads: spec
457
60
                .common
458
60
                .multipart_max_concurrent_uploads
459
60
                .unwrap_or(DEFAULT_MAX_CONCURRENT_UPLOADS),
460
60
            throttle_retry_delay,
461
60
            throttle_retry_max_delay,
462
60
            read_permits,
463
60
            request_permits,
464
60
            max_concurrent_requests,
465
60
            last_port_hint: Mutex::new(None),
466
        }))
467
63
    }
468
469
    /// The read limit the permit pool stands at, or `None` with reads
470
    /// unbounded.
471
6
    pub fn read_limit(&self) -> Option<usize> {
472
6
        self.read_permits.as_ref().map(|pool| pool.limit())
473
6
    }
474
475
    /// A download permit when reads are bounded. Held for the request,
476
    /// body included, so what is in flight is what the limit says. Only
477
    /// `get_part` takes one: a `HEAD` moves no bytes and must not wait
478
    /// behind a body still being sent to a slow client, and a throttle
479
    /// on either side says nothing about the other.
480
57
    async fn read_permit(&self) -> Option<AdaptivePermit> {
481
57
        match &self.read_permits {
482
28
            Some(pool) => Some(pool.acquire().await),
483
29
            None => None,
484
        }
485
57
    }
486
487
    /// A request permit when requests are bounded; see [`request_permit`].
488
277
    async fn request_permit(&self) -> Option<OwnedSemaphorePermit> {
489
277
        request_permit(self.request_permits.as_ref()).await
490
277
    }
491
492
    /// Names the likely cause, and the setting that bounds it, when a
493
    /// request failed because the host ran out of local ports. At most once
494
    /// per `PORT_HINT_INTERVAL`: under exhaustion most requests fail alike.
495
29
    fn hint_if_out_of_local_ports(&self, err: &azure_core::Error) {
496
29
        if !is_out_of_local_ports(err) {
497
23
            return;
498
6
        }
499
        {
500
6
            let now = Instant::now();
501
6
            let mut last = self.last_port_hint.lock();
502
6
            if last.is_some_and(|last| 
now4
.duration_since(last) <
PORT_HINT_INTERVAL4
) {
503
4
                return;
504
2
            }
505
2
            *last = Some(now);
506
        }
507
2
        if self.max_concurrent_requests == 0 {
508
1
            event!(
509
1
                Level::WARN,
510
                container = %self.container,
511
                "Out of local ports toward the Azure endpoint (AddrNotAvailable), and this store's requests are unbounded. Set max_concurrent_requests on it to cap its connections; idle connections and ones in TIME_WAIT hold ports too"
512
            );
513
        } else {
514
1
            event!(
515
1
                Level::WARN,
516
                container = %self.container,
517
                max_concurrent_requests = self.max_concurrent_requests,
518
                "Out of local ports toward the Azure endpoint (AddrNotAvailable). Lower max_concurrent_requests, and count every store in a shard, each of which holds up to that many; idle connections and ones in TIME_WAIT hold ports too"
519
            );
520
        }
521
29
    }
522
523
    /// A read was answered (found or not): one toward growing the limit.
524
39
    fn note_read_ok(&self) {
525
39
        if let Some(
pool25
) = &self.read_permits {
526
25
            pool.record_success();
527
25
        
}14
528
39
    }
529
530
    /// A read was throttled: the limit is cut.
531
16
    fn note_read_throttled(&self) {
532
16
        if let Some(
pool3
) = &self.read_permits {
533
3
            pool.record_throttle();
534
13
        }
535
16
    }
536
537
259
    fn make_blob_path(&self, key: &StoreKey<'_>) -> String {
538
259
        format!("{}{}", self.blob_prefix, key.as_str())
539
259
    }
540
541
    /// How long a throttled response (503 `ServerBusy` or 429) asked us to
542
    /// wait, or `None` when the error is anything else. The wait is the
543
    /// `Retry-After` header when present, never less than the configured
544
    /// floor and never more than the configured cap. Every failed request
545
    /// passes through here, so this is also where running out of local
546
    /// ports is named.
547
29
    fn throttle_wait(&self, err: &azure_core::Error, op: &'static str) -> Option<Duration> {
548
29
        self.hint_if_out_of_local_ports(err);
549
        let ErrorKind::HttpResponse {
550
23
            status,
551
23
            error_code,
552
23
            raw_response,
553
29
        } = err.kind()
554
        else {
555
6
            return None;
556
        };
557
23
        let throttled = 
matches!6
(
558
23
            status,
559
            StatusCode::ServiceUnavailable | StatusCode::TooManyRequests
560
6
        ) || error_code.as_deref() == Some(StorageErrorCode::ServerBusy.as_ref());
561
23
        if !throttled {
562
6
            return None;
563
17
        }
564
17
        record_object_store_throttle("azure", op);
565
17
        let header_wait = raw_response.as_ref().and_then(|response| {
566
17
            let headers = response.headers();
567
17
            let millis = headers
568
17
                .get_optional_str(&RETRY_AFTER_MS)
569
17
                .or_else(|| headers.get_optional_str(&X_MS_RETRY_AFTER_MS))
570
17
                .and_then(|value| 
value.trim()0
.
parse0
::<u64>().
ok0
())
571
17
                .map(Duration::from_millis);
572
17
            millis.or_else(|| {
573
17
                headers
574
17
                    .get_optional_str(&RETRY_AFTER)
575
17
                    .and_then(|value| 
value.trim()2
.
parse2
::<u64>().
ok2
())
576
17
                    .map(Duration::from_secs)
577
17
            })
578
17
        });
579
17
        let asked = header_wait
580
17
            .unwrap_or(Duration::ZERO)
581
17
            .max(self.throttle_retry_delay);
582
17
        if asked > self.throttle_retry_max_delay {
583
1
            event!(
584
1
                Level::WARN,
585
                ?op,
586
1
                asked_s = asked.as_secs_f32(),
587
1
                cap_s = self.throttle_retry_max_delay.as_secs_f32(),
588
                "Throttled response asked for a longer wait than throttle_retry_max_delay_s; waiting the cap"
589
            );
590
1
            return Some(self.throttle_retry_max_delay);
591
16
        }
592
16
        Some(asked)
593
29
    }
594
595
10
    fn retry_later<T>(
596
10
        &self,
597
10
        sdk_err: &azure_core::Error,
598
10
        op: &'static str,
599
10
        err: Error,
600
10
    ) -> RetryResult<T> {
601
10
        match self.throttle_wait(sdk_err, op) {
602
1
            Some(wait) => RetryResult::RetryAfter(err, wait),
603
9
            None => RetryResult::Retry(err),
604
        }
605
10
    }
606
607
159
    async fn has(self: Pin<&Self>, digest: &StoreKey<'_>) -> Result<Option<u64>, Error> {
608
159
        let blob_path = self.make_blob_path(digest);
609
159
        let declared_size = match digest.borrow() {
610
159
            StoreKey::Digest(digest) => Some(digest.size_bytes()),
611
0
            StoreKey::Str(_) => None,
612
        };
613
614
159
        self.retrier
615
161
            .
retry159
(
unfold159
(
()159
, move |state| {
616
161
                let blob_path = blob_path.clone();
617
161
                let client = Arc::clone(&self.client);
618
161
                async move {
619
161
                    let _request_permit = self.request_permit().await;
620
161
                    let _permit = match fs::get_permit().await {
621
161
                        Ok(permit) => permit,
622
0
                        Err(e) => {
623
0
                            return Some((
624
0
                                RetryResult::Retry(make_err!(
625
0
                                    Code::Unavailable,
626
0
                                    "Failed to acquire permit: {e}"
627
0
                                )),
628
0
                                state,
629
0
                            ));
630
                        }
631
                    };
632
633
161
                    let result = client.blob_client(&blob_path).get_properties(None).await;
634
635
161
                    match result {
636
152
                        Ok(props) => {
637
152
                            if self.consider_expired_after_s > 0
638
2
                                && let Some(last_modified) = props.last_modified().ok().flatten()
639
                            {
640
2
                                let now = (self.now_fn)()
641
2
                                    .unix_timestamp()
642
2
                                    .try_into()
643
2
                                    .unwrap_or(i64::MAX);
644
2
                                if last_modified.unix_timestamp() + self.consider_expired_after_s
645
2
                                    <= now
646
                                {
647
1
                                    return Some((RetryResult::Ok(None), state));
648
1
                                }
649
150
                            }
650
151
                            let blob_size = props.content_length().ok().flatten().unwrap_or(0);
651
                            // An empty object cannot be a blob whose digest
652
                            // names bytes, compressed or not: a failed upload
653
                            // published it. Reported present it is read as
654
                            // DataLoss forever and never uploaded again, so
655
                            // say it is missing and let the client replace it.
656
151
                            if blob_size == 0 && 
declared_size1
.
is_some_and1
(|size|
size1
> 0) {
657
1
                                event!(
658
1
                                    Level::WARN,
659
                                    blob_path,
660
                                    "Azure holds an empty object for a digest that names bytes; reporting it missing"
661
                                );
662
1
                                return Some((RetryResult::Ok(None), state));
663
150
                            }
664
150
                            Some((RetryResult::Ok(Some(blob_size)), state))
665
                        }
666
9
                        Err(err) => {
667
9
                            if err.http_status() == Some(StatusCode::NotFound) {
668
                                // Distinguish a missing container (a config error) from a
669
                                // missing blob (a normal cache miss).
670
                                if let ErrorKind::HttpResponse {
671
0
                                    error_code: Some(error_code),
672
                                    ..
673
1
                                } = err.kind()
674
0
                                    && error_code == StorageErrorCode::ContainerNotFound.as_ref()
675
                                {
676
0
                                    return Some((
677
0
                                        RetryResult::Err(make_err!(
678
0
                                            Code::InvalidArgument,
679
0
                                            "Container not found: {err}"
680
0
                                        )),
681
0
                                        state,
682
0
                                    ));
683
1
                                }
684
1
                                Some((RetryResult::Ok(None), state))
685
                            } else {
686
8
                                Some((
687
8
                                    self.retry_later(
688
8
                                        &err,
689
8
                                        "has",
690
8
                                        make_err!(
691
8
                                            Code::Unavailable,
692
8
                                            "Failed to get blob properties: {err:?}"
693
8
                                        ),
694
8
                                    ),
695
8
                                    state,
696
8
                                ))
697
                            }
698
                        }
699
                    }
700
161
                }
701
161
            }))
702
159
            .await
703
159
    }
704
705
12
    async fn upload_staged_blocks(
706
12
        self: Pin<&Self>,
707
12
        blob_path: &str,
708
12
        mut reader: DropCloserReadHalf,
709
12
        upload_size: UploadSizeInfo,
710
12
    ) -> Result<u64, Error> {
711
12
        let blob_path = blob_path.to_owned();
712
12
        let max_size = match upload_size {
713
12
            UploadSizeInfo::ExactSize(
size10
) | UploadSizeInfo::MaxSize(
size2
) => size,
714
        };
715
12
        let block_size =
716
12
            cmp::min(max_size / (MAX_BLOCKS as u64 - 1), MAX_BLOCK_SIZE).max(DEFAULT_BLOCK_SIZE);
717
718
12
        let (tx, mut rx) = mpsc::channel(self.max_concurrent_uploads);
719
12
        let mut block_ids: Vec<Vec<u8>> = Vec::with_capacity(MAX_BLOCKS);
720
12
        let retrier = self.retrier.clone();
721
722
12
        let read_stream_fut = {
723
12
            let tx = tx.clone();
724
12
            let blob_path = blob_path.clone();
725
12
            async move {
726
12
                let mut total_uploaded = 0;
727
33
                for block_id in 
0..MAX_BLOCKS12
{
728
33
                    let 
write_buf32
= reader
729
33
                        .consume(Some(
730
33
                            usize::try_from(block_size)
731
33
                                .err_tip(|| "Could not convert block_size to usize")
?0
,
732
                        ))
733
33
                        .await
734
32
                        .err_tip(|| "Failed to read chunk in azure_store")
?0
;
735
736
32
                    if write_buf.is_empty() {
737
11
                        break;
738
21
                    }
739
740
21
                    total_uploaded += write_buf.len() as u64;
741
742
                    // Fixed-width, zero-padded ids keep the committed block list ordered
743
                    // after a lexicographic sort.
744
21
                    let block_id = format!("{block_id:032}").into_bytes();
745
21
                    let blob_path = blob_path.clone();
746
747
21
                    tx.send(async move {
748
21
                        self.retrier
749
21
                            .retry(unfold(
750
21
                                (write_buf, block_id),
751
21
                                move |(write_buf, block_id)| {
752
21
                                    let client = Arc::clone(&self.client);
753
21
                                    let blob_path = blob_path.clone();
754
21
                                    async move {
755
21
                                        let _request_permit = self.request_permit().await;
756
21
                                        let _permit = match fs::get_permit().await {
757
21
                                            Ok(permit) => permit,
758
0
                                            Err(e) => {
759
0
                                                return Some((
760
0
                                                    RetryResult::Retry(make_err!(
761
0
                                                        Code::Unavailable,
762
0
                                                        "Failed to acquire permit: {e}"
763
0
                                                    )),
764
0
                                                    (write_buf, block_id),
765
0
                                                ));
766
                                            }
767
                                        };
768
21
                                        let content_length = write_buf.len() as u64;
769
21
                                        let retry_result = client
770
21
                                            .blob_client(&blob_path)
771
21
                                            .block_blob_client()
772
21
                                            .stage_block(
773
21
                                                &block_id,
774
21
                                                content_length,
775
21
                                                RequestContent::from(write_buf.to_vec()),
776
21
                                                None,
777
21
                                            )
778
21
                                            .await
779
21
                                            .map_or_else(
780
2
                                                |e| {
781
2
                                                    self.retry_later(
782
2
                                                        &e,
783
2
                                                        "put",
784
2
                                                        make_err!(
785
2
                                                            Code::Aborted,
786
2
                                                            "Failed to upload block in Azure store: {e:?}"
787
2
                                                        ),
788
2
                                                    )
789
2
                                                },
790
19
                                                |_| RetryResult::Ok(block_id.clone()),
791
                                            );
792
21
                                        Some((retry_result, (write_buf, block_id)))
793
21
                                    }
794
21
                                },
795
                            ))
796
21
                            .await
797
21
                    })
798
21
                    .await
799
21
                    .map_err(|err| 
{0
800
0
                        Error::from_std_err(Code::Internal, &err)
801
0
                            .append("Failed to send block to channel")
802
0
                    })?;
803
                }
804
11
                Ok::<_, Error>(total_uploaded)
805
11
            }
806
12
            .fuse()
807
        };
808
809
12
        let mut upload_futures = FuturesUnordered::new();
810
12
        let mut total_uploaded = 0;
811
812
12
        tokio::pin!(read_stream_fut);
813
814
12
        let mut failure: Option<Error> = None;
815
        loop {
816
63
            if read_stream_fut.is_terminated() && 
rx49
.
is_empty49
() &&
upload_futures26
.
is_empty26
() {
817
9
                break;
818
54
            }
819
54
            tokio::select! {
820
54
                
result11
= &mut read_stream_fut => match
result11
{
821
11
                    Ok(uploaded) => total_uploaded = uploaded,
822
0
                    Err(err) => {
823
0
                        failure = Some(err);
824
0
                        break;
825
                    }
826
                },
827
54
                Some(
block_id21
) = upload_futures.next() => match
block_id21
{
828
19
                    Ok(block_id) => block_ids.push(block_id),
829
2
                    Err(err) => {
830
2
                        failure = Some(err);
831
2
                        break;
832
                    }
833
                },
834
54
                Some(
fut21
) = rx.recv() =>
upload_futures21
.
push21
(
fut21
),
835
            }
836
        }
837
11
        if let Some(
err2
) = failure {
838
            // The read future is never polled again, so nothing new is
839
            // staged; blocks already on their way to Azure are allowed to
840
            // land so the discard that follows sees every one of them
841
            // instead of racing a request still in flight.
842
2
            drop(rx);
843
2
            while upload_futures.next().await.is_some() 
{}0
844
2
            return Err(err);
845
9
        }
846
847
        // Reject an incomplete transfer before publishing its staged blocks.
848
        // The caller discards them using the same cleanup as other failures.
849
9
        upload_size.validate_size(total_uploaded)
?6
;
850
851
        // Sorting block IDs to ensure consistent ordering of the committed blob.
852
3
        block_ids.sort_unstable();
853
854
3
        let block_list = BlockLookupList {
855
3
            latest: Some(block_ids),
856
3
            ..Default::default()
857
3
        };
858
859
3
        retrier
860
3
            .retry(unfold(block_list, move |block_list| {
861
3
                let client = Arc::clone(&self.client);
862
3
                let blob_path = blob_path.clone();
863
864
3
                async move {
865
3
                    let _request_permit = self.request_permit().await;
866
3
                    let _permit = match fs::get_permit().await {
867
3
                        Ok(permit) => permit,
868
0
                        Err(e) => {
869
0
                            return Some((
870
0
                                RetryResult::Retry(make_err!(
871
0
                                    Code::Unavailable,
872
0
                                    "Failed to acquire permit: {e}"
873
0
                                )),
874
0
                                block_list,
875
0
                            ));
876
                        }
877
                    };
878
879
3
                    let blocks = match RequestContent::try_from(block_list.clone()) {
880
3
                        Ok(blocks) => blocks,
881
0
                        Err(e) => {
882
0
                            return Some((
883
0
                                RetryResult::Err(make_err!(
884
0
                                    Code::Internal,
885
0
                                    "Failed to serialize block list in Azure store: {e:?}"
886
0
                                )),
887
0
                                block_list,
888
0
                            ));
889
                        }
890
                    };
891
892
3
                    let retry_result = client
893
3
                        .blob_client(&blob_path)
894
3
                        .block_blob_client()
895
3
                        .commit_block_list(blocks, None)
896
3
                        .await
897
3
                        .map_or_else(
898
0
                            |e| {
899
0
                                self.retry_later(
900
0
                                    &e,
901
0
                                    "put",
902
0
                                    Error::from_std_err(Code::Aborted, &e)
903
0
                                        .append("Failed to commit block list in Azure store:"),
904
0
                                )
905
0
                            },
906
3
                            |_| RetryResult::Ok(total_uploaded),
907
                        );
908
3
                    Some((retry_result, block_list))
909
3
                }
910
3
            }))
911
3
            .await
912
11
    }
913
}
914
915
#[async_trait]
916
impl<I, NowFn> StoreDriver for AzureBlobStore<NowFn>
917
where
918
    I: InstantWrapper,
919
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
920
{
921
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
922
        Ok(())
923
0
    }
924
925
    async fn has_with_results(
926
        self: Pin<&Self>,
927
        keys: &[StoreKey<'_>],
928
        results: &mut [Option<u64>],
929
34
    ) -> Result<(), Error> {
930
        keys.iter()
931
            .zip(results.iter_mut())
932
160
            .map(|(key, result)| async move {
933
160
                if is_zero_digest(key.borrow()) {
934
1
                    *result = Some(0);
935
1
                    return Ok::<_, Error>(());
936
159
                }
937
159
                *result = self.has(key).await
?6
;
938
153
                Ok::<_, Error>(())
939
320
            })
940
            .collect::<FuturesUnordered<_>>()
941
            .try_collect()
942
            .await
943
34
    }
944
945
0
    fn optimized_for(&self, optimization: StoreOptimizations) -> bool {
946
0
        matches!(optimization, StoreOptimizations::LazyExistenceOnSync)
947
0
    }
948
949
0
    fn max_upload_size(&self) -> u64 {
950
0
        MAX_BLOCKS as u64 * MAX_BLOCK_SIZE
951
0
    }
952
953
    async fn update(
954
        self: Pin<&Self>,
955
        digest: StoreKey<'_>,
956
        mut reader: DropCloserReadHalf,
957
        upload_size: UploadSizeInfo,
958
61
    ) -> Result<u64, Error> {
959
        let blob_path = self.make_blob_path(&digest);
960
        // Handling zero-sized content check
961
        if is_zero_digest(digest.borrow()) {
962
            let size = reader.drain().await?;
963
            // The empty digest constrains the content; the caller's transfer
964
            // declaration must independently agree with the received size.
965
            UploadSizeInfo::ExactSize(0).validate_size(size)?;
966
            upload_size.validate_size(size)?;
967
            return Ok(0);
968
        }
969
970
        let max_size = match upload_size {
971
            UploadSizeInfo::ExactSize(sz) | UploadSizeInfo::MaxSize(sz) => sz,
972
        };
973
974
        // For small files of a known size we buffer to `Bytes` and upload in a single request.
975
        if max_size < DEFAULT_BLOCK_SIZE && matches!(upload_size, UploadSizeInfo::ExactSize(_)) {
976
            let UploadSizeInfo::ExactSize(sz) = upload_size else {
977
                unreachable!("upload_size must be UploadSizeInfo::ExactSize here");
978
            };
979
980
            reader.set_max_recent_data_size(
981
                u64::try_from(self.max_retry_buffer_per_request)
982
                    .err_tip(|| "Could not convert max_retry_buffer_per_request to u64")?,
983
            );
984
985
            return self
986
                .retrier
987
39
                .retry(unfold(reader, move |mut reader| {
988
39
                    let client = Arc::clone(&self.client);
989
39
                    let blob_path = blob_path.clone();
990
39
                    async move {
991
                        // Collect through EOF before issuing a PUT. A short or failed
992
                        // producer must never replace a valid object with partial data.
993
                        // No permit is held meanwhile: the bytes arrive at the
994
                        // producer's pace, and no connection is open yet.
995
39
                        let 
content37
= match reader
996
39
                            .consume(Some(usize::try_from(sz).unwrap_or(usize::MAX).saturating_add(1)))
997
39
                            .await
998
                        {
999
37
                            Ok(content) => content,
1000
2
                            Err(err) => return Some((RetryResult::Err(err), reader)),
1001
                        };
1002
37
                        if let Err(
err2
) = upload_size.validate_size(content.len() as u64) {
1003
2
                            return Some((RetryResult::Err(err), reader));
1004
35
                        }
1005
1006
35
                        let _request_permit = self.request_permit().await;
1007
35
                        let _permit = match fs::get_permit().await {
1008
35
                            Ok(permit) => permit,
1009
0
                            Err(e) => {
1010
                                // The next attempt reads the content again.
1011
0
                                let err = make_err!(
1012
0
                                    Code::Unavailable,
1013
                                    "Failed to acquire permit: {e}"
1014
                                );
1015
0
                                let retry = match reader.try_reset_stream() {
1016
0
                                    Ok(()) => RetryResult::Retry(err),
1017
0
                                    Err(reset_err) => RetryResult::Err(err.merge(reset_err)),
1018
                                };
1019
0
                                return Some((retry, reader));
1020
                            }
1021
                        };
1022
35
                        let result = client
1023
35
                            .blob_client(&blob_path)
1024
35
                            .block_blob_client()
1025
35
                            .upload(RequestContent::from(content.to_vec()), None)
1026
35
                            .await
1027
35
                            .map(|_| ())
1028
35
                            .map_err(|e| (
1029
1
                                make_err!(Code::Aborted, "{e:?}"),
1030
1
                                self.throttle_wait(&e, "put"),
1031
                            ));
1032
1033
35
                        match result {
1034
                            Ok(()) => {
1035
34
                                Some((RetryResult::Ok(reader.get_bytes_received()), reader))
1036
                            }
1037
1
                            Err((mut err, throttle_wait)) => {
1038
1
                                err.code = Code::Aborted;
1039
1
                                let bytes_received = reader.get_bytes_received();
1040
1041
1
                                if let Err(
try_reset_err0
) = reader.try_reset_stream() {
1042
0
                                    event!(
1043
0
                                        Level::ERROR,
1044
                                        ?bytes_received,
1045
                                        err = ?try_reset_err,
1046
                                        "Unable to reset stream after failed upload in AzureStore::update"
1047
                                    );
1048
0
                                    Some((
1049
0
                                        RetryResult::Err(err.merge(try_reset_err).append(format!(
1050
0
                                            "Failed to retry upload with {bytes_received} bytes received in AzureStore::update"
1051
0
                                        ))),
1052
0
                                        reader,
1053
0
                                    ))
1054
                                } else {
1055
1
                                    let err = err.append(format!(
1056
                                        "Retry on upload happened with {bytes_received} bytes received in AzureStore::update"
1057
                                    ));
1058
1
                                    event!(
1059
1
                                        Level::INFO,
1060
                                        ?err,
1061
                                        ?bytes_received,
1062
                                        "Retryable Azure error"
1063
                                    );
1064
1
                                    let retry = match throttle_wait {
1065
0
                                        Some(wait) => RetryResult::RetryAfter(err, wait),
1066
1
                                        None => RetryResult::Retry(err),
1067
                                    };
1068
1
                                    Some((retry, reader))
1069
                                }
1070
                            }
1071
                        }
1072
39
                    }
1073
39
                }))
1074
                .await;
1075
        }
1076
1077
        // For larger files we stream the content as staged blocks and commit a block list.
1078
        // A block that is staged but never committed is billed until Azure collects it a
1079
        // week later, so a failure anywhere past this point discards what was staged.
1080
        let mut guard = StagedBlocksGuard {
1081
            client: Arc::clone(&self.client),
1082
            requests: self.request_permits.clone(),
1083
            blob_path: blob_path.clone(),
1084
            armed: true,
1085
        };
1086
        let result = self
1087
            .upload_staged_blocks(&blob_path, reader, upload_size)
1088
            .await;
1089
        if result.is_ok() {
1090
            guard.disarm();
1091
        }
1092
        result
1093
61
    }
1094
1095
    async fn get_part(
1096
        self: Pin<&Self>,
1097
        key: StoreKey<'_>,
1098
        writer: &mut DropCloserWriteHalf,
1099
        offset: u64,
1100
        length: Option<u64>,
1101
40
    ) -> Result<(), Error> {
1102
        if is_zero_digest(key.borrow()) {
1103
            writer
1104
                .send_eof()
1105
                .err_tip(|| "Failed to send zero EOF in azure store get_part")?;
1106
            return Ok(());
1107
        }
1108
1109
        let blob_path = self.make_blob_path(&key);
1110
1111
        // How many bytes this read may yield: the explicit length when the
1112
        // caller gives one, else the whole object. The digest's own size is
1113
        // not a bound here. A compression store above reads the object it
1114
        // wrote under this key, and an lz4 frame of data that does not
1115
        // compress is longer than the digest it is stored under; cut at the
1116
        // digest's size it is a torn frame and every read of that blob fails.
1117
        // A raw object longer than its digest, which the upload-size
1118
        // validation no longer publishes, is the verify store's to judge.
1119
        let size_from_key = match &key {
1120
            StoreKey::Digest(digest) => Some(digest.size_bytes()),
1121
            StoreKey::Str(_) => None,
1122
        };
1123
        let max_bytes = length;
1124
1125
        self.retrier
1126
57
            .retry(unfold(writer, move |writer| {
1127
                // Resumes where the writer stopped, so a retry continues the
1128
                // read instead of starting it again and appending a second
1129
                // copy of what the consumer already has. The S3 store does
1130
                // the same; this one did not, and with a throttling account
1131
                // every retry doubled up.
1132
57
                let sent = writer.get_bytes_written();
1133
57
                let remaining = max_bytes.map(|max| 
max4
.
saturating_sub4
(
sent4
));
1134
57
                let range = match 
remaining53
{
1135
0
                    Some(0) => None,
1136
4
                    Some(rem) => Some(HttpRange::new(offset + sent, rem)),
1137
53
                    None if offset + sent == 0 => None,
1138
0
                    None => Some(HttpRange::from_offset(offset + sent)),
1139
                };
1140
57
                let client = Arc::clone(&self.client);
1141
57
                let blob_path = blob_path.clone();
1142
57
                async move {
1143
                    // Held through the body: what is in flight is what the
1144
                    // read limit says, not only what has been started.
1145
57
                    let _read_permit = self.read_permit().await;
1146
57
                    let _request_permit = self.request_permit().await;
1147
57
                    let _permit = match fs::get_permit().await {
1148
57
                        Ok(permit) => permit,
1149
0
                        Err(e) => {
1150
0
                            return Some((
1151
0
                                RetryResult::Retry(make_err!(
1152
0
                                    Code::Unavailable,
1153
0
                                    "Failed to acquire permit: {e}"
1154
0
                                )),
1155
0
                                writer,
1156
0
                            ));
1157
                        }
1158
                    };
1159
1160
                    // The error carries the wait a throttled response asked for.
1161
57
                    let result: Result<(), (Error, Option<Duration>)> = async {
1162
57
                        let options = BlobClientDownloadOptions {
1163
57
                            range,
1164
57
                            ..Default::default()
1165
57
                        };
1166
57
                        let 
response38
= client
1167
57
                            .blob_client(&blob_path)
1168
57
                            .download(Some(options))
1169
57
                            .await
1170
57
                            .map_err(|e| 
{19
1171
19
                                if e.http_status() == Some(StatusCode::NotFound) {
1172
1
                                    (
1173
1
                                        make_err!(Code::NotFound, "Blob not found in Azure: {e:?}"),
1174
1
                                        None,
1175
1
                                    )
1176
                                } else {
1177
18
                                    (
1178
18
                                        make_err!(
1179
18
                                            Code::Aborted,
1180
18
                                            "Failed to start download from Azure: {e:?}"
1181
18
                                        ),
1182
18
                                        self.throttle_wait(&e, "get"),
1183
18
                                    )
1184
                                }
1185
19
                            })?;
1186
1187
38
                        let mut body = response.body;
1188
73
                        while let Some(
chunk38
) = body.try_next().await.map_err(|e|
{0
1189
0
                            (
1190
0
                                make_err!(Code::Aborted, "Error reading from Azure stream: {e:?}"),
1191
0
                                self.throttle_wait(&e, "get"),
1192
0
                            )
1193
0
                        })? {
1194
38
                            if chunk.is_empty() {
1195
2
                                continue;
1196
36
                            }
1197
                            // Trim the last chunk rather than forward it
1198
                            // whole. Azure hands back the block it read, so
1199
                            // the final one runs past the end of a blob whose
1200
                            // length is not a multiple of the block size.
1201
36
                            let chunk = match max_bytes {
1202
3
                                Some(max) => {
1203
3
                                    let sent = writer.get_bytes_written();
1204
3
                                    if sent >= max {
1205
0
                                        break;
1206
3
                                    }
1207
3
                                    let room = usize::try_from(max - sent).unwrap_or(usize::MAX);
1208
3
                                    if chunk.len() > room {
1209
2
                                        chunk.slice(..room)
1210
                                    } else {
1211
1
                                        chunk
1212
                                    }
1213
                                }
1214
33
                                None => chunk,
1215
                            };
1216
36
                            writer.send(chunk).await.map_err(|e| 
{0
1217
0
                                (
1218
0
                                    make_err!(
1219
0
                                        Code::Aborted,
1220
0
                                        "Failed to send data to writer: {e:?}"
1221
0
                                    ),
1222
0
                                    None,
1223
0
                                )
1224
0
                            })?;
1225
36
                            if max_bytes.is_some_and(|max| 
writer3
.
get_bytes_written3
() >=
max3
) {
1226
3
                                break;
1227
33
                            }
1228
                        }
1229
1230
                        // A body of no bytes where the digest names some is
1231
                        // the empty object a failed upload left behind, not
1232
                        // the blob: NotFound sends the client back to upload
1233
                        // it, where EOF would hand on an empty input.
1234
38
                        if writer.get_bytes_written() == 0
1235
2
                            && size_from_key.is_some_and(|size| size > offset)
1236
2
                            && max_bytes.is_none_or(|max| 
max0
> 0)
1237
                        {
1238
2
                            return Err((
1239
2
                                make_err!(
1240
2
                                    Code::NotFound,
1241
2
                                    "Azure holds an empty object for {blob_path}, which names {} bytes; treating it as missing so the client uploads it again",
1242
2
                                    size_from_key.unwrap_or(0)
1243
2
                                ),
1244
2
                                None,
1245
2
                            ));
1246
36
                        }
1247
36
                        writer.send_eof().map_err(|e| 
{0
1248
0
                            (
1249
0
                                make_err!(Code::Aborted, "Failed to send EOF to writer: {e:?}"),
1250
0
                                None,
1251
0
                            )
1252
0
                        })?;
1253
36
                        Ok(())
1254
57
                    }
1255
57
                    .await;
1256
1257
18
                    match result {
1258
                        Ok(()) => {
1259
36
                            self.note_read_ok();
1260
36
                            Some((RetryResult::Ok(()), writer))
1261
                        }
1262
21
                        Err((
e3
, _)) if e.code == Code::NotFoun
d3
=> {
1263
3
                            self.note_read_ok();
1264
3
                            Some((RetryResult::Err(e), writer))
1265
                        }
1266
16
                        Err((e, Some(wait))) => {
1267
16
                            self.note_read_throttled();
1268
16
                            Some((RetryResult::RetryAfter(e, wait), writer))
1269
                        }
1270
2
                        Err((e, None)) => Some((RetryResult::Retry(e), writer)),
1271
                    }
1272
57
                }
1273
57
            }))
1274
            .await
1275
40
    }
1276
1277
0
    fn inner_store(&self, _digest: Option<StoreKey>) -> &'_ dyn StoreDriver {
1278
0
        self
1279
0
    }
1280
1281
0
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
1282
0
        self
1283
0
    }
1284
1285
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
1286
0
        self
1287
0
    }
1288
1289
0
    fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) {
1290
0
        registry.register_indicator(self);
1291
0
    }
1292
1293
0
    fn register_remove_callback(self: Arc<Self>, _callback: RemoveCallback) -> Result<(), Error> {
1294
        // Azure Blob Storage manages object lifecycle externally,
1295
        // so we can safely ignore remove callbacks.
1296
0
        Ok(())
1297
0
    }
1298
}
1299
1300
#[async_trait]
1301
impl<I, NowFn> HealthStatusIndicator for AzureBlobStore<NowFn>
1302
where
1303
    I: InstantWrapper,
1304
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
1305
{
1306
0
    fn get_name(&self) -> &'static str {
1307
0
        "AzureBlobStore"
1308
0
    }
1309
1310
0
    async fn check_health(&self, namespace: Cow<'static, str>) -> HealthStatus {
1311
        StoreDriver::check_health(Pin::new(self), namespace).await
1312
0
    }
1313
}