1use std::fmt::{Display, Formatter};
12use std::ops::Range;
13#[cfg(feature = "test-util")]
14use std::sync::atomic::AtomicU16;
15use std::sync::{Arc, Mutex};
16#[cfg(feature = "metrics")]
17use std::time::Instant;
18
19use bytes::Bytes;
20use futures::StreamExt;
21use futures::TryStreamExt;
22use futures::stream::BoxStream;
23use object_store::path::Path;
24use object_store::{
25 CopyOptions, GetOptions, GetRange, GetResult, ListResult, MultipartUpload, ObjectMeta,
26 ObjectStore, PutMultipartOptions, PutOptions, PutPayload, PutResult, RenameOptions,
27 Result as OSResult, UploadPart,
28};
29
30use crate::object_store::WrappingObjectStore;
31#[cfg(feature = "metrics")]
32use crate::object_store::metrics::{InFlightGuard, record_outcome};
33use object_store::list::PaginatedListStore;
34
35#[derive(Debug, Default, Clone)]
36pub struct IOTracker {
37 stats: Arc<Mutex<IoStats>>,
38 #[cfg(feature = "metrics")]
43 metrics_base: Option<Arc<str>>,
44}
45
46impl IOTracker {
47 pub fn incremental_stats(&self) -> IoStats {
52 std::mem::take(&mut *self.stats.lock().unwrap())
53 }
54
55 pub fn stats(&self) -> IoStats {
60 self.stats.lock().unwrap().clone()
61 }
62
63 pub fn record_read(
68 &self,
69 #[allow(unused_variables)] method: &'static str,
70 #[allow(unused_variables)] path: Path,
71 num_bytes: u64,
72 #[allow(unused_variables)] range: Option<Range<u64>>,
73 ) {
74 let mut stats = self.stats.lock().unwrap();
75 stats.read_iops += 1;
76 stats.read_bytes += num_bytes;
77 #[cfg(feature = "test-util")]
78 stats.requests.push(IoRequestRecord {
79 method,
80 path,
81 range,
82 });
83 }
84
85 pub fn record_write(
90 &self,
91 #[allow(unused_variables)] method: &'static str,
92 #[allow(unused_variables)] path: Path,
93 num_bytes: u64,
94 ) {
95 let mut stats = self.stats.lock().unwrap();
96 stats.write_iops += 1;
97 stats.written_bytes += num_bytes;
98 #[cfg(feature = "test-util")]
99 stats.requests.push(IoRequestRecord {
100 method,
101 path,
102 range: None,
103 });
104 }
105
106 #[cfg(feature = "metrics")]
113 pub(crate) fn set_metrics_base(&mut self, base: &str) {
114 self.metrics_base = Some(base.into());
115 }
116
117 #[cfg(feature = "metrics")]
125 pub fn begin_io(&self, operation: &'static str) -> IoMetricsGuard {
126 IoMetricsGuard {
127 state: self.metrics_base.as_ref().map(|base| IoMetricsState {
128 _in_flight: InFlightGuard::new(base, operation),
129 base: base.clone(),
130 operation,
131 start: Instant::now(),
132 }),
133 }
134 }
135
136 #[cfg(not(feature = "metrics"))]
138 pub fn begin_io(&self, _operation: &'static str) -> IoMetricsGuard {
139 IoMetricsGuard {}
140 }
141}
142
143#[must_use = "the operation is not recorded until `record` is called"]
150pub struct IoMetricsGuard {
151 #[cfg(feature = "metrics")]
152 state: Option<IoMetricsState>,
153}
154
155#[cfg(feature = "metrics")]
156struct IoMetricsState {
157 base: Arc<str>,
158 operation: &'static str,
159 start: Instant,
160 _in_flight: InFlightGuard,
162}
163
164impl IoMetricsGuard {
165 pub fn record<T, E>(self, result: &std::result::Result<T, E>, num_bytes: u64) {
168 #[cfg(feature = "metrics")]
169 if let Some(state) = self.state {
170 record_outcome(
171 &state.base,
172 state.operation,
173 state.start,
174 num_bytes,
175 result.is_err(),
176 );
177 }
178 #[cfg(not(feature = "metrics"))]
179 let _ = (result, num_bytes);
180 }
181}
182
183impl WrappingObjectStore for IOTracker {
184 fn wrap(&self, _store_prefix: &str, target: Arc<dyn ObjectStore>) -> Arc<dyn ObjectStore> {
185 Arc::new(IoTrackingStore::new(target, self.stats.clone()))
186 }
187
188 fn wrap_paginated(
191 &self,
192 _store_prefix: &str,
193 original: Arc<dyn PaginatedListStore>,
194 ) -> Option<Arc<dyn PaginatedListStore>> {
195 Some(original)
196 }
197}
198
199#[derive(Debug, Default, Clone)]
200pub struct IoStats {
201 pub read_iops: u64,
202 pub read_bytes: u64,
203 pub write_iops: u64,
204 pub written_bytes: u64,
205 #[cfg(feature = "test-util")]
207 pub num_stages: u64,
209 #[cfg(feature = "test-util")]
210 pub requests: Vec<IoRequestRecord>,
211}
212
213#[cfg(feature = "test-util")]
218#[macro_export]
219macro_rules! assert_io_eq {
220 ($io_stats:expr, $field:ident, $expected:expr) => {
221 assert_eq!(
222 $io_stats.$field, $expected,
223 "Expected {} to be {}, got {}. Requests: {:#?}",
224 stringify!($field),
225 $expected,
226 $io_stats.$field,
227 $io_stats.requests
228 );
229 };
230 ($io_stats:expr, $field:ident, $expected:expr, $($arg:tt)+) => {
231 assert_eq!(
232 $io_stats.$field, $expected,
233 "Expected {} to be {}, got {}. Requests: {:#?} {}",
234 stringify!($field),
235 $expected,
236 $io_stats.$field,
237 $io_stats.requests,
238 format_args!($($arg)+)
239 );
240 };
241}
242
243#[cfg(feature = "test-util")]
244#[macro_export]
245macro_rules! assert_io_gt {
246 ($io_stats:expr, $field:ident, $expected:expr) => {
247 assert!(
248 $io_stats.$field > $expected,
249 "Expected {} to be > {}, got {}. Requests: {:#?}",
250 stringify!($field),
251 $expected,
252 $io_stats.$field,
253 $io_stats.requests
254 );
255 };
256 ($io_stats:expr, $field:ident, $expected:expr, $($arg:tt)+) => {
257 assert!(
258 $io_stats.$field > $expected,
259 "Expected {} to be > {}, got {}. Requests: {:#?} {}",
260 stringify!($field),
261 $expected,
262 $io_stats.$field,
263 $io_stats.requests,
264 format_args!($($arg)+)
265 );
266 };
267}
268
269#[cfg(feature = "test-util")]
270#[macro_export]
271macro_rules! assert_io_lt {
272 ($io_stats:expr, $field:ident, $expected:expr) => {
273 assert!(
274 $io_stats.$field < $expected,
275 "Expected {} to be < {}, got {}. Requests: {:#?}",
276 stringify!($field),
277 $expected,
278 $io_stats.$field,
279 $io_stats.requests
280 );
281 };
282 ($io_stats:expr, $field:ident, $expected:expr, $($arg:tt)+) => {
283 assert!(
284 $io_stats.$field < $expected,
285 "Expected {} to be < {}, got {}. Requests: {:#?} {}",
286 stringify!($field),
287 $expected,
288 $io_stats.$field,
289 $io_stats.requests,
290 format_args!($($arg)+)
291 );
292 };
293}
294
295#[cfg(feature = "test-util")]
297#[derive(Clone)]
298pub struct IoRequestRecord {
299 pub method: &'static str,
300 pub path: Path,
301 pub range: Option<Range<u64>>,
302}
303
304#[cfg(feature = "test-util")]
305impl std::fmt::Debug for IoRequestRecord {
306 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
307 write!(
309 f,
310 "IORequest(method={}, path=\"{}\"",
311 self.method, self.path
312 )?;
313 if let Some(range) = &self.range {
314 write!(f, ", range={:?}", range)?;
315 }
316 write!(f, ")")?;
317 Ok(())
318 }
319}
320
321impl Display for IoStats {
322 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
323 write!(f, "{:#?}", self)
324 }
325}
326
327#[derive(Debug)]
328pub struct IoTrackingStore {
329 target: Arc<dyn ObjectStore>,
330 stats: Arc<Mutex<IoStats>>,
331 #[cfg(feature = "test-util")]
332 active_requests: Arc<AtomicU16>,
333}
334
335impl Display for IoTrackingStore {
336 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
337 write!(f, "{:#?}", self)
338 }
339}
340
341impl IoTrackingStore {
342 pub fn new(target: Arc<dyn ObjectStore>, stats: Arc<Mutex<IoStats>>) -> Self {
343 Self {
344 target,
345 stats,
346 #[cfg(feature = "test-util")]
347 active_requests: Arc::new(AtomicU16::new(0)),
348 }
349 }
350
351 fn record_read(
352 &self,
353 method: &'static str,
354 path: Path,
355 num_bytes: u64,
356 range: Option<Range<u64>>,
357 ) {
358 let mut stats = self.stats.lock().unwrap();
359 stats.read_iops += 1;
360 stats.read_bytes += num_bytes;
361 #[cfg(feature = "test-util")]
362 stats.requests.push(IoRequestRecord {
363 method,
364 path,
365 range,
366 });
367 #[cfg(not(feature = "test-util"))]
368 let _ = (method, path, range); }
370
371 fn record_write(&self, method: &'static str, path: Path, num_bytes: u64) {
372 let mut stats = self.stats.lock().unwrap();
373 stats.write_iops += 1;
374 stats.written_bytes += num_bytes;
375 #[cfg(feature = "test-util")]
376 stats.requests.push(IoRequestRecord {
377 method,
378 path,
379 range: None,
380 });
381 #[cfg(not(feature = "test-util"))]
382 let _ = (method, path); }
384
385 #[cfg(feature = "test-util")]
386 fn stage_guard(&self) -> StageGuard {
387 StageGuard::new(self.active_requests.clone(), self.stats.clone())
388 }
389
390 #[cfg(not(feature = "test-util"))]
391 fn stage_guard(&self) -> StageGuard {
392 StageGuard
393 }
394}
395
396#[async_trait::async_trait]
397#[deny(clippy::missing_trait_methods)]
398impl ObjectStore for IoTrackingStore {
399 async fn put_opts(
400 &self,
401 location: &Path,
402 bytes: PutPayload,
403 opts: PutOptions,
404 ) -> OSResult<PutResult> {
405 let _guard = self.stage_guard();
406 self.record_write(
407 "put_opts",
408 location.to_owned(),
409 bytes.content_length() as u64,
410 );
411 self.target.put_opts(location, bytes, opts).await
412 }
413
414 async fn put_multipart_opts(
415 &self,
416 location: &Path,
417 opts: PutMultipartOptions,
418 ) -> OSResult<Box<dyn MultipartUpload>> {
419 let _guard = self.stage_guard();
420 let target = self.target.put_multipart_opts(location, opts).await?;
421 Ok(Box::new(IoTrackingMultipartUpload {
422 target,
423 stats: self.stats.clone(),
424 #[cfg(feature = "test-util")]
425 path: location.to_owned(),
426 #[cfg(feature = "test-util")]
427 _guard,
428 }))
429 }
430
431 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
432 let _guard = self.stage_guard();
433 let is_head = options.head;
434 let range = match &options.range {
435 Some(GetRange::Bounded(range)) => Some(range.clone()),
436 _ => None, };
438 let result = self.target.get_opts(location, options).await;
439 if let Ok(result) = &result {
440 let num_bytes = if is_head {
441 0
442 } else {
443 result.range.end - result.range.start
444 };
445 self.record_read(
446 if is_head { "head" } else { "get_opts" },
447 location.to_owned(),
448 num_bytes,
449 range,
450 );
451 }
452 result
453 }
454
455 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
456 let _guard = self.stage_guard();
457 let result = self.target.get_ranges(location, ranges).await;
458 if let Ok(result) = &result {
459 self.record_read(
460 "get_ranges",
461 location.to_owned(),
462 result.iter().map(|b| b.len() as u64).sum(),
463 None,
464 );
465 }
466 result
467 }
468
469 fn delete_stream(
470 &self,
471 locations: BoxStream<'static, OSResult<Path>>,
472 ) -> BoxStream<'static, OSResult<Path>> {
473 let stats = Arc::clone(&self.stats);
474 let tracked = locations
475 .map_ok(move |path| {
476 let mut stats = stats.lock().unwrap();
477 stats.write_iops += 1;
478 #[cfg(feature = "test-util")]
479 stats.requests.push(IoRequestRecord {
480 method: "delete",
481 path: path.clone(),
482 range: None,
483 });
484 path
485 })
486 .boxed();
487 self.target.delete_stream(tracked)
488 }
489
490 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
491 let _guard = self.stage_guard();
492 self.record_read("list", prefix.cloned().unwrap_or_default(), 0, None);
493 self.target.list(prefix)
494 }
495
496 fn list_with_offset(
497 &self,
498 prefix: Option<&Path>,
499 offset: &Path,
500 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
501 self.record_read(
502 "list_with_offset",
503 prefix.cloned().unwrap_or_default(),
504 0,
505 None,
506 );
507 self.target.list_with_offset(prefix, offset)
508 }
509
510 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
511 let _guard = self.stage_guard();
512 self.record_read(
513 "list_with_delimiter",
514 prefix.cloned().unwrap_or_default(),
515 0,
516 None,
517 );
518 self.target.list_with_delimiter(prefix).await
519 }
520
521 async fn copy_opts(&self, from: &Path, to: &Path, opts: CopyOptions) -> OSResult<()> {
522 let _guard = self.stage_guard();
523 self.record_write("copy", from.to_owned(), 0);
524 self.target.copy_opts(from, to, opts).await
525 }
526
527 async fn rename_opts(&self, from: &Path, to: &Path, opts: RenameOptions) -> OSResult<()> {
528 let _guard = self.stage_guard();
529 self.record_write("rename", from.to_owned(), 0);
530 self.target.rename_opts(from, to, opts).await
531 }
532}
533
534#[derive(Debug)]
535struct IoTrackingMultipartUpload {
536 target: Box<dyn MultipartUpload>,
537 #[cfg(feature = "test-util")]
538 path: Path,
539 stats: Arc<Mutex<IoStats>>,
540 #[cfg(feature = "test-util")]
541 _guard: StageGuard,
542}
543
544#[async_trait::async_trait]
545impl MultipartUpload for IoTrackingMultipartUpload {
546 async fn abort(&mut self) -> OSResult<()> {
547 self.target.abort().await
548 }
549
550 async fn complete(&mut self) -> OSResult<PutResult> {
551 self.target.complete().await
552 }
553
554 fn put_part(&mut self, payload: PutPayload) -> UploadPart {
555 {
556 let mut stats = self.stats.lock().unwrap();
557 stats.write_iops += 1;
558 stats.written_bytes += payload.content_length() as u64;
559 #[cfg(feature = "test-util")]
560 stats.requests.push(IoRequestRecord {
561 method: "put_part",
562 path: self.path.to_owned(),
563 range: None,
564 });
565 }
566 self.target.put_part(payload)
567 }
568}
569
570#[cfg(feature = "test-util")]
571#[derive(Debug)]
572struct StageGuard {
573 active_requests: Arc<AtomicU16>,
574 stats: Arc<Mutex<IoStats>>,
575}
576
577#[cfg(not(feature = "test-util"))]
578struct StageGuard;
579
580#[cfg(feature = "test-util")]
581impl StageGuard {
582 fn new(active_requests: Arc<AtomicU16>, stats: Arc<Mutex<IoStats>>) -> Self {
583 active_requests.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
584 Self {
585 active_requests,
586 stats,
587 }
588 }
589}
590
591#[cfg(feature = "test-util")]
592impl Drop for StageGuard {
593 fn drop(&mut self) {
594 if self
595 .active_requests
596 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst)
597 == 1
598 {
599 let mut stats = self.stats.lock().unwrap();
600 stats.num_stages += 1;
601 }
602 }
603}