Coverage Report

Created: 2026-07-21 15:28

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 The NativeLink Authors. 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::cmp;
16
use core::pin::Pin;
17
use core::time::Duration;
18
use std::borrow::Cow;
19
use std::sync::Arc;
20
21
use async_trait::async_trait;
22
use azure_core::credentials::TokenCredential;
23
use azure_core::error::ErrorKind;
24
use azure_core::http::{RequestContent, RetryOptions, StatusCode, Transport, Url};
25
use azure_identity::WorkloadIdentityCredential;
26
use azure_storage_blob::clients::{BlobContainerClient, BlobContainerClientOptions};
27
use azure_storage_blob::models::{
28
    BlobClientDownloadOptions, BlobClientGetPropertiesResultHeaders, BlockLookupList, HttpRange,
29
    StorageErrorCode,
30
};
31
use futures::future::FusedFuture;
32
use futures::stream::{FuturesUnordered, unfold};
33
use futures::{FutureExt, StreamExt, TryStreamExt};
34
use nativelink_config::stores::ExperimentalAzureSpec;
35
use nativelink_error::{Code, Error, ResultExt, make_err};
36
use nativelink_metric::MetricsComponent;
37
use nativelink_util::buf_channel::{
38
    DropCloserReadHalf, DropCloserWriteHalf, make_buf_channel_pair,
39
};
40
use nativelink_util::fs;
41
use nativelink_util::health_utils::{HealthRegistryBuilder, HealthStatus, HealthStatusIndicator};
42
use nativelink_util::instant_wrapper::InstantWrapper;
43
use nativelink_util::retry::{Retrier, RetryResult};
44
use nativelink_util::store_trait::{
45
    RemoveCallback, StoreDriver, StoreKey, StoreOptimizations, UploadSizeInfo,
46
};
47
use tokio::sync::mpsc;
48
use tokio::time::sleep;
49
use tracing::{Level, event};
50
51
use crate::cas_utils::is_zero_digest;
52
use crate::common_s3_utils::install_default_rustls_crypto_provider;
53
54
// Check the below doc for the limits specific to Azure.
55
// https://learn.microsoft.com/en-us/azure/storage/blobs/scalability-targets#scale-targets-for-blob-storage
56
57
// Maximum number of blocks in a block blob or append blob
58
const MAX_BLOCKS: usize = 50_000;
59
60
// Maximum size of a block in a block blob (4,000 MiB)
61
const MAX_BLOCK_SIZE: u64 = 4_000 * 1024 * 1024; // 4,000 MiB = 4 GiB
62
63
// Default block size for uploads (5 MiB)
64
const DEFAULT_BLOCK_SIZE: u64 = 5 * 1024 * 1024; // 5 MiB
65
66
// Default maximum retry buffer per request
67
const DEFAULT_MAX_RETRY_BUFFER_PER_REQUEST: usize = 5 * 1024 * 1024; // 5 MiB
68
69
// Default maximum number of concurrent uploads
70
const DEFAULT_MAX_CONCURRENT_UPLOADS: usize = 10;
71
72
// Default public Azure Blob Storage endpoint suffix.
73
const DEFAULT_BLOB_ENDPOINT_SUFFIX: &str = "blob.core.windows.net";
74
75
#[derive(MetricsComponent)]
76
pub struct AzureBlobStore<NowFn> {
77
    client: Arc<BlobContainerClient>,
78
    now_fn: NowFn,
79
    #[metric(help = "The container name for the Azure store")]
80
    container: String,
81
    #[metric(help = "The blob prefix for the Azure store")]
82
    blob_prefix: String,
83
    retrier: Retrier,
84
    #[metric(help = "The number of seconds to consider an object expired")]
85
    consider_expired_after_s: i64,
86
    #[metric(help = "The number of bytes to buffer for retrying requests")]
87
    max_retry_buffer_per_request: usize,
88
    #[metric(help = "The number of concurrent uploads allowed")]
89
    max_concurrent_uploads: usize,
90
}
91
92
impl<NowFn> core::fmt::Debug for AzureBlobStore<NowFn> {
93
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
94
0
        f.debug_struct("AzureBlobStore")
95
0
            .field("container", &self.container)
96
0
            .field("blob_prefix", &self.blob_prefix)
97
0
            .field("consider_expired_after_s", &self.consider_expired_after_s)
98
0
            .finish_non_exhaustive()
99
0
    }
