1use std::collections::HashMap;
24use std::fmt::{Debug, Display, Formatter};
25use std::ops::Range;
26use std::sync::Arc;
27
28use async_trait::async_trait;
29use bytes::Bytes;
30use futures::StreamExt;
31use futures::stream::BoxStream;
32use lance_core::utils::aimd::{AimdConfig, AimdController, RequestOutcome};
33use lance_core::utils::tracing::TRACE_OBJECT_STORE_THROTTLE;
34#[cfg(test)]
35use object_store::ObjectStoreExt;
36#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
37use object_store::client::{
38 ClientOptions, HttpClient, HttpConnector, HttpError, HttpErrorKind, HttpRequest, HttpResponse,
39 HttpResponseBody, HttpService,
40};
41use object_store::path::Path;
42use object_store::{
43 CopyOptions, GetOptions, GetResult, ListResult, MultipartUpload, ObjectMeta, ObjectStore,
44 PutMultipartOptions, PutOptions, PutPayload, PutResult, RenameOptions, Result as OSResult,
45 UploadPart,
46};
47use rand::Rng;
48use tokio::sync::Mutex;
49use tracing::{debug, warn};
50
51use object_store::list::{PaginatedListOptions, PaginatedListResult, PaginatedListStore};
52
53pub fn is_throttle_error(err: &object_store::Error) -> bool {
71 if let object_store::Error::Generic { source, .. } = err {
73 let message = source.to_string();
74 let lowercase = message.to_ascii_lowercase();
75 lowercase.contains("retries, max_retries")
76 || lowercase.contains("serverbusy")
77 || lowercase.contains("server busy")
78 || lowercase.contains("egress is over the account limit")
79 || lowercase.contains("http 429")
80 || lowercase.contains("status code: 429")
81 || lowercase.contains("429 too many requests")
82 || lowercase.contains("too many requests")
83 || lowercase.contains("slowdown")
84 || lowercase.contains("please reduce your request rate")
85 || lowercase.contains("rate limit")
86 || lowercase.contains("throttling")
87 || lowercase.contains("throttled")
88 } else {
89 false
90 }
91}
92
93#[derive(Debug, Clone)]
100pub struct AimdThrottleConfig {
101 pub read: AimdConfig,
103 pub write: AimdConfig,
105 pub delete: AimdConfig,
107 pub list: AimdConfig,
109 pub burst_capacity: u32,
111 pub max_retries: usize,
113 pub min_backoff_ms: u64,
115 pub max_backoff_ms: u64,
117}
118
119impl Default for AimdThrottleConfig {
120 fn default() -> Self {
121 let aimd = AimdConfig::default();
122 Self {
123 read: aimd.clone(),
124 write: aimd.clone(),
125 delete: aimd.clone(),
126 list: aimd,
127 burst_capacity: 100,
128 max_retries: 3,
129 min_backoff_ms: 100,
130 max_backoff_ms: 300,
131 }
132 }
133}
134
135impl AimdThrottleConfig {
136 pub fn with_aimd(self, aimd: AimdConfig) -> Self {
138 Self {
139 read: aimd.clone(),
140 write: aimd.clone(),
141 delete: aimd.clone(),
142 list: aimd,
143 ..self
144 }
145 }
146
147 pub fn with_read_aimd(self, aimd: AimdConfig) -> Self {
149 Self { read: aimd, ..self }
150 }
151
152 pub fn with_write_aimd(self, aimd: AimdConfig) -> Self {
154 Self {
155 write: aimd,
156 ..self
157 }
158 }
159
160 pub fn with_delete_aimd(self, aimd: AimdConfig) -> Self {
162 Self {
163 delete: aimd,
164 ..self
165 }
166 }
167
168 pub fn with_list_aimd(self, aimd: AimdConfig) -> Self {
170 Self { list: aimd, ..self }
171 }
172
173 pub fn is_disabled(&self) -> bool {
175 self.max_retries == 0
176 }
177
178 pub fn with_burst_capacity(self, burst_capacity: u32) -> Self {
179 Self {
180 burst_capacity,
181 ..self
182 }
183 }
184
185 pub fn from_storage_options(
203 storage_options: Option<&HashMap<String, String>>,
204 ) -> lance_core::Result<Self> {
205 fn resolve_f64(
206 key: &str,
207 storage_options: Option<&HashMap<String, String>>,
208 default: f64,
209 ) -> lance_core::Result<f64> {
210 let env_key = key.to_ascii_uppercase();
211 if let Some(val) = storage_options.and_then(|opts| opts.get(key)) {
212 val.parse::<f64>().map_err(|_| {
213 lance_core::Error::invalid_input(format!(
214 "Invalid value for storage option '{key}': '{val}'"
215 ))
216 })
217 } else if let Ok(val) = std::env::var(&env_key) {
218 val.parse::<f64>().map_err(|_| {
219 lance_core::Error::invalid_input(format!(
220 "Invalid value for env var '{env_key}': '{val}'"
221 ))
222 })
223 } else {
224 Ok(default)
225 }
226 }
227
228 fn resolve_u32(
229 key: &str,
230 storage_options: Option<&HashMap<String, String>>,
231 default: u32,
232 ) -> lance_core::Result<u32> {
233 let env_key = key.to_ascii_uppercase();
234 if let Some(val) = storage_options.and_then(|opts| opts.get(key)) {
235 val.parse::<u32>().map_err(|_| {
236 lance_core::Error::invalid_input(format!(
237 "Invalid value for storage option '{key}': '{val}'"
238 ))
239 })
240 } else if let Ok(val) = std::env::var(&env_key) {
241 val.parse::<u32>().map_err(|_| {
242 lance_core::Error::invalid_input(format!(
243 "Invalid value for env var '{env_key}': '{val}'"
244 ))
245 })
246 } else {
247 Ok(default)
248 }
249 }
250
251 fn resolve_usize(
252 key: &str,
253 storage_options: Option<&HashMap<String, String>>,
254 default: usize,
255 ) -> lance_core::Result<usize> {
256 let env_key = key.to_ascii_uppercase();
257 if let Some(val) = storage_options.and_then(|opts| opts.get(key)) {
258 val.parse::<usize>().map_err(|_| {
259 lance_core::Error::invalid_input(format!(
260 "Invalid value for storage option '{key}': '{val}'"
261 ))
262 })
263 } else if let Ok(val) = std::env::var(&env_key) {
264 val.parse::<usize>().map_err(|_| {
265 lance_core::Error::invalid_input(format!(
266 "Invalid value for env var '{env_key}': '{val}'"
267 ))
268 })
269 } else {
270 Ok(default)
271 }
272 }
273
274 fn resolve_u64(
275 key: &str,
276 storage_options: Option<&HashMap<String, String>>,
277 default: u64,
278 ) -> lance_core::Result<u64> {
279 let env_key = key.to_ascii_uppercase();
280 if let Some(val) = storage_options.and_then(|opts| opts.get(key)) {
281 val.parse::<u64>().map_err(|_| {
282 lance_core::Error::invalid_input(format!(
283 "Invalid value for storage option '{key}': '{val}'"
284 ))
285 })
286 } else if let Ok(val) = std::env::var(&env_key) {
287 val.parse::<u64>().map_err(|_| {
288 lance_core::Error::invalid_input(format!(
289 "Invalid value for env var '{env_key}': '{val}'"
290 ))
291 })
292 } else {
293 Ok(default)
294 }
295 }
296
297 let initial_rate = resolve_f64("lance_aimd_initial_rate", storage_options, 2000.0)?;
298 let min_rate = resolve_f64("lance_aimd_min_rate", storage_options, 1.0)?;
299 let max_rate = resolve_f64("lance_aimd_max_rate", storage_options, 5000.0)?;
300 let decrease_factor = resolve_f64("lance_aimd_decrease_factor", storage_options, 0.5)?;
301 let additive_increment =
302 resolve_f64("lance_aimd_additive_increment", storage_options, 300.0)?;
303 let burst_capacity = resolve_u32("lance_aimd_burst_capacity", storage_options, 100)?;
304 let max_retries = resolve_usize("lance_aimd_max_retries", storage_options, 3)?;
305 let min_backoff_ms = resolve_u64("lance_aimd_min_backoff_ms", storage_options, 100)?;
306 let max_backoff_ms = resolve_u64("lance_aimd_max_backoff_ms", storage_options, 300)?;
307
308 let aimd = AimdConfig::default()
309 .with_initial_rate(initial_rate)
310 .with_min_rate(min_rate)
311 .with_max_rate(max_rate)
312 .with_decrease_factor(decrease_factor)
313 .with_additive_increment(additive_increment);
314
315 Ok(Self {
316 max_retries,
317 min_backoff_ms,
318 max_backoff_ms,
319 ..Self::default()
320 .with_aimd(aimd)
321 .with_burst_capacity(burst_capacity)
322 })
323 }
324}
325
326struct TokenBucketState {
327 tokens: f64,
328 last_refill: tokio::time::Instant,
329 rate: f64,
330}
331
332struct OperationThrottle {
334 controller: AimdController,
335 bucket: Mutex<TokenBucketState>,
336 burst_capacity: f64,
337 max_retries: usize,
338 min_backoff_ms: u64,
339 max_backoff_ms: u64,
340}
341
342impl OperationThrottle {
343 fn new(
344 aimd_config: AimdConfig,
345 burst_capacity: f64,
346 max_retries: usize,
347 min_backoff_ms: u64,
348 max_backoff_ms: u64,
349 ) -> lance_core::Result<Self> {
350 let initial_rate = aimd_config.initial_rate;
351 let controller = AimdController::new(aimd_config)?;
352 Ok(Self {
353 controller,
354 bucket: Mutex::new(TokenBucketState {
355 tokens: burst_capacity,
356 last_refill: tokio::time::Instant::now(),
357 rate: initial_rate,
358 }),
359 burst_capacity,
360 max_retries,
361 min_backoff_ms,
362 max_backoff_ms,
363 })
364 }
365
366 async fn acquire_token(&self) {
372 let sleep_duration = {
373 let mut bucket = self.bucket.lock().await;
374 let now = tokio::time::Instant::now();
375 let elapsed = now.duration_since(bucket.last_refill).as_secs_f64();
376 bucket.tokens = (bucket.tokens + elapsed * bucket.rate).min(self.burst_capacity);
377 bucket.last_refill = now;
378
379 bucket.tokens -= 1.0;
381
382 if bucket.tokens >= 0.0 {
383 return;
385 }
386
387 std::time::Duration::from_secs_f64(-bucket.tokens / bucket.rate)
389 };
390
391 tokio::time::sleep(sleep_duration).await;
392 }
393
394 async fn update_bucket_rate(&self, new_rate: f64) {
396 let mut bucket = self.bucket.lock().await;
397 bucket.rate = new_rate;
398 }
399
400 fn observe_outcome<T>(&self, result: &OSResult<T>) {
405 let outcome = match result {
406 Ok(_) => RequestOutcome::Success,
407 Err(err) if is_throttle_error(err) => {
408 debug!(
409 target: TRACE_OBJECT_STORE_THROTTLE,
410 error = %err,
411 "Throttle error detected in stream"
412 );
413 RequestOutcome::Throttled
414 }
415 Err(_) => RequestOutcome::Success,
416 };
417 let error = result
418 .as_ref()
419 .err()
420 .map(|error| error as &dyn std::fmt::Display);
421 let new_rate = self.record_outcome(outcome, error);
422 if let Ok(mut bucket) = self.bucket.try_lock() {
423 bucket.rate = new_rate;
424 }
425 }
426
427 fn record_outcome(
428 &self,
429 outcome: RequestOutcome,
430 error: Option<&dyn std::fmt::Display>,
431 ) -> f64 {
432 let prev_rate = self.controller.current_rate();
433 let new_rate = self.controller.record_outcome(outcome);
434 if new_rate < prev_rate {
435 if let Some(error) = error {
436 warn!(
437 target: TRACE_OBJECT_STORE_THROTTLE,
438 previous_rate = format!("{prev_rate:.1}"),
439 new_rate = format!("{new_rate:.1}"),
440 error = %error,
441 "AIMD throttle: rate reduced due to throttle errors"
442 );
443 } else {
444 warn!(
445 target: TRACE_OBJECT_STORE_THROTTLE,
446 previous_rate = format!("{prev_rate:.1}"),
447 new_rate = format!("{new_rate:.1}"),
448 "AIMD throttle: rate reduced due to throttle errors"
449 );
450 }
451 }
452 new_rate
453 }
454
455 async fn throttled<T, F, Fut>(&self, f: F) -> OSResult<T>
459 where
460 F: Fn() -> Fut,
461 Fut: std::future::Future<Output = OSResult<T>>,
462 {
463 for attempt in 0..=self.max_retries {
464 self.acquire_token().await;
465 let result = f().await;
466 let outcome = match &result {
467 Ok(_) => RequestOutcome::Success,
468 Err(err) if is_throttle_error(err) => {
469 debug!(
470 target: TRACE_OBJECT_STORE_THROTTLE,
471 error = %err,
472 "Throttle error detected"
473 );
474 RequestOutcome::Throttled
475 }
476 Err(_) => RequestOutcome::Success, };
478 let error = result
479 .as_ref()
480 .err()
481 .map(|error| error as &dyn std::fmt::Display);
482 let new_rate = self.record_outcome(outcome, error);
483 self.update_bucket_rate(new_rate).await;
484
485 match &result {
486 Err(err) if is_throttle_error(err) && attempt < self.max_retries => {
487 let backoff_ms =
488 rand::rng().random_range(self.min_backoff_ms..=self.max_backoff_ms);
489 debug!(
490 target: TRACE_OBJECT_STORE_THROTTLE,
491 attempt = attempt + 1,
492 max_retries = self.max_retries,
493 backoff_ms,
494 error = %err,
495 "Retrying after throttle error"
496 );
497 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
498 continue;
499 }
500 _ => return result,
501 }
502 }
503 unreachable!()
504 }
505}
506
507impl Debug for OperationThrottle {
508 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
509 f.debug_struct("OperationThrottle")
510 .field("controller", &self.controller)
511 .field("burst_capacity", &self.burst_capacity)
512 .finish()
513 }
514}
515
516#[derive(Clone)]
517pub(crate) struct AimdThrottleState {
518 read: Arc<OperationThrottle>,
519 write: Arc<OperationThrottle>,
520 delete: Arc<OperationThrottle>,
521 list: Arc<OperationThrottle>,
522}
523
524impl AimdThrottleState {
525 pub(crate) fn new(config: AimdThrottleConfig) -> lance_core::Result<Self> {
526 let burst_capacity = config.burst_capacity as f64;
527 let max_retries = config.max_retries;
528 let min_backoff_ms = config.min_backoff_ms;
529 let max_backoff_ms = config.max_backoff_ms;
530 Ok(Self {
531 read: Arc::new(OperationThrottle::new(
532 config.read,
533 burst_capacity,
534 max_retries,
535 min_backoff_ms,
536 max_backoff_ms,
537 )?),
538 write: Arc::new(OperationThrottle::new(
539 config.write,
540 burst_capacity,
541 max_retries,
542 min_backoff_ms,
543 max_backoff_ms,
544 )?),
545 delete: Arc::new(OperationThrottle::new(
546 config.delete,
547 burst_capacity,
548 max_retries,
549 min_backoff_ms,
550 max_backoff_ms,
551 )?),
552 list: Arc::new(OperationThrottle::new(
553 config.list,
554 burst_capacity,
555 max_retries,
556 min_backoff_ms,
557 max_backoff_ms,
558 )?),
559 })
560 }
561}
562
563#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
564#[derive(Debug)]
565pub(crate) struct AimdMultipartUploadConnector<C> {
566 inner: C,
567 write: Option<Arc<OperationThrottle>>,
568}
569
570#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
571impl<C> AimdMultipartUploadConnector<C> {
572 fn new(inner: C, state: Option<&AimdThrottleState>) -> Self {
573 Self {
574 inner,
575 write: state.map(|state| Arc::clone(&state.write)),
576 }
577 }
578}
579
580#[cfg(all(
581 any(feature = "aws", feature = "azure", feature = "gcp"),
582 feature = "metrics"
583))]
584pub(crate) fn cloud_http_connector(
585 state: Option<&AimdThrottleState>,
586 metrics_base: String,
587) -> AimdMultipartUploadConnector<crate::object_store::metrics::MeteringHttpConnector> {
588 AimdMultipartUploadConnector::new(
589 crate::object_store::metrics::MeteringHttpConnector::new(metrics_base),
590 state,
591 )
592}
593
594#[cfg(all(
595 any(feature = "aws", feature = "azure", feature = "gcp"),
596 not(feature = "metrics")
597))]
598pub(crate) fn cloud_http_connector(
599 state: Option<&AimdThrottleState>,
600 _metrics_base: String,
601) -> AimdMultipartUploadConnector<object_store::client::ReqwestConnector> {
602 AimdMultipartUploadConnector::new(object_store::client::ReqwestConnector::default(), state)
603}
604
605#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
606impl<C: HttpConnector> HttpConnector for AimdMultipartUploadConnector<C> {
607 fn connect(&self, options: &ClientOptions) -> object_store::Result<HttpClient> {
608 Ok(HttpClient::new(AimdMultipartUploadService {
609 inner: self.inner.connect(options)?,
610 write: self.write.clone(),
611 }))
612 }
613}
614
615#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
616#[derive(Debug)]
617struct AimdMultipartUploadService {
618 inner: HttpClient,
619 write: Option<Arc<OperationThrottle>>,
620}
621
622#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
623fn is_multipart_part_request(request: &HttpRequest) -> bool {
624 if request.method() != ::http::Method::PUT {
625 return false;
626 }
627 request.uri().query().is_some_and(|query| {
628 url::form_urlencoded::parse(query.as_bytes()).any(|(key, value)| {
629 key.eq_ignore_ascii_case("partNumber")
630 || (key.eq_ignore_ascii_case("comp") && value.eq_ignore_ascii_case("block"))
631 })
632 })
633}
634
635#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
636fn is_retryable_http_error(error: &HttpError) -> bool {
637 matches!(
638 error.kind(),
639 HttpErrorKind::Connect
640 | HttpErrorKind::Request
641 | HttpErrorKind::Timeout
642 | HttpErrorKind::Interrupted
643 )
644}
645
646#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
647#[async_trait]
648impl HttpService for AimdMultipartUploadService {
649 async fn call(&self, request: HttpRequest) -> Result<HttpResponse, HttpError> {
650 let Some(write) = self.write.as_ref() else {
651 return self.inner.execute(request).await;
652 };
653 if !is_multipart_part_request(&request) {
654 return self.inner.execute(request).await;
655 }
656
657 for attempt in 0..=write.max_retries {
658 write.acquire_token().await;
659 let mut result = self.inner.execute(request.clone()).await;
660 let mut is_retryable = result.as_ref().err().is_some_and(is_retryable_http_error);
661 let mut is_throttle = false;
662 let mut response_status = None;
663
664 if let Ok(response) = result {
665 let status = response.status();
666 response_status = Some(status);
667 is_retryable = status == ::http::StatusCode::REQUEST_TIMEOUT
668 || status == ::http::StatusCode::TOO_MANY_REQUESTS
669 || status.is_server_error();
670 is_throttle = status == ::http::StatusCode::TOO_MANY_REQUESTS
671 || status == ::http::StatusCode::SERVICE_UNAVAILABLE;
672
673 let (parts, body) = response.into_parts();
674 result = match body.bytes().await {
675 Ok(bytes) => {
676 let body = String::from_utf8_lossy(&bytes).to_ascii_lowercase();
677 let is_throttle_body = body.contains("requesttimeout")
678 || body.contains("slowdown")
679 || body.contains("serverbusy")
680 || body.contains("throttl");
681 is_retryable |= is_throttle_body;
682 is_throttle |= is_throttle_body;
683 Ok(HttpResponse::from_parts(
684 parts,
685 HttpResponseBody::from(bytes),
686 ))
687 }
688 Err(error) => {
689 is_retryable = is_retryable_http_error(&error);
690 Err(error)
691 }
692 };
693 }
694
695 let detail = response_status
696 .filter(|status| !status.is_success())
697 .map(|status| format!("HTTP status {status}"));
698 let error = result
699 .as_ref()
700 .err()
701 .map(|error| error as &dyn std::fmt::Display)
702 .or_else(|| {
703 detail
704 .as_ref()
705 .map(|detail| detail as &dyn std::fmt::Display)
706 });
707 let outcome = if is_throttle {
708 RequestOutcome::Throttled
709 } else {
710 RequestOutcome::Success
711 };
712 let new_rate = write.record_outcome(outcome, error);
713 write.update_bucket_rate(new_rate).await;
714
715 if is_retryable && attempt < write.max_retries {
716 let backoff_ms =
717 rand::rng().random_range(write.min_backoff_ms..=write.max_backoff_ms);
718 debug!(
719 target: TRACE_OBJECT_STORE_THROTTLE,
720 attempt = attempt + 1,
721 max_retries = write.max_retries,
722 backoff_ms,
723 "Retrying multipart upload part after retryable HTTP response"
724 );
725 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
726 continue;
727 }
728 return result;
729 }
730 unreachable!()
731 }
732}
733
734struct ThrottledMultipartUpload {
736 target: Box<dyn MultipartUpload>,
737 write: Arc<OperationThrottle>,
738 parts_throttled_at_http: bool,
739}
740
741impl Debug for ThrottledMultipartUpload {
742 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
743 f.debug_struct("ThrottledMultipartUpload").finish()
744 }
745}
746
747#[async_trait]
748impl MultipartUpload for ThrottledMultipartUpload {
749 fn put_part(&mut self, data: PutPayload) -> UploadPart {
750 let fut = self.target.put_part(data);
753 if self.parts_throttled_at_http {
754 return fut;
755 }
756 let write = Arc::clone(&self.write);
757 Box::pin(async move {
758 write.acquire_token().await;
759 let result = fut.await;
760 write.observe_outcome(&result);
761 result
762 })
763 }
764
765 async fn complete(&mut self) -> OSResult<PutResult> {
766 let target = &mut self.target;
767 for attempt in 0..=self.write.max_retries {
768 self.write.acquire_token().await;
769 let result = target.complete().await;
770 self.write.observe_outcome(&result);
771
772 match &result {
773 Err(err) if is_throttle_error(err) && attempt < self.write.max_retries => {
774 let backoff_ms = rand::rng()
775 .random_range(self.write.min_backoff_ms..=self.write.max_backoff_ms);
776 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
777 continue;
778 }
779 _ => return result,
780 }
781 }
782 unreachable!()
783 }
784
785 async fn abort(&mut self) -> OSResult<()> {
786 let target = &mut self.target;
787 for attempt in 0..=self.write.max_retries {
788 self.write.acquire_token().await;
789 let result = target.abort().await;
790 self.write.observe_outcome(&result);
791
792 match &result {
793 Err(err) if is_throttle_error(err) && attempt < self.write.max_retries => {
794 let backoff_ms = rand::rng()
795 .random_range(self.write.min_backoff_ms..=self.write.max_backoff_ms);
796 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
797 continue;
798 }
799 _ => return result,
800 }
801 }
802 unreachable!()
803 }
804}
805
806pub struct AimdThrottledStore {
822 target: Arc<dyn ObjectStore>,
823 read: Arc<OperationThrottle>,
824 write: Arc<OperationThrottle>,
825 delete: Arc<OperationThrottle>,
826 list: Arc<OperationThrottle>,
827 multipart_parts_throttled_at_http: bool,
828}
829
830impl Debug for AimdThrottledStore {
831 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
832 f.debug_struct("AimdThrottledStore")
833 .field("target", &self.target)
834 .field("read", &self.read)
835 .field("write", &self.write)
836 .field("delete", &self.delete)
837 .field("list", &self.list)
838 .field(
839 "multipart_parts_throttled_at_http",
840 &self.multipart_parts_throttled_at_http,
841 )
842 .finish()
843 }
844}
845
846impl Display for AimdThrottledStore {
847 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
848 write!(f, "AimdThrottledStore({})", self.target)
849 }
850}
851
852impl AimdThrottledStore {
853 pub fn new(
854 target: Arc<dyn ObjectStore>,
855 config: AimdThrottleConfig,
856 ) -> lance_core::Result<Self> {
857 Ok(Self::new_with_state(
858 target,
859 AimdThrottleState::new(config)?,
860 false,
861 ))
862 }
863
864 pub(crate) fn new_with_state(
865 target: Arc<dyn ObjectStore>,
866 state: AimdThrottleState,
867 multipart_parts_throttled_at_http: bool,
868 ) -> Self {
869 Self {
870 target,
871 read: state.read,
872 write: state.write,
873 delete: state.delete,
874 list: state.list,
875 multipart_parts_throttled_at_http,
876 }
877 }
878
879 pub fn wrap_paginated(
881 &self,
882 inner: Arc<dyn PaginatedListStore>,
883 ) -> Arc<dyn PaginatedListStore> {
884 Arc::new(ThrottledListStore {
885 inner,
886 throttle: self.list.clone(),
887 })
888 }
889}
890
891#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
893type StoreWithLister = (Arc<dyn ObjectStore>, Option<Arc<dyn PaginatedListStore>>);
894
895#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
901pub(crate) fn with_throttling(
902 state: Option<AimdThrottleState>,
903 multipart_parts_throttled_at_http: bool,
904 store: Arc<dyn ObjectStore>,
905 lister: Option<Arc<dyn PaginatedListStore>>,
906) -> StoreWithLister {
907 let Some(state) = state else {
908 return (store, lister);
909 };
910 let store = Arc::new(AimdThrottledStore::new_with_state(
911 store,
912 state,
913 multipart_parts_throttled_at_http,
914 ));
915 let lister = lister.map(|lister| store.wrap_paginated(lister));
916 (store, lister)
917}
918
919struct ThrottledListStore {
921 inner: Arc<dyn PaginatedListStore>,
922 throttle: Arc<OperationThrottle>,
923}
924
925#[async_trait]
929#[deny(clippy::missing_trait_methods)]
930impl PaginatedListStore for ThrottledListStore {
931 async fn list_paginated(
932 &self,
933 prefix: Option<&str>,
934 opts: PaginatedListOptions,
935 ) -> OSResult<PaginatedListResult> {
936 self.throttle
937 .throttled(|| self.inner.list_paginated(prefix, opts.clone()))
938 .await
939 }
940}
941
942#[async_trait]
943#[deny(clippy::missing_trait_methods)]
944impl ObjectStore for AimdThrottledStore {
945 async fn put_opts(
946 &self,
947 location: &Path,
948 bytes: PutPayload,
949 opts: PutOptions,
950 ) -> OSResult<PutResult> {
951 self.write
952 .throttled(|| self.target.put_opts(location, bytes.clone(), opts.clone()))
953 .await
954 }
955
956 async fn put_multipart_opts(
957 &self,
958 location: &Path,
959 opts: PutMultipartOptions,
960 ) -> OSResult<Box<dyn MultipartUpload>> {
961 let target = self
962 .write
963 .throttled(|| self.target.put_multipart_opts(location, opts.clone()))
964 .await?;
965 Ok(Box::new(ThrottledMultipartUpload {
966 target,
967 write: Arc::clone(&self.write),
968 parts_throttled_at_http: self.multipart_parts_throttled_at_http,
969 }))
970 }
971
972 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
973 self.read
974 .throttled(|| self.target.get_opts(location, options.clone()))
975 .await
976 }
977
978 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
979 self.read
980 .throttled(|| self.target.get_ranges(location, ranges))
981 .await
982 }
983
984 fn delete_stream(
985 &self,
986 locations: BoxStream<'static, OSResult<Path>>,
987 ) -> BoxStream<'static, OSResult<Path>> {
988 let delete = Arc::clone(&self.delete);
989 self.target
990 .delete_stream(locations)
991 .map(move |item| {
992 delete.observe_outcome(&item);
993 item
994 })
995 .boxed()
996 }
997
998 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
999 let throttle = Arc::clone(&self.list);
1000 let throttle_for_start = Arc::clone(&throttle);
1001 let target = Arc::clone(&self.target);
1002 let prefix = prefix.cloned();
1003 futures::stream::once(async move {
1004 throttle_for_start.acquire_token().await;
1005 target.list(prefix.as_ref())
1006 })
1007 .flatten()
1008 .map(move |item| {
1009 throttle.observe_outcome(&item);
1010 item
1011 })
1012 .boxed()
1013 }
1014
1015 fn list_with_offset(
1016 &self,
1017 prefix: Option<&Path>,
1018 offset: &Path,
1019 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
1020 let throttle = Arc::clone(&self.list);
1021 let throttle_for_start = Arc::clone(&throttle);
1022 let target = Arc::clone(&self.target);
1023 let prefix = prefix.cloned();
1024 let offset = offset.clone();
1025 futures::stream::once(async move {
1026 throttle_for_start.acquire_token().await;
1027 target.list_with_offset(prefix.as_ref(), &offset)
1028 })
1029 .flatten()
1030 .map(move |item| {
1031 throttle.observe_outcome(&item);
1032 item
1033 })
1034 .boxed()
1035 }
1036
1037 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
1038 self.list
1039 .throttled(|| self.target.list_with_delimiter(prefix))
1040 .await
1041 }
1042
1043 async fn copy_opts(&self, from: &Path, to: &Path, opts: CopyOptions) -> OSResult<()> {
1044 self.write
1045 .throttled(|| self.target.copy_opts(from, to, opts.clone()))
1046 .await
1047 }
1048
1049 async fn rename_opts(&self, from: &Path, to: &Path, opts: RenameOptions) -> OSResult<()> {
1050 self.write
1051 .throttled(|| self.target.rename_opts(from, to, opts.clone()))
1052 .await
1053 }
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058 use super::*;
1059 use object_store::memory::InMemory;
1060 use rstest::rstest;
1061 use std::collections::VecDeque;
1062 use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
1063
1064 const THROTTLE_ERROR_RESPONSE: &str = "request failed, after 3 retries, max_retries: 3, retry_timeout: 30s - Server returned non-2xx status code: 503: x-ms-request-id: azure-request-id";
1065
1066 fn make_generic_error(msg: &str) -> object_store::Error {
1067 object_store::Error::Generic {
1068 store: "test",
1069 source: msg.into(),
1070 }
1071 }
1072
1073 #[rstest]
1074 #[case::retry_error("Error after 10 retries, max_retries: 10, retry_timeout: 180s", true)]
1075 #[case::retries_in_message(
1076 "request failed, after 3 retries, max_retries: 5, retry_timeout: 60s",
1077 true
1078 )]
1079 #[case::not_found("Object not found", false)]
1080 #[case::permission_denied("Access denied", false)]
1081 #[case::timeout("Connection timed out", false)]
1082 #[case::http_429_without_retries("HTTP 429 Too Many Requests", true)]
1083 #[case::slowdown_without_retries("SlowDown: Please reduce your request rate", true)]
1084 #[case::azure_server_busy("Code: ServerBusy", true)]
1085 #[case::azure_egress_limit("Message: Egress is over the account limit", true)]
1086 fn test_is_throttle_error(#[case] msg: &str, #[case] expected: bool) {
1087 let err = make_generic_error(msg);
1088 assert_eq!(
1089 is_throttle_error(&err),
1090 expected,
1091 "is_throttle_error for '{}' should be {}",
1092 msg,
1093 expected
1094 );
1095 }
1096
1097 #[test]
1098 fn test_non_generic_errors_are_not_throttle() {
1099 let err = object_store::Error::NotFound {
1100 path: "test".to_string(),
1101 source: "not found".into(),
1102 };
1103 assert!(!is_throttle_error(&err));
1104 }
1105
1106 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1107 #[rstest]
1108 #[case::s3("https://bucket/object?partNumber=1&uploadId=id", true)]
1109 #[case::azure_block("https://account/object?comp=block&blockid=id", true)]
1110 #[case::azure_block_list("https://account/object?comp=blocklist", false)]
1111 #[case::ordinary_put("https://bucket/object", false)]
1112 fn test_is_multipart_part_request(#[case] uri: &str, #[case] expected: bool) {
1113 let request = ::http::Request::builder()
1114 .method(::http::Method::PUT)
1115 .uri(uri)
1116 .body(object_store::client::HttpRequestBody::empty())
1117 .unwrap();
1118 assert_eq!(is_multipart_part_request(&request), expected);
1119 }
1120
1121 #[derive(Default)]
1123 struct CountingListStore {
1124 calls: AtomicUsize,
1125 fail_with: Option<String>,
1126 }
1127
1128 #[async_trait]
1129 impl PaginatedListStore for CountingListStore {
1130 async fn list_paginated(
1131 &self,
1132 _prefix: Option<&str>,
1133 _opts: PaginatedListOptions,
1134 ) -> OSResult<PaginatedListResult> {
1135 self.calls.fetch_add(1, Ordering::SeqCst);
1136 match &self.fail_with {
1137 Some(message) => Err(make_generic_error(message)),
1138 None => Ok(PaginatedListResult {
1139 result: ListResult {
1140 common_prefixes: vec![Path::from("prefix/child")],
1141 objects: Vec::new(),
1142 extensions: Default::default(),
1143 },
1144 page_token: None,
1145 }),
1146 }
1147 }
1148 }
1149
1150 #[tokio::test(start_paused = true)]
1151 async fn test_paginated_lister_acquires_a_token_before_listing() {
1152 let lister = Arc::new(CountingListStore::default());
1153 let throttled = AimdThrottledStore::new(
1154 Arc::new(InMemory::new()) as Arc<dyn ObjectStore>,
1155 list_start_throttle_config(),
1156 )
1157 .unwrap();
1158 let throttled_lister = throttled.wrap_paginated(lister.clone());
1159
1160 let mut page = Box::pin(
1161 throttled_lister.list_paginated(Some("prefix/"), PaginatedListOptions::default()),
1162 );
1163 assert!(
1166 tokio::time::timeout(std::time::Duration::from_millis(50), &mut page)
1167 .await
1168 .is_err()
1169 );
1170 assert_eq!(lister.calls.load(Ordering::SeqCst), 0);
1171
1172 let page = tokio::time::timeout(std::time::Duration::from_millis(300), page)
1173 .await
1174 .unwrap()
1175 .unwrap();
1176 assert_eq!(page.result.common_prefixes.len(), 1);
1177 assert_eq!(lister.calls.load(Ordering::SeqCst), 1);
1178 }
1179
1180 #[tokio::test]
1181 async fn test_paginated_lister_throttle_errors_decrease_rate() {
1182 let lister = Arc::new(CountingListStore {
1183 calls: AtomicUsize::new(0),
1184 fail_with: Some(THROTTLE_ERROR_RESPONSE.to_string()),
1185 });
1186 let mut config = AimdThrottleConfig::default().with_list_aimd(
1187 AimdConfig::default()
1188 .with_initial_rate(100.0)
1189 .with_decrease_factor(0.5)
1190 .with_window_duration(std::time::Duration::from_millis(1)),
1191 );
1192 config.max_retries = 1;
1193 config.min_backoff_ms = 5;
1199 config.max_backoff_ms = 5;
1200 let throttled =
1201 AimdThrottledStore::new(Arc::new(InMemory::new()) as Arc<dyn ObjectStore>, config)
1202 .unwrap();
1203 let throttled_lister = throttled.wrap_paginated(lister.clone());
1204
1205 assert!(
1206 throttled_lister
1207 .list_paginated(Some("prefix/"), PaginatedListOptions::default())
1208 .await
1209 .is_err()
1210 );
1211
1212 assert_eq!(lister.calls.load(Ordering::SeqCst), 2);
1214 assert!(throttled.list.controller.current_rate() < 100.0);
1215 }
1216
1217 #[tokio::test]
1218 async fn test_basic_put_get_through_wrapper() {
1219 let store = Arc::new(InMemory::new());
1220 let config = AimdThrottleConfig::default();
1221 let throttled = AimdThrottledStore::new(store, config).unwrap();
1222
1223 let path = Path::from("test/file.txt");
1224 let data = PutPayload::from_static(b"hello world");
1225 throttled.put(&path, data).await.unwrap();
1226
1227 let result = throttled.get(&path).await.unwrap();
1228 let bytes = result.bytes().await.unwrap();
1229 assert_eq!(bytes.as_ref(), b"hello world");
1230 }
1231
1232 #[tokio::test]
1233 async fn test_rate_decreases_on_throttle() {
1234 let store = Arc::new(InMemory::new());
1235 let config = AimdThrottleConfig::default().with_aimd(
1236 AimdConfig::default()
1237 .with_initial_rate(100.0)
1238 .with_decrease_factor(0.5)
1239 .with_window_duration(std::time::Duration::from_millis(10)),
1240 );
1241 let throttled = AimdThrottledStore::new(store, config).unwrap();
1242
1243 let initial_rate = throttled.read.controller.current_rate();
1244 assert_eq!(initial_rate, 100.0);
1245
1246 throttled
1248 .read
1249 .controller
1250 .record_outcome(RequestOutcome::Throttled);
1251
1252 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1254 throttled
1255 .read
1256 .controller
1257 .record_outcome(RequestOutcome::Success);
1258
1259 let new_rate = throttled.read.controller.current_rate();
1260 assert!(
1261 new_rate < initial_rate,
1262 "Rate should decrease after throttle: {} < {}",
1263 new_rate,
1264 initial_rate
1265 );
1266 }
1267
1268 #[tokio::test]
1269 async fn test_rate_recovers_on_success() {
1270 let store = Arc::new(InMemory::new());
1271 let config = AimdThrottleConfig::default().with_aimd(
1272 AimdConfig::default()
1273 .with_initial_rate(100.0)
1274 .with_decrease_factor(0.5)
1275 .with_additive_increment(10.0)
1276 .with_window_duration(std::time::Duration::from_millis(10)),
1277 );
1278 let throttled = AimdThrottledStore::new(store, config).unwrap();
1279
1280 throttled
1282 .read
1283 .controller
1284 .record_outcome(RequestOutcome::Throttled);
1285 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1286 throttled
1287 .read
1288 .controller
1289 .record_outcome(RequestOutcome::Success);
1290 let decreased_rate = throttled.read.controller.current_rate();
1291 assert_eq!(decreased_rate, 50.0);
1292
1293 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1295 throttled
1296 .read
1297 .controller
1298 .record_outcome(RequestOutcome::Success);
1299 let recovered_rate = throttled.read.controller.current_rate();
1300 assert_eq!(recovered_rate, 60.0);
1301 }
1302
1303 #[tokio::test]
1304 async fn test_as_dyn_object_store() {
1305 let store: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
1306 let throttled: Arc<dyn ObjectStore> =
1307 Arc::new(AimdThrottledStore::new(store, AimdThrottleConfig::default()).unwrap());
1308
1309 let path = Path::from("test/data.bin");
1310 let data = PutPayload::from_static(b"test data");
1311 throttled.put(&path, data).await.unwrap();
1312
1313 let result = throttled.get(&path).await.unwrap();
1314 let bytes = result.bytes().await.unwrap();
1315 assert_eq!(bytes.as_ref(), b"test data");
1316 }
1317
1318 #[tokio::test]
1319 async fn test_token_bucket_delays_when_exhausted() {
1320 let store = Arc::new(InMemory::new());
1321 let config = AimdThrottleConfig::default()
1323 .with_burst_capacity(1)
1324 .with_aimd(AimdConfig::default().with_initial_rate(10.0));
1325 let throttled = Arc::new(AimdThrottledStore::new(store, config).unwrap());
1326
1327 let path = Path::from("test/file.txt");
1328 let data = PutPayload::from_static(b"data");
1329 throttled.put(&path, data).await.unwrap();
1330
1331 let start = std::time::Instant::now();
1334 let data2 = PutPayload::from_static(b"data2");
1335 throttled.put(&path, data2).await.unwrap();
1336 let elapsed = start.elapsed();
1337
1338 assert!(
1339 elapsed >= std::time::Duration::from_millis(50),
1340 "Expected delay for token refill, but elapsed was {:?}",
1341 elapsed
1342 );
1343 }
1344
1345 #[tokio::test]
1346 async fn test_list_observes_outcomes() {
1347 let store = Arc::new(InMemory::new());
1348 let config = AimdThrottleConfig::default();
1349 let throttled = AimdThrottledStore::new(store.clone(), config).unwrap();
1350
1351 let path = Path::from("prefix/file.txt");
1352 let data = PutPayload::from_static(b"data");
1353 store.put(&path, data).await.unwrap();
1354
1355 let items: Vec<_> = throttled.list(Some(&Path::from("prefix"))).collect().await;
1356 assert_eq!(items.len(), 1);
1357 assert!(items[0].is_ok());
1358 }
1359
1360 struct ThrottlingListMockStore {
1364 inner: InMemory,
1365 throttle_count: usize,
1367 }
1368
1369 impl Display for ThrottlingListMockStore {
1370 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1371 write!(f, "ThrottlingListMockStore")
1372 }
1373 }
1374
1375 impl Debug for ThrottlingListMockStore {
1376 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1377 f.debug_struct("ThrottlingListMockStore").finish()
1378 }
1379 }
1380
1381 #[async_trait]
1382 impl ObjectStore for ThrottlingListMockStore {
1383 async fn put_opts(
1384 &self,
1385 location: &Path,
1386 bytes: PutPayload,
1387 opts: PutOptions,
1388 ) -> OSResult<PutResult> {
1389 self.inner.put_opts(location, bytes, opts).await
1390 }
1391 async fn put_multipart_opts(
1392 &self,
1393 location: &Path,
1394 opts: PutMultipartOptions,
1395 ) -> OSResult<Box<dyn MultipartUpload>> {
1396 self.inner.put_multipart_opts(location, opts).await
1397 }
1398 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
1399 self.inner.get_opts(location, options).await
1400 }
1401 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
1402 self.inner.get_ranges(location, ranges).await
1403 }
1404 fn delete_stream(
1405 &self,
1406 locations: BoxStream<'static, OSResult<Path>>,
1407 ) -> BoxStream<'static, OSResult<Path>> {
1408 self.inner.delete_stream(locations)
1409 }
1410 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
1411 let n = self.throttle_count;
1412 let inner_stream = self.inner.list(prefix);
1413 let errors = futures::stream::iter((0..n).map(|_| {
1414 Err(object_store::Error::Generic {
1415 store: "ThrottlingListMock",
1416 source: "request failed, after 3 retries, max_retries: 5, retry_timeout: 60s"
1417 .into(),
1418 })
1419 }));
1420 errors.chain(inner_stream).boxed()
1421 }
1422 fn list_with_offset(
1423 &self,
1424 prefix: Option<&Path>,
1425 offset: &Path,
1426 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
1427 self.inner.list_with_offset(prefix, offset)
1428 }
1429 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
1430 self.inner.list_with_delimiter(prefix).await
1431 }
1432 async fn copy_opts(&self, from: &Path, to: &Path, opts: CopyOptions) -> OSResult<()> {
1433 self.inner.copy_opts(from, to, opts).await
1434 }
1435 }
1436
1437 #[tokio::test]
1438 async fn test_list_stream_throttle_errors_decrease_rate() {
1439 let mock = Arc::new(ThrottlingListMockStore {
1440 inner: InMemory::new(),
1441 throttle_count: 5,
1442 });
1443
1444 mock.put(
1446 &Path::from("prefix/file.txt"),
1447 PutPayload::from_static(b"data"),
1448 )
1449 .await
1450 .unwrap();
1451
1452 let config = AimdThrottleConfig::default().with_list_aimd(
1453 AimdConfig::default()
1454 .with_initial_rate(100.0)
1455 .with_decrease_factor(0.5)
1456 .with_window_duration(std::time::Duration::from_millis(10)),
1457 );
1458 let throttled = AimdThrottledStore::new(mock as Arc<dyn ObjectStore>, config).unwrap();
1459
1460 let initial_rate = throttled.list.controller.current_rate();
1461 assert_eq!(initial_rate, 100.0);
1462
1463 let items: Vec<_> = throttled.list(Some(&Path::from("prefix"))).collect().await;
1464
1465 assert_eq!(items.len(), 6);
1467 assert!(items[0].is_err());
1468 assert!(items[5].is_ok());
1469
1470 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1472 throttled
1473 .list
1474 .controller
1475 .record_outcome(RequestOutcome::Success);
1476
1477 let new_rate = throttled.list.controller.current_rate();
1478 assert!(
1479 new_rate < initial_rate,
1480 "List rate should decrease after stream throttle errors: {} < {}",
1481 new_rate,
1482 initial_rate
1483 );
1484 }
1485
1486 struct CountingListStartStore {
1487 inner: InMemory,
1488 list_calls: AtomicUsize,
1489 offset_calls: AtomicUsize,
1490 }
1491
1492 impl Default for CountingListStartStore {
1493 fn default() -> Self {
1494 Self {
1495 inner: InMemory::new(),
1496 list_calls: AtomicUsize::new(0),
1497 offset_calls: AtomicUsize::new(0),
1498 }
1499 }
1500 }
1501
1502 impl CountingListStartStore {
1503 fn list_calls(&self) -> usize {
1504 self.list_calls.load(Ordering::SeqCst)
1505 }
1506
1507 fn offset_calls(&self) -> usize {
1508 self.offset_calls.load(Ordering::SeqCst)
1509 }
1510 }
1511
1512 impl Display for CountingListStartStore {
1513 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1514 write!(f, "CountingListStartStore")
1515 }
1516 }
1517
1518 impl Debug for CountingListStartStore {
1519 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1520 f.debug_struct("CountingListStartStore").finish()
1521 }
1522 }
1523
1524 #[async_trait]
1525 impl ObjectStore for CountingListStartStore {
1526 async fn put_opts(
1527 &self,
1528 location: &Path,
1529 bytes: PutPayload,
1530 opts: PutOptions,
1531 ) -> OSResult<PutResult> {
1532 self.inner.put_opts(location, bytes, opts).await
1533 }
1534
1535 async fn put_multipart_opts(
1536 &self,
1537 location: &Path,
1538 opts: PutMultipartOptions,
1539 ) -> OSResult<Box<dyn MultipartUpload>> {
1540 self.inner.put_multipart_opts(location, opts).await
1541 }
1542
1543 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
1544 self.inner.get_opts(location, options).await
1545 }
1546
1547 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
1548 self.inner.get_ranges(location, ranges).await
1549 }
1550
1551 fn delete_stream(
1552 &self,
1553 locations: BoxStream<'static, OSResult<Path>>,
1554 ) -> BoxStream<'static, OSResult<Path>> {
1555 self.inner.delete_stream(locations)
1556 }
1557
1558 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
1559 self.list_calls.fetch_add(1, Ordering::SeqCst);
1560 self.inner.list(prefix)
1561 }
1562
1563 fn list_with_offset(
1564 &self,
1565 prefix: Option<&Path>,
1566 offset: &Path,
1567 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
1568 self.offset_calls.fetch_add(1, Ordering::SeqCst);
1569 self.inner.list_with_offset(prefix, offset)
1570 }
1571
1572 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
1573 self.inner.list_with_delimiter(prefix).await
1574 }
1575
1576 async fn copy_opts(&self, from: &Path, to: &Path, opts: CopyOptions) -> OSResult<()> {
1577 self.inner.copy_opts(from, to, opts).await
1578 }
1579 }
1580
1581 fn list_start_throttle_config() -> AimdThrottleConfig {
1582 AimdThrottleConfig::default()
1586 .with_burst_capacity(0)
1587 .with_list_aimd(AimdConfig::default().with_initial_rate(10.0))
1588 }
1589
1590 #[tokio::test(start_paused = true)]
1591 async fn test_list_acquires_token_before_starting_underlying_stream() {
1592 let store = Arc::new(CountingListStartStore::default());
1593 store
1594 .put(
1595 &Path::from("prefix/file.txt"),
1596 PutPayload::from_static(b"data"),
1597 )
1598 .await
1599 .unwrap();
1600 let throttled = AimdThrottledStore::new(
1601 store.clone() as Arc<dyn ObjectStore>,
1602 list_start_throttle_config(),
1603 )
1604 .unwrap();
1605
1606 let mut stream = throttled.list(Some(&Path::from("prefix")));
1607 assert_eq!(store.list_calls(), 0);
1608 assert!(
1611 tokio::time::timeout(std::time::Duration::from_millis(50), stream.next())
1612 .await
1613 .is_err()
1614 );
1615 assert_eq!(store.list_calls(), 0);
1616
1617 let item = tokio::time::timeout(std::time::Duration::from_millis(300), stream.next())
1618 .await
1619 .unwrap()
1620 .unwrap()
1621 .unwrap();
1622 assert_eq!(item.location, Path::from("prefix/file.txt"));
1623 assert_eq!(store.list_calls(), 1);
1624 }
1625
1626 #[tokio::test(start_paused = true)]
1627 async fn test_list_with_offset_acquires_token_before_starting_underlying_stream() {
1628 let store = Arc::new(CountingListStartStore::default());
1629 store
1630 .put(&Path::from("prefix/b"), PutPayload::from_static(b"data"))
1631 .await
1632 .unwrap();
1633 let throttled = AimdThrottledStore::new(
1634 store.clone() as Arc<dyn ObjectStore>,
1635 list_start_throttle_config(),
1636 )
1637 .unwrap();
1638
1639 let mut stream =
1640 throttled.list_with_offset(Some(&Path::from("prefix")), &Path::from("prefix/a"));
1641 assert_eq!(store.offset_calls(), 0);
1642 assert!(
1645 tokio::time::timeout(std::time::Duration::from_millis(50), stream.next())
1646 .await
1647 .is_err()
1648 );
1649 assert_eq!(store.offset_calls(), 0);
1650
1651 let item = tokio::time::timeout(std::time::Duration::from_millis(300), stream.next())
1652 .await
1653 .unwrap()
1654 .unwrap()
1655 .unwrap();
1656 assert_eq!(item.location, Path::from("prefix/b"));
1657 assert_eq!(store.offset_calls(), 1);
1658 }
1659
1660 #[tokio::test]
1661 async fn test_per_category_independence() {
1662 let store = Arc::new(InMemory::new());
1663 let config = AimdThrottleConfig::default().with_aimd(
1664 AimdConfig::default()
1665 .with_initial_rate(100.0)
1666 .with_decrease_factor(0.5)
1667 .with_window_duration(std::time::Duration::from_millis(10)),
1668 );
1669 let throttled = AimdThrottledStore::new(store, config).unwrap();
1670
1671 throttled
1673 .read
1674 .controller
1675 .record_outcome(RequestOutcome::Throttled);
1676 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1677 throttled
1678 .read
1679 .controller
1680 .record_outcome(RequestOutcome::Success);
1681
1682 let read_rate = throttled.read.controller.current_rate();
1683 let write_rate = throttled.write.controller.current_rate();
1684 let delete_rate = throttled.delete.controller.current_rate();
1685 let list_rate = throttled.list.controller.current_rate();
1686
1687 assert_eq!(read_rate, 50.0, "Read rate should have decreased");
1688 assert_eq!(write_rate, 100.0, "Write rate should be unaffected");
1689 assert_eq!(delete_rate, 100.0, "Delete rate should be unaffected");
1690 assert_eq!(list_rate, 100.0, "List rate should be unaffected");
1691 }
1692
1693 #[tokio::test]
1694 async fn test_per_category_config() {
1695 let store = Arc::new(InMemory::new());
1696 let config = AimdThrottleConfig::default()
1697 .with_read_aimd(AimdConfig::default().with_initial_rate(200.0))
1698 .with_write_aimd(AimdConfig::default().with_initial_rate(100.0))
1699 .with_delete_aimd(AimdConfig::default().with_initial_rate(50.0))
1700 .with_list_aimd(AimdConfig::default().with_initial_rate(25.0));
1701 let throttled = AimdThrottledStore::new(store, config).unwrap();
1702
1703 assert_eq!(throttled.read.controller.current_rate(), 200.0);
1704 assert_eq!(throttled.write.controller.current_rate(), 100.0);
1705 assert_eq!(throttled.delete.controller.current_rate(), 50.0);
1706 assert_eq!(throttled.list.controller.current_rate(), 25.0);
1707 }
1708
1709 struct RateLimitingMockStore {
1713 inner: InMemory,
1714 timestamps: std::sync::Mutex<VecDeque<std::time::Instant>>,
1716 max_per_window: usize,
1718 window: std::time::Duration,
1720 success_count: AtomicU64,
1721 throttle_count: AtomicU64,
1722 }
1723
1724 impl RateLimitingMockStore {
1725 fn new(max_per_window: usize, window: std::time::Duration) -> Self {
1726 Self {
1727 inner: InMemory::new(),
1728 timestamps: std::sync::Mutex::new(VecDeque::new()),
1729 max_per_window,
1730 window,
1731 success_count: AtomicU64::new(0),
1732 throttle_count: AtomicU64::new(0),
1733 }
1734 }
1735
1736 fn check_rate(&self) -> bool {
1738 let mut ts = self.timestamps.lock().unwrap();
1739 let now = std::time::Instant::now();
1740 while let Some(&front) = ts.front() {
1741 if now.duration_since(front) > self.window {
1742 ts.pop_front();
1743 } else {
1744 break;
1745 }
1746 }
1747 if ts.len() >= self.max_per_window {
1748 self.throttle_count.fetch_add(1, Ordering::Relaxed);
1749 false
1750 } else {
1751 ts.push_back(now);
1752 self.success_count.fetch_add(1, Ordering::Relaxed);
1753 true
1754 }
1755 }
1756
1757 fn throttle_error() -> object_store::Error {
1758 object_store::Error::Generic {
1759 store: "RateLimitingMock",
1760 source: THROTTLE_ERROR_RESPONSE.into(),
1761 }
1762 }
1763 }
1764
1765 impl Display for RateLimitingMockStore {
1766 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1767 write!(f, "RateLimitingMockStore")
1768 }
1769 }
1770
1771 impl Debug for RateLimitingMockStore {
1772 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1773 f.debug_struct("RateLimitingMockStore").finish()
1774 }
1775 }
1776
1777 #[async_trait]
1778 impl ObjectStore for RateLimitingMockStore {
1779 async fn put_opts(
1780 &self,
1781 location: &Path,
1782 bytes: PutPayload,
1783 opts: PutOptions,
1784 ) -> OSResult<PutResult> {
1785 self.inner.put_opts(location, bytes, opts).await
1786 }
1787
1788 async fn put_multipart_opts(
1789 &self,
1790 location: &Path,
1791 opts: PutMultipartOptions,
1792 ) -> OSResult<Box<dyn MultipartUpload>> {
1793 self.inner.put_multipart_opts(location, opts).await
1794 }
1795
1796 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
1797 if self.check_rate() {
1798 self.inner.get_opts(location, options).await
1799 } else {
1800 Err(Self::throttle_error())
1801 }
1802 }
1803
1804 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
1805 if self.check_rate() {
1806 self.inner.get_ranges(location, ranges).await
1807 } else {
1808 Err(Self::throttle_error())
1809 }
1810 }
1811
1812 fn delete_stream(
1813 &self,
1814 locations: BoxStream<'static, OSResult<Path>>,
1815 ) -> BoxStream<'static, OSResult<Path>> {
1816 self.inner.delete_stream(locations)
1817 }
1818
1819 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
1820 self.inner.list(prefix)
1821 }
1822
1823 fn list_with_offset(
1824 &self,
1825 prefix: Option<&Path>,
1826 offset: &Path,
1827 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
1828 self.inner.list_with_offset(prefix, offset)
1829 }
1830
1831 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
1832 self.inner.list_with_delimiter(prefix).await
1833 }
1834
1835 async fn copy_opts(&self, from: &Path, to: &Path, opts: CopyOptions) -> OSResult<()> {
1836 self.inner.copy_opts(from, to, opts).await
1837 }
1838 }
1839
1840 #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
1857 async fn test_aimd_throttle_under_concurrent_load() {
1858 let mock = Arc::new(RateLimitingMockStore::new(
1859 30,
1860 std::time::Duration::from_millis(100),
1861 ));
1862
1863 let path = Path::from("test/data.bin");
1865 mock.put(&path, PutPayload::from_static(b"test data"))
1866 .await
1867 .unwrap();
1868
1869 let aimd = AimdConfig::default()
1870 .with_initial_rate(100.0)
1871 .with_decrease_factor(0.5)
1872 .with_additive_increment(2.0)
1873 .with_window_duration(std::time::Duration::from_millis(100));
1874 let throttle_config = AimdThrottleConfig::default()
1875 .with_aimd(aimd)
1876 .with_burst_capacity(100);
1877
1878 let num_readers = 5;
1879 let test_duration = std::time::Duration::from_secs(2);
1880 let mut handles = Vec::new();
1881
1882 for _ in 0..num_readers {
1883 let store = Arc::new(
1884 AimdThrottledStore::new(
1885 mock.clone() as Arc<dyn ObjectStore>,
1886 throttle_config.clone(),
1887 )
1888 .unwrap(),
1889 );
1890 let p = path.clone();
1891 handles.push(tokio::spawn(async move {
1892 let deadline = std::time::Instant::now() + test_duration;
1893 let mut count = 0u64;
1894 while std::time::Instant::now() < deadline {
1895 let _ = store.head(&p).await;
1896 count += 1;
1897 }
1898 count
1899 }));
1900 }
1901
1902 let mut total_reader_requests = 0u64;
1903 for handle in handles {
1904 total_reader_requests += handle.await.unwrap();
1905 }
1906
1907 let successes = mock.success_count.load(Ordering::Relaxed);
1908 let throttled = mock.throttle_count.load(Ordering::Relaxed);
1909 let total_mock = successes + throttled;
1910
1911 assert!(
1914 total_mock >= total_reader_requests,
1915 "Mock-side count ({total_mock}) should be >= reader-side count ({total_reader_requests})"
1916 );
1917
1918 assert!(
1921 successes >= 300,
1922 "Expected >= 300 successful requests over 2s, got {successes}"
1923 );
1924 assert!(
1925 successes <= 900,
1926 "Expected <= 900 successful requests, got {successes}"
1927 );
1928
1929 assert!(throttled > 0, "Expected some throttled requests but got 0");
1931
1932 assert!(
1935 total_mock <= 5000,
1936 "AIMD should limit total requests, got {total_mock}"
1937 );
1938 }
1939
1940 struct RetryTestMockStore {
1944 inner: InMemory,
1945 errors_remaining: std::sync::Mutex<usize>,
1947 get_call_count: AtomicU64,
1949 }
1950
1951 impl RetryTestMockStore {
1952 fn new(errors_before_success: usize) -> Self {
1953 Self {
1954 inner: InMemory::new(),
1955 errors_remaining: std::sync::Mutex::new(errors_before_success),
1956 get_call_count: AtomicU64::new(0),
1957 }
1958 }
1959 }
1960
1961 impl Display for RetryTestMockStore {
1962 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1963 write!(f, "RetryTestMockStore")
1964 }
1965 }
1966
1967 impl Debug for RetryTestMockStore {
1968 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1969 f.debug_struct("RetryTestMockStore").finish()
1970 }
1971 }
1972
1973 #[async_trait]
1974 impl ObjectStore for RetryTestMockStore {
1975 async fn put_opts(
1976 &self,
1977 location: &Path,
1978 bytes: PutPayload,
1979 opts: PutOptions,
1980 ) -> OSResult<PutResult> {
1981 self.inner.put_opts(location, bytes, opts).await
1982 }
1983 async fn put_multipart_opts(
1984 &self,
1985 location: &Path,
1986 opts: PutMultipartOptions,
1987 ) -> OSResult<Box<dyn MultipartUpload>> {
1988 self.inner.put_multipart_opts(location, opts).await
1989 }
1990 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
1991 self.get_call_count.fetch_add(1, Ordering::Relaxed);
1992 let should_error = {
1993 let mut remaining = self.errors_remaining.lock().unwrap();
1994 if *remaining > 0 {
1995 *remaining -= 1;
1996 true
1997 } else {
1998 false
1999 }
2000 };
2001 if should_error {
2002 Err(object_store::Error::Generic {
2003 store: "RetryTestMock",
2004 source: THROTTLE_ERROR_RESPONSE.into(),
2005 })
2006 } else {
2007 self.inner.get_opts(location, options).await
2008 }
2009 }
2010 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
2011 self.inner.get_ranges(location, ranges).await
2012 }
2013 fn delete_stream(
2014 &self,
2015 locations: BoxStream<'static, OSResult<Path>>,
2016 ) -> BoxStream<'static, OSResult<Path>> {
2017 self.inner.delete_stream(locations)
2018 }
2019 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
2020 self.inner.list(prefix)
2021 }
2022 fn list_with_offset(
2023 &self,
2024 prefix: Option<&Path>,
2025 offset: &Path,
2026 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
2027 self.inner.list_with_offset(prefix, offset)
2028 }
2029 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
2030 self.inner.list_with_delimiter(prefix).await
2031 }
2032 async fn copy_opts(&self, from: &Path, to: &Path, opts: CopyOptions) -> OSResult<()> {
2033 self.inner.copy_opts(from, to, opts).await
2034 }
2035 }
2036
2037 #[tokio::test]
2038 async fn test_throttled_retries_on_throttle_error_then_succeeds() {
2039 let mock = Arc::new(RetryTestMockStore::new(2));
2041 let path = Path::from("test/retry.txt");
2042 mock.put(&path, PutPayload::from_static(b"retry data"))
2043 .await
2044 .unwrap();
2045
2046 let config = AimdThrottleConfig::default();
2047 let throttled =
2048 AimdThrottledStore::new(mock.clone() as Arc<dyn ObjectStore>, config).unwrap();
2049
2050 let result = throttled.get(&path).await;
2051 assert!(result.is_ok(), "Expected success after retries");
2052
2053 let bytes = result.unwrap().bytes().await.unwrap();
2054 assert_eq!(bytes.as_ref(), b"retry data");
2055
2056 assert_eq!(mock.get_call_count.load(Ordering::Relaxed), 3);
2058 }
2059
2060 #[tokio::test]
2061 async fn test_throttled_fails_after_max_retries_exceeded() {
2062 let mock = Arc::new(RetryTestMockStore::new(10));
2065 let path = Path::from("test/fail.txt");
2066 mock.put(&path, PutPayload::from_static(b"fail data"))
2067 .await
2068 .unwrap();
2069
2070 let config = AimdThrottleConfig::default();
2071 let throttled =
2072 AimdThrottledStore::new(mock.clone() as Arc<dyn ObjectStore>, config).unwrap();
2073
2074 let result = throttled.get(&path).await;
2075 assert!(result.is_err(), "Expected error after max retries");
2076 let err = result.unwrap_err();
2077 assert!(is_throttle_error(&err));
2078
2079 let lance_error = lance_core::Error::from(err);
2080 let error_message = lance_error.to_string();
2081 assert!(error_message.contains("x-ms-request-id"));
2082 assert!(error_message.contains("azure-request-id"));
2083
2084 assert_eq!(mock.get_call_count.load(Ordering::Relaxed), 4);
2086 }
2087
2088 #[cfg(feature = "aws")]
2089 #[derive(Debug)]
2090 struct MultipartRetryState {
2091 failures_remaining: AtomicUsize,
2092 part_uris: std::sync::Mutex<Vec<String>>,
2093 }
2094
2095 #[cfg(feature = "aws")]
2096 #[derive(Debug)]
2097 struct MultipartRetryConnector {
2098 state: Arc<MultipartRetryState>,
2099 }
2100
2101 #[cfg(feature = "aws")]
2102 impl HttpConnector for MultipartRetryConnector {
2103 fn connect(&self, _options: &ClientOptions) -> object_store::Result<HttpClient> {
2104 Ok(HttpClient::new(MultipartRetryService {
2105 state: Arc::clone(&self.state),
2106 }))
2107 }
2108 }
2109
2110 #[cfg(feature = "aws")]
2111 #[derive(Debug)]
2112 struct MultipartRetryService {
2113 state: Arc<MultipartRetryState>,
2114 }
2115
2116 #[cfg(feature = "aws")]
2117 #[async_trait]
2118 impl HttpService for MultipartRetryService {
2119 async fn call(&self, request: HttpRequest) -> Result<HttpResponse, HttpError> {
2120 let method = request.method().clone();
2121 let query = request.uri().query().unwrap_or_default();
2122 let (status, body, e_tag) = if method == ::http::Method::POST
2123 && query
2124 .split('&')
2125 .any(|part| part == "uploads" || part == "uploads=")
2126 {
2127 (
2128 ::http::StatusCode::OK,
2129 "<InitiateMultipartUploadResult><Bucket>bucket</Bucket><Key>object</Key><UploadId>upload-id</UploadId></InitiateMultipartUploadResult>",
2130 None,
2131 )
2132 } else if method == ::http::Method::PUT && query.contains("partNumber=") {
2133 self.state
2134 .part_uris
2135 .lock()
2136 .unwrap()
2137 .push(request.uri().to_string());
2138 let mut remaining = self.state.failures_remaining.load(Ordering::SeqCst);
2139 let should_fail = loop {
2140 let Some(next) = remaining.checked_sub(1) else {
2141 break false;
2142 };
2143 match self.state.failures_remaining.compare_exchange_weak(
2144 remaining,
2145 next,
2146 Ordering::SeqCst,
2147 Ordering::SeqCst,
2148 ) {
2149 Ok(_) => break true,
2150 Err(actual) => remaining = actual,
2151 }
2152 };
2153 if should_fail {
2154 (
2155 ::http::StatusCode::SERVICE_UNAVAILABLE,
2156 "<Error><Code>SlowDown</Code><Message>Please reduce your request rate.</Message></Error>",
2157 None,
2158 )
2159 } else {
2160 (::http::StatusCode::OK, "", Some("\"part-etag\""))
2161 }
2162 } else if method == ::http::Method::POST && query.contains("uploadId=") {
2163 (
2164 ::http::StatusCode::OK,
2165 "<CompleteMultipartUploadResult><Location>https://bucket/object</Location><Bucket>bucket</Bucket><Key>object</Key><ETag>\"object-etag\"</ETag></CompleteMultipartUploadResult>",
2166 None,
2167 )
2168 } else {
2169 (::http::StatusCode::BAD_REQUEST, "unexpected request", None)
2170 };
2171
2172 let mut response = ::http::Response::builder().status(status);
2173 if let Some(e_tag) = e_tag {
2174 response = response.header(::http::header::ETAG, e_tag);
2175 }
2176 Ok(response
2177 .body(HttpResponseBody::from(body.to_string()))
2178 .unwrap())
2179 }
2180 }
2181
2182 #[cfg(feature = "aws")]
2186 #[tokio::test(start_paused = true)]
2187 async fn test_multipart_http_retry_reuses_part_number() {
2188 use object_store::RetryConfig;
2189 use object_store::aws::AmazonS3Builder;
2190
2191 let retry_state = Arc::new(MultipartRetryState {
2192 failures_remaining: AtomicUsize::new(3),
2193 part_uris: std::sync::Mutex::new(Vec::new()),
2194 });
2195 let throttle_state = AimdThrottleState::new(AimdThrottleConfig::default()).unwrap();
2196 let connector = AimdMultipartUploadConnector::new(
2197 MultipartRetryConnector {
2198 state: Arc::clone(&retry_state),
2199 },
2200 Some(&throttle_state),
2201 );
2202 let store = AmazonS3Builder::new()
2203 .with_bucket_name("bucket")
2204 .with_region("us-east-1")
2205 .with_skip_signature(true)
2206 .with_retry(RetryConfig {
2207 max_retries: 0,
2208 ..Default::default()
2209 })
2210 .with_http_connector(connector)
2211 .build()
2212 .unwrap();
2213
2214 let mut upload = store.put_multipart(&Path::from("object")).await.unwrap();
2215 upload
2216 .put_part(PutPayload::from_static(b"payload"))
2217 .await
2218 .unwrap();
2219 upload.complete().await.unwrap();
2220
2221 let part_uris = retry_state.part_uris.lock().unwrap();
2222 assert_eq!(part_uris.len(), 4);
2223 assert!(part_uris.iter().all(|uri| uri == &part_uris[0]));
2224 assert!(part_uris[0].contains("partNumber=1"));
2225 }
2226
2227 #[tokio::test]
2228 async fn test_throttled_multipart_reorders_parts() {
2229 let store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
2230 let config = AimdThrottleConfig::default();
2231 let throttled = AimdThrottledStore::new(store.clone(), config).unwrap();
2232
2233 let path = Path::from("test/multipart_ordering.bin");
2234 let mut upload = throttled.put_multipart(&path).await.unwrap();
2235
2236 let fut_a = upload.put_part(PutPayload::from_static(b"AAAA"));
2238 let fut_b = upload.put_part(PutPayload::from_static(b"BBBB"));
2239
2240 fut_b.await.unwrap();
2243 fut_a.await.unwrap();
2244
2245 upload.complete().await.unwrap();
2246
2247 let result = store.get(&path).await.unwrap();
2248 let bytes = result.bytes().await.unwrap();
2249
2250 assert_eq!(
2251 bytes.as_ref(),
2252 b"AAAABBBB",
2253 "Parts were reordered! Got {:?} instead of AAAABBBB.",
2254 std::str::from_utf8(&bytes).unwrap_or("<non-utf8>"),
2255 );
2256 }
2257}