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