100
}
101
102
impl<I, NowFn> AzureBlobStore<NowFn>
103
where
104
    I: InstantWrapper,
105
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
106
{
107
0
    pub async fn new(spec: &ExperimentalAzureSpec, now_fn: NowFn) -> Result<Arc<Self>, Error> {
108
0
        let jitter_fn = spec.common.retry.make_jitter_fn();
109
0
        let client = Self::build_container_client(spec)?;
110
0
        Self::new_with_client_and_jitter(spec, client, jitter_fn, now_fn)
111
0
    }
112
113
    /// Builds the container URL and selects the auth strategy:
114
    ///   * `sas_url` set    -> use it verbatim as the container URL with no credential.
115
    ///   * otherwise        -> `https://{account}.{endpoint}/{container}` authenticated with
116
    ///     Entra ID via Workload Identity (keyless).
117
0
    fn build_container_client(spec: &ExperimentalAzureSpec) -> Result<BlobContainerClient, Error> {
118
0
        let mut options = BlobContainerClientOptions::default();
119
0
        options.client_options.retry = RetryOptions::none();
120
        // Hand the SDK an HTTP client with an explicit rustls (ring) config.
121
0
        options.client_options.transport = Some(Self::build_http_transport()?);
122
123
0
        let (container_url, credential): (Url, Option<Arc<dyn TokenCredential>>) =
124
0
            if let Some(sas_url) = spec.sas_url.as_ref() {
125
0
                let url = Url::parse(sas_url)
126
0
                    .map_err(|e| make_err!(Code::InvalidArgument, "Invalid Azure sas_url: {e}"))?;
127
0
                (url, None)
128
            } else {
129
0
                let endpoint = spec.endpoint.clone().unwrap_or_else(|| {
130
0
                    format!(
131
                        "https://{}.{DEFAULT_BLOB_ENDPOINT_SUFFIX}",
132
                        spec.account_name
133
                    )
134
0
                });
135
0
                let mut url = Url::parse(&endpoint)
136
0
                    .map_err(|e| make_err!(Code::InvalidArgument, "Invalid Azure endpoint: {e}"))?;
137
0
                url.path_segments_mut()
138
0
                    .map_err(|()| {
139
0
                        make_err!(
140
0
                            Code::InvalidArgument,
141
                            "Azure endpoint is not a valid base URL: {endpoint}"
142
                        )
143
0
                    })?
144
0
                    .pop_if_empty()
145
0
                    .push(&spec.container);
146
0
                let credential: Arc<dyn TokenCredential> = WorkloadIdentityCredential::new(None)
147
0
                    .map_err(|e| {
148
0
                        make_err!(
149
0
                            Code::FailedPrecondition,
150
                            "Failed to create Azure Workload Identity credential: {e}"
151
                        )
152
0
                    })?;
153
0
                (url, Some(credential))
154
            };
155
156
0
        BlobContainerClient::new(container_url, credential, Some(options))
157
0
            .map_err(|e| make_err!(Code::Unavailable, "Failed to create Azure client: {e}"))
158
0
    }
159
160
    /// Builds an HTTP transport for the Azure SDK backed by a reqwest client with
161
    /// an explicit rustls config using `NativeLink`'s ring crypto provider, so the
162
    /// SDK never falls back to guessing a provider (which breaks HTTPS here).
163
0
    fn build_http_transport() -> Result<Transport, Error> {
164
0
        install_default_rustls_crypto_provider();
165
166
0
        let mut roots = rustls::RootCertStore::empty();
167
0
        roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
168
0
        let tls_config = rustls::ClientConfig::builder()
169
0
            .with_root_certificates(roots)
170
0
            .with_no_client_auth();
171
172
0
        let client = reqwest::Client::builder()
173
0
            .use_preconfigured_tls(tls_config)
174
0
            .build()
175
0
            .map_err(|e| make_err!(Code::Unavailable, "Failed to build Azure HTTP client: {e}"))?;
176
177
0
        Ok(Transport::new(Arc::new(client)))
178
0
    }
179
180
14
    pub fn new_with_client_and_jitter(
181
14
        spec: &ExperimentalAzureSpec,
182
14
        client: BlobContainerClient,
183
14
        jitter_fn: Arc<dyn Fn(Duration) -> Duration + Send + Sync>,
184
14
        now_fn: NowFn,
185
14
    ) -> Result<Arc<Self>, Error> {
186
14
        Ok(Arc::new(Self {
187
14
            client: Arc::new(client),
188
14
            now_fn,
189
14
            container: spec.container.clone(),
190
14
            blob_prefix: spec
191
14
                .common
192
14
                .key_prefix
193
14
                .as_ref()
194
14
                .unwrap_or(&String::new())
195
14
                .clone(),
196
14
            retrier: Retrier::new(
197
14
                Arc::new(|duration| 
Box::pin5
(
sleep5
(
duration5
))),
198
14
                jitter_fn,
199
14
                spec.common.retry.clone(),
200
            ),
201
14
            consider_expired_after_s: i64::from(spec.common.consider_expired_after_s),
202
14
            max_retry_buffer_per_request: spec
203
14
                .common
204
14
                .max_retry_buffer_per_request
205
14
                .unwrap_or(DEFAULT_MAX_RETRY_BUFFER_PER_REQUEST),
206
14
            max_concurrent_uploads: spec
207
14
                .common
208
14
                .multipart_max_concurrent_uploads
209
14
                .map_or(DEFAULT_MAX_CONCURRENT_UPLOADS, |v| v),
210
        }))
211
14
    }
212
213
13
    fn make_blob_path(&self, key: &StoreKey<'_>) -> String {
214
13
        format!("{}{}", self.blob_prefix, key.as_str())
215
13
    }
216
217
5
    async fn has(self: Pin<&Self>, digest: &StoreKey<'_>) -> Result<Option<u64>, Error> {
218
5
        let blob_path = self.make_blob_path(digest);
219
220
5
        self.retrier
221
6
            .
retry5
(
unfold5
(
()5
, move |state| {
222
6
                let blob_path = blob_path.clone();
223
6
                let client = Arc::clone(&self.client);
224
6
                async move {
225
6
                    let _permit = match fs::get_permit().await {
226
6
                        Ok(permit) => permit,
227
0
                        Err(e) => {
228
0
                            return Some((
229
0
                                RetryResult::Retry(make_err!(
230
0
                                    Code::Unavailable,
231
0
                                    "Failed to acquire permit: {e}"
232
0
                                )),
233
0
                                state,
234
0
                            ));
235
                        }
236
                    };
237
238
6
                    let result = client.blob_client(&blob_path).get_properties(None).await;
239
240
6
                    match result {
241
4
                        Ok(props) => {
242
4
                            if self.consider_expired_after_s > 0
243
2
                                && let Some(last_modified) = props.last_modified().ok().flatten()
244
                            {
245
2
                                let now = (self.now_fn)().unix_timestamp() as i64;
246
2
                                if last_modified.unix_timestamp() + self.consider_expired_after_s
247
2
                                    <= now
248
                                {
249
1
                                    return Some((RetryResult::Ok(None), state));
250
1
                                }
251
2
                            }
252
3
                            let blob_size = props.content_length().ok().flatten().unwrap_or(0);
253
3
                            Some((RetryResult::Ok(Some(blob_size)), state))
254
                        }
255
2
                        Err(err) => {
256
2
                            if err.http_status() == Some(StatusCode::NotFound) {
257
                                // Distinguish a missing container (a config error) from a
258
                                // missing blob (a normal cache miss).
259
                                if let ErrorKind::HttpResponse {
260
0
                                    error_code: Some(error_code),
261
                                    ..
262
1
                                } = err.kind()
263
0
                                    && error_code == StorageErrorCode::ContainerNotFound.as_ref()
264
                                {
265
0
                                    return Some((
266
0
                                        RetryResult::Err(make_err!(
267
0
                                            Code::InvalidArgument,
268
0
                                            "Container not found: {err}"
269
0
                                        )),
270
0
                                        state,
271
0
                                    ));
272
1
                                }
273
1
                                Some((RetryResult::Ok(None), state))
274
                            } else {
275
1
                                Some((
276
1
                                    RetryResult::Retry(make_err!(
277
1
                                        Code::Unavailable,
278
1
                                        "Failed to get blob properties: {err:?}"
279
1
                                    )),
280
1
                                    state,
281
1
                                ))
282
                            }
283
                        }
284
                    }
285
6
                }
286
6
            }))
287
5
            .await
288
5
    }
289
}
290
291
#[async_trait]
292
impl<I, NowFn> StoreDriver for AzureBlobStore<NowFn>
293
where
294
    I: InstantWrapper,
