/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 | | } |