295
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
296
{
297
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
298
        Ok(())
299
0
    }
300
301
    async fn has_with_results(
302
        self: Pin<&Self>,
303
        keys: &[StoreKey<'_>],
304
        results: &mut [Option<u64>],
305
6
    ) -> Result<(), Error> {
306
        keys.iter()
307
            .zip(results.iter_mut())
308
6
            .map(|(key, result)| async move {
309
6
                if is_zero_digest(key.borrow()) {
310
1
                    *result = Some(0);
311
1
                    return Ok::<_, Error>(());
312
5
                }
313
5
                *result = self.has(key).await
?0
;
314
5
                Ok::<_, Error>(())
315
12
            })
316
            .collect::<FuturesUnordered<_>>()
317
            .try_collect()
318
            .await
319
6
    }
320
321
0
    fn optimized_for(&self, optimization: StoreOptimizations) -> bool {
322
0
        matches!(optimization, StoreOptimizations::LazyExistenceOnSync)
323
0
    }
324
325
    async fn update(
326
        self: Pin<&Self>,
327
        digest: StoreKey<'_>,
328
        mut reader: DropCloserReadHalf,
329
        upload_size: UploadSizeInfo,
330
4
    ) -> Result<u64, Error> {
331
        let blob_path = self.make_blob_path(&digest);
332
        // Handling zero-sized content check
333
        if upload_size == UploadSizeInfo::ExactSize(0) {
334
            return Ok(0);
335
        }
336
337
        let max_size = match upload_size {
338
            UploadSizeInfo::ExactSize(sz) | UploadSizeInfo::MaxSize(sz) => sz,
339
        };
340
341
        // For small files of a known size we buffer to `Bytes` and upload in a single request.
342
        if max_size < DEFAULT_BLOCK_SIZE && matches!(upload_size, UploadSizeInfo::ExactSize(_)) {
343
            let UploadSizeInfo::ExactSize(sz) = upload_size else {
344
                unreachable!("upload_size must be UploadSizeInfo::ExactSize here");
345
            };
346
347
            reader.set_max_recent_data_size(
348
                u64::try_from(self.max_retry_buffer_per_request)
349
                    .err_tip(|| "Could not convert max_retry_buffer_per_request to u64")?,
350
            );
351
352
            return self
353
                .retrier
354
3
                .retry(unfold(reader, move |mut reader| {
355
3
                    let client = Arc::clone(&self.client);
356
3
                    let blob_path = blob_path.clone();
357
3
                    async move {
358
3
                        let _permit = match fs::get_permit().await {
359
3
                            Ok(permit) => permit,
360
0
                            Err(e) => {
361
0
                                return Some((
362
0
                                    RetryResult::Retry(make_err!(
363
0
                                        Code::Unavailable,
364
0
                                        "Failed to acquire permit: {e}"
365
0
                                    )),
366
0
                                    reader,
367
0
                                ));
368
                            }
369
                        };
370
371
3
                        let (mut tx, mut rx) = make_buf_channel_pair();
372
373
3
                        let result = {
374
3
                            let reader_ref = &mut reader;
375
3
                            let (upload_res, bind_res) = tokio::join!(
376
3
                                async {
377
3
                                    let mut buffer = Vec::with_capacity(
378
3
                                        usize::try_from(sz).expect(
379
3
                                            "size must be non-negative and fit in usize",
380
                                        ),
381
                                    );
382
9
                                    while let Ok(Some(
chunk6
)) = rx.try_next().await {
383
6
                                        buffer.extend_from_slice(&chunk);
384
6
                                    }
385
386
3
                                    client
387
3
                                        .blob_client(&blob_path)
388
3
                                        .block_blob_client()
389
3
                                        .upload(RequestContent::from(buffer), None)
390
3
                                        .await
391
3
                                        .map(|_| ())
392
3
                                        .map_err(|e| 
make_err!1
(
Code::Aborted1
, "{e:?}"))
393
3
                                },
394
3
                                async { tx.bind_buffered(reader_ref).await }
395
                            );
396
397
3
                            match (upload_res, bind_res) {
398
2
                                (Ok(()), Ok(())) => Ok(()),
399
1
                                (Err(e), _) | (_, Err(
e0
)) => Err(e),
400
                            }
401
3
                            .err_tip(|| "Failed to upload blob in single chunk")
402
                        };
403
404
3
                        match result {
405
                            Ok(()) => {
406
2
                                Some((RetryResult::Ok(reader.get_bytes_received()), reader))
407
                            }
408
1
                            Err(mut err) => {
409
1
                                err.code = Code::Aborted;
410
1
                                let bytes_received = reader.get_bytes_received();
411
412
1
                                if let Err(
try_reset_err0
) = reader.try_reset_stream() {
413
0
                                    event!(
414
0
                                        Level::ERROR,
415
                                        ?bytes_received,
416
                                        err = ?try_reset_err,
417
                                        "Unable to reset stream after failed upload in AzureStore::update"
418
                                    );
419
0
                                    Some((
420
0
                                        RetryResult::Err(err.merge(try_reset_err).append(format!(
421
0
                                            "Failed to retry upload with {bytes_received} bytes received in AzureStore::update"
422
0
                                        ))),
423
0
                                        reader,
424
0
                                    ))
425
                                } else {
426
1
                                    let err = err.append(format!(
427
                                        "Retry on upload happened with {bytes_received} bytes received in AzureStore::update"
428
                                    ));
429
1
                                    event!(
430
1
                                        Level::INFO,
431
                                        ?err,
432
                                        ?bytes_received,
433
                                        "Retryable Azure error"
434
                                    );
435
1
                                    Some((RetryResult::Retry(err), reader))
436
                                }
437
                            }
438
                        }
439
3
                    }
440
3
                }))
441
                .await;
442
        }
443
444
        // For larger files we stream the content as staged blocks and commit a block list.
445
        let block_size =
446
            cmp::min(max_size / (MAX_BLOCKS as u64 - 1), MAX_BLOCK_SIZE).max(DEFAULT_BLOCK_SIZE);
447
448
        let (tx, mut rx) = mpsc::channel(self.max_concurrent_uploads);
449
        let mut block_ids: Vec<Vec<u8>> = Vec::with_capacity(MAX_BLOCKS);
450
        let retrier = self.retrier.clone();
451
452
        let read_stream_fut = {
453
            let tx = tx.clone();
454
            let blob_path = blob_path.clone();
455
1
            async move {
456
1
                let mut total_uploaded = 0;
457
4
                for block_id in 
0..MAX_BLOCKS1
{
458
4
                    let write_buf = reader
459
4
                        .consume(Some(
460
4
                            usize::try_from(block_size)
461
4
                                .err_tip(|| "Could not convert block_size to usize")
?0
,
462
                        ))
463
4
                        .await
464
4
                        .err_tip(|| "Failed to read chunk in azure_store")
?0
;
465
466
4
                    if write_buf.is_empty() {
467
1
                        break;
468
3
                    }
469
470
3
                    total_uploaded += write_buf.len() as u64;
471
472
                    // Fixed-width, zero-padded ids keep the committed block list ordered
473
                    // after a lexicographic sort.
474
3
                    let block_id = format!("{block_id:032}").into_bytes();
475
3
                    let blob_path = blob_path.clone();
476
477
3
                    tx.send(async move {
478
3
                        self.retrier
479
3
                            .retry(unfold(
480
3
                                (write_buf, block_id),
481
3
                                move |(write_buf, block_id)| {
482
3
                                    let client = Arc::clone(&self.client);
483
3
                                    let blob_path = blob_path.clone();
484
3
                                    async move {
485
3
                                        let _permit = match fs::get_permit().await {
486
3
                                            Ok(permit) => permit,
487
0
                                            Err(e) => {
488
0
                                                return Some((
489
0
                                                    RetryResult::Retry(make_err!(
490
0
                                                        Code::Unavailable,
491
0
                                                        "Failed to acquire permit: {e}"
492
0
                                                    )),
493
0
                                                    (write_buf, block_id),
494
0
                                                ));
495
                                            }
496
                                        };
497
3
                                        let content_length = write_buf.len() as u64;
498
3
                                        let retry_result = client
499
3
                                            .blob_client(&blob_path)
500
3
                                            .block_blob_client()
501
3
                                            .stage_block(
502
3
                                                &block_id,
503
3
                                                content_length,
504
3
                                                RequestContent::from(write_buf.to_vec()),
505
3
                                                None,
506
3
                                            )
507
3
                                            .await
508
3
                                            .map_or_else(
509
0
                                                |e| {
510
0
                                                    RetryResult::Retry(make_err!(
511
0
                                                        Code::Aborted,
512
0
                                                        "Failed to upload block in Azure store: {e:?}"
513
0
                                                    ))
514
0
                                                },
515
3
                                                |_| RetryResult::Ok(block_id.clone()),
516
                                            );
517
3
                                        Some((retry_result, (write_buf, block_id)))
518
3
                                    }
519
3
                                },
520
                            ))
521
3
                            .await
522
3
                    })
523
3
                    .await
524
3
                    .map_err(|err| 
{0
525
0
                        Error::from_std_err(Code::Internal, &err)
526
0
                            .append("Failed to send block to channel")
527
0
                    })?;
528
                }
529
1
                Ok::<_, Error>(total_uploaded)
530
1
            }
531
            .fuse()
532
        };
533
534
        let mut upload_futures = FuturesUnordered::new();
535
        let mut total_uploaded = 0;
536
537
        tokio::pin!(read_stream_fut);
538
539
        loop {
540
            if read_stream_fut.is_terminated() && rx.is_empty() && upload_futures.is_empty() {
541
                break;
542
            }
543
            tokio::select! {
544
                result = &mut read_stream_fut => {
545
                    total_uploaded = result?;
546
                },
547
                Some(block_id) = upload_futures.next() => block_ids.push(block_id?),
548
                Some(fut) = rx.recv() => upload_futures.push(fut),
549
            }
550
        }
551
552
        // Sorting block IDs to ensure consistent ordering of the committed blob.
553
        block_ids.sort_unstable();
554
555
        let block_list = BlockLookupList {
556
            latest: Some(block_ids),
557
            ..Default::default()
558
        };
559
560
        retrier
561
1
            .retry(unfold(block_list, move |block_list| {
562
1
                let client = Arc::clone(&self.client);
563
1
                let blob_path = blob_path.clone();
564
565
1
                async move {
566
1
                    let _permit = match fs::get_permit().await {
567
1
                        Ok(permit) => permit,
568
0
                        Err(e) => {
569
0
                            return Some((
570
0
                                RetryResult::Retry(make_err!(
571
0
                                    Code::Unavailable,
572
0
                                    "Failed to acquire permit: {e}"
573
0
                                )),
574
0
                                block_list,
575
0
                            ));
576
                        }
577
                    };
578
579
1
                    let blocks = match RequestContent::try_from(block_list.clone()) {
580
1
                        Ok(blocks) => blocks,
581
0
                        Err(e) => {
582
0
                            return Some((
583
0
                                RetryResult::Err(make_err!(
584
0
                                    Code::Internal,
585
0
                                    "Failed to serialize block list in Azure store: {e:?}"
586
0
                                )),
587
0
                                block_list,
588
0
                            ));
589
                        }
590
                    };
591
592
1
                    let retry_result = client
593
1
                        .blob_client(&blob_path)
594
1
                        .block_blob_client()
595
1
                        .commit_block_list(blocks, None)
596
1
                        .await
597
1
                        .map_or_else(
598
0
                            |e| {
599
0
                                RetryResult::Retry(
600
0
                                    Error::from_std_err(Code::Aborted, &e)
601
0
                                        .append("Failed to commit block list in Azure store:"),
602
0
                                )
603
0
                            },
604
1
                            |_| RetryResult::Ok(total_uploaded),
605
                        );
606
1
                    Some((retry_result, block_list))
607
1
                }
608
1
            }))
609
            .await
610
4
    }
611
612
    async fn get_part(
613
        self: Pin<&Self>,
614
        key: StoreKey<'_>,
615
        writer: &mut DropCloserWriteHalf,
616
        offset: u64,
617
        length: Option<u64>,
618
5
    ) -> Result<(), Error> {
619
        if is_zero_digest(key.borrow()) {
620
            writer
621
                .send_eof()
622
                .err_tip(|| "Failed to send zero EOF in azure store get_part")?;
623
            return Ok(());
624
        }
625
626
        let blob_path = self.make_blob_path(&key);
627
628
        let range = match length {
629
            Some(len) => Some(HttpRange::new(offset, len)),
630
            None if offset == 0 => None,
631
            None => Some(HttpRange::from_offset(offset)),
632
        };
633
634
        self.retrier
635
7
            .retry(unfold(writer, move |writer| {
636
7
                let range = range.clone();
637
7
                let client = Arc::clone(&self.client);
638
7
                let blob_path = blob_path.clone();
639
7
                async move {
640
7
                    let _permit = match fs::get_permit().await {
641
7
                        Ok(permit) => permit,
642
0
                        Err(e) => {
643
0
                            return Some((
644
0
                                RetryResult::Retry(make_err!(
645
0
                                    Code::Unavailable,
646
0
                                    "Failed to acquire permit: {e}"
647
0
                                )),
648
0
                                writer,
649
0
                            ));
650
                        }
651
                    };
652
653
7
                    let result: Result<(), Error> = async {
654
7
                        let options = BlobClientDownloadOptions {
655
7
                            range,
656
7
                            ..Default::default()
657
7
                        };
658
7
                        let 
response3
= client
659
7
                            .blob_client(&blob_path)
660
7
                            .download(Some(options))
661
7
                            .await
662
7
                            .map_err(|e| 
{4
663
4
                                if e.http_status() == Some(StatusCode::NotFound) {
664
1
                                    make_err!(Code::NotFound, "Blob not found in Azure: {e:?}")
665
                                } else {
666
3
                                    make_err!(
667
3
                                        Code::Aborted,
668
                                        "Failed to start download from Azure: {e:?}"
669
                                    )
670
                                }
671
4
                            })?;
672
673
3
                        let mut body = response.body;
674
6
                        while let Some(
chunk3
) = body.try_next().await.map_err(|e|
{0
675
0
                            make_err!(Code::Aborted, "Error reading from Azure stream: {e:?}")
676
0
                        })? {
677
3
                            if chunk.is_empty() {
678
0
                                continue;
679
3
                            }
680
3
                            writer.send(chunk).await.map_err(|e| 
{0
681
0
                                make_err!(Code::Aborted, "Failed to send data to writer: {e:?}")
682
0
                            })?;
683
                        }
684
685
3
                        writer.send_eof().map_err(|e| 
{0
686
0
                            make_err!(Code::Aborted, "Failed to send EOF to writer: {e:?}")
687
0
                        })?;
688
3
                        Ok(())
689
7
                    }
690
7
                    .await;
691
692
7
                    match result {
693
3
                        Ok(()) => Some((RetryResult::Ok(()), writer)),
694
4
                        Err(e) => {
695
4
                            if e.code == Code::NotFound {
696
1
                                Some((RetryResult::Err(e), writer))
697
                            } else {
698
3
                                Some((RetryResult::Retry(e), writer))
699
                            }
700
                        }
701
                    }
702
7
                }
703
7
            }))
704
            .await
705
5
    }
706
707
0
    fn inner_store(&self, _digest: Option<StoreKey>) -> &'_ dyn StoreDriver {
708
0
        self
709
0
    }
710
711
0
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
712
0
        self
713
0
    }
714
715
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
716
0
        self
717
0
    }
718
719
0
    fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) {
720
0
        registry.register_indicator(self);
721
0
    }
722
723
0
    fn register_remove_callback(self: Arc<Self>, _callback: RemoveCallback) -> Result<(), Error> {
724
        // Azure Blob Storage manages object lifecycle externally,
725
        // so we can safely ignore remove callbacks.
726
0
        Ok(())
727
0
    }
728
}
729
730
#[async_trait]
731
impl<I, NowFn> HealthStatusIndicator for AzureBlobStore<NowFn>
732
where
733
    I: InstantWrapper,
734
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
735
{
736
0
    fn get_name(&self) -> &'static str {
737
0
        "AzureBlobStore"
738
0
    }
739
740
0
    async fn check_health(&self, namespace: Cow<'static, str>) -> HealthStatus {
741
        StoreDriver::check_health(Pin::new(self), namespace).await
742
0
    }
743
}