1use {
21 crate::{build::build_object_store, config::ObjectStoreConfig, error::ObjectStoreError},
22 bytes::Bytes,
23 futures_util::{
24 stream,
25 stream::{BoxStream, StreamExt as _},
26 },
27 object_store::{
28 CopyOptions, GetOptions, GetResult, ListResult, MultipartUpload, ObjectMeta, ObjectStore,
29 PutMultipartOptions, PutOptions, PutPayload, PutResult, memory::InMemory, path::Path,
30 },
31 std::{
32 collections::BTreeMap,
33 fmt,
34 sync::{
35 Arc,
36 atomic::{AtomicU64, Ordering},
37 },
38 time::Duration,
39 },
40 tokio::time,
41};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, krabka_macros::EnumStr)]
53#[enum_str(as_str = label, all)]
54pub enum StoreOp {
55 #[enum_str(name = "FaultInjectingStore::put")]
57 Put,
58 #[enum_str(name = "FaultInjectingStore::get")]
60 Get,
61 #[enum_str(name = "FaultInjectingStore::delete")]
63 Delete,
64 #[enum_str(name = "FaultInjectingStore::list")]
66 List,
67 #[enum_str(name = "FaultInjectingStore::copy")]
69 Copy,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum FaultKind {
75 Throttled,
79 ServerError,
81 NotFound,
84}
85
86impl FaultKind {
87 fn error(self, store: &'static str, location: &Path) -> object_store::Error {
89 match self {
90 Self::Throttled => object_store::Error::Generic {
91 store,
92 source: "503 SlowDown: Please reduce your request rate.".into(),
93 },
94 Self::ServerError => object_store::Error::Generic {
95 store,
96 source: "500 InternalError: We encountered an internal error.".into(),
97 },
98 Self::NotFound => object_store::Error::NotFound {
99 path: location.to_string(),
100 source: "injected".into(),
101 },
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct OpFault {
109 pub kind: FaultKind,
111 pub fail_per_thousand: u32,
115 pub latency: Duration,
119}
120
121impl OpFault {
122 #[must_use]
124 pub const fn always(kind: FaultKind) -> Self {
125 Self {
126 kind,
127 fail_per_thousand: 1_000,
128 latency: Duration::ZERO,
129 }
130 }
131
132 #[must_use]
134 pub const fn rate(kind: FaultKind, per_thousand: u32) -> Self {
135 Self {
136 kind,
137 fail_per_thousand: per_thousand,
138 latency: Duration::ZERO,
139 }
140 }
141
142 #[must_use]
145 pub const fn stall(latency: Duration) -> Self {
146 Self {
147 kind: FaultKind::ServerError,
148 fail_per_thousand: 0,
149 latency,
150 }
151 }
152
153 #[must_use]
155 pub const fn with_latency(mut self, latency: Duration) -> Self {
156 self.latency = latency;
157 self
158 }
159
160 const fn fails_call(self, n: u64) -> bool {
162 let rate = self.fail_per_thousand as u64;
163 ((n + 1) * rate) / 1_000 > (n * rate) / 1_000
164 }
165}
166
167#[derive(Debug, Clone, Default, PartialEq, Eq)]
171pub struct FaultPolicy {
172 faults: BTreeMap<StoreOp, OpFault>,
173}
174
175impl FaultPolicy {
176 #[must_use]
178 pub fn none() -> Self { Self::default() }
179
180 #[must_use]
182 pub fn with(mut self, op: StoreOp, fault: OpFault) -> Self {
183 self.faults.insert(op, fault);
184 self
185 }
186
187 #[must_use]
189 pub fn with_all(mut self, fault: OpFault) -> Self {
190 for op in StoreOp::ALL {
191 self.faults.insert(op, fault);
192 }
193 self
194 }
195
196 #[must_use]
198 pub fn fault(&self, op: StoreOp) -> Option<OpFault> { self.faults.get(&op).copied() }
199}
200
201#[derive(Debug, Default)]
204struct Counters {
205 put: AtomicU64,
206 get: AtomicU64,
207 delete: AtomicU64,
208 list: AtomicU64,
209 copy: AtomicU64,
210}
211
212impl Counters {
213 fn slot(&self, op: StoreOp) -> &AtomicU64 {
214 match op {
215 StoreOp::Put => &self.put,
216 StoreOp::Get => &self.get,
217 StoreOp::Delete => &self.delete,
218 StoreOp::List => &self.list,
219 StoreOp::Copy => &self.copy,
220 }
221 }
222}
223
224#[derive(Debug)]
227pub struct FaultInjectingStore {
228 inner: Arc<dyn ObjectStore>,
229 policy: FaultPolicy,
230 counters: Counters,
231 get_replacement: Option<Bytes>,
232}
233
234fn failed_stream<T: Send + 'static>(
235 kind: FaultKind,
236 operation: StoreOp,
237) -> BoxStream<'static, object_store::Result<T>> {
238 let error = kind.error(operation.label(), &listing_path());
239 stream::once(async move { Err(error) }).boxed()
240}
241
242impl FaultInjectingStore {
243 #[must_use]
246 pub fn memory() -> Self { Self::new(Arc::new(InMemory::new()), FaultPolicy::none()) }
247
248 #[must_use]
250 pub fn new(inner: Arc<dyn ObjectStore>, policy: FaultPolicy) -> Self {
251 Self {
252 inner,
253 policy,
254 counters: Counters::default(),
255 get_replacement: None,
256 }
257 }
258
259 #[must_use]
265 pub fn with_get_overwrite(mut self, replacement: Bytes) -> Self {
266 self.get_replacement = Some(replacement);
267 self
268 }
269
270 #[must_use]
273 pub fn attempts(&self, op: StoreOp) -> u64 { self.counters.slot(op).load(Ordering::SeqCst) }
274
275 async fn enter(&self, op: StoreOp, location: &Path) -> Result<(), object_store::Error> {
278 let n = self.counters.slot(op).fetch_add(1, Ordering::SeqCst);
279 let Some(fault) = self.policy.fault(op) else {
280 return Ok(());
281 };
282 if !fault.latency.is_zero() {
283 time::sleep(fault.latency).await;
284 }
285 if fault.fails_call(n) {
286 return Err(fault.kind.error(op.label(), location));
287 }
288 Ok(())
289 }
290}
291
292impl fmt::Display for FaultInjectingStore {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 write!(f, "FaultInjectingStore({})", self.inner)
295 }
296}
297
298fn listing_path() -> Path { Path::from("") }
301
302#[async_trait::async_trait]
303impl ObjectStore for FaultInjectingStore {
304 async fn put_opts(
305 &self,
306 location: &Path,
307 payload: PutPayload,
308 opts: PutOptions,
309 ) -> object_store::Result<PutResult> {
310 self.enter(StoreOp::Put, location).await?;
311 self.inner.put_opts(location, payload, opts).await
312 }
313
314 async fn put_multipart_opts(
315 &self,
316 location: &Path,
317 opts: PutMultipartOptions,
318 ) -> object_store::Result<Box<dyn MultipartUpload>> {
319 self.enter(StoreOp::Put, location).await?;
320 self.inner.put_multipart_opts(location, opts).await
321 }
322
323 async fn get_opts(
324 &self,
325 location: &Path,
326 options: GetOptions,
327 ) -> object_store::Result<GetResult> {
328 self.enter(StoreOp::Get, location).await?;
329 if !options.head
330 && let Some(replacement) = &self.get_replacement
331 {
332 self.inner
333 .put_opts(
334 location,
335 PutPayload::from_bytes(replacement.clone()),
336 PutOptions::default(),
337 )
338 .await?;
339 }
340 self.inner.get_opts(location, options).await
341 }
342
343 fn delete_stream(
344 &self,
345 locations: BoxStream<'static, object_store::Result<Path>>,
346 ) -> BoxStream<'static, object_store::Result<Path>> {
347 let n = self
348 .counters
349 .slot(StoreOp::Delete)
350 .fetch_add(1, Ordering::SeqCst);
351 match self.policy.fault(StoreOp::Delete) {
352 Some(fault) if fault.fails_call(n) => failed_stream(fault.kind, StoreOp::Delete),
355 Some(fault) if !fault.latency.is_zero() => {
356 let stream = self.inner.delete_stream(locations);
357 stream
358 .then(move |item| async move {
359 time::sleep(fault.latency).await;
360 item
361 })
362 .boxed()
363 }
364 _ => self.inner.delete_stream(locations),
365 }
366 }
367
368 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, object_store::Result<ObjectMeta>> {
369 let n = self
370 .counters
371 .slot(StoreOp::List)
372 .fetch_add(1, Ordering::SeqCst);
373 match self.policy.fault(StoreOp::List) {
374 Some(fault) if fault.fails_call(n) => failed_stream(fault.kind, StoreOp::List),
375 Some(fault) if !fault.latency.is_zero() => {
376 let stream = self.inner.list(prefix);
377 let latency = fault.latency;
378 stream::once(async move { time::sleep(latency).await })
379 .flat_map(move |()| stream::empty())
380 .chain(stream)
381 .boxed()
382 }
383 _ => self.inner.list(prefix),
384 }
385 }
386
387 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> object_store::Result<ListResult> {
388 self.enter(StoreOp::List, &listing_path()).await?;
389 self.inner.list_with_delimiter(prefix).await
390 }
391
392 async fn copy_opts(
393 &self,
394 from: &Path,
395 to: &Path,
396 options: CopyOptions,
397 ) -> object_store::Result<()> {
398 self.enter(StoreOp::Copy, from).await?;
399 self.inner.copy_opts(from, to, options).await
400 }
401}
402
403pub fn build_faulty_object_store(
414 cfg: &ObjectStoreConfig,
415 policy: FaultPolicy,
416) -> Result<Arc<FaultInjectingStore>, ObjectStoreError> {
417 Ok(Arc::new(FaultInjectingStore::new(
418 build_object_store(cfg)?,
419 policy,
420 )))
421}
422
423#[cfg(test)]
424mod tests {
425 use {
426 super::*,
427 crate::{config::ObjectStoreConfig, test_support::round_trip},
428 assert2::{assert, check},
429 object_store::{ObjectStoreExt as _, memory::InMemory},
430 std::array,
431 tokio::time::Instant,
432 };
433
434 fn memory(policy: FaultPolicy) -> Arc<FaultInjectingStore> {
435 build_faulty_object_store(&ObjectStoreConfig::InMemory, policy).unwrap()
436 }
437
438 #[tokio::test]
439 async fn get_overwrite_changes_the_body_after_head_without_changing_head_itself() {
440 let inner: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
441 let key = Path::from("mutable");
442 inner
443 .put(&key, PutPayload::from_static(b"old"))
444 .await
445 .unwrap();
446 let store = FaultInjectingStore::new(inner, FaultPolicy::none())
447 .with_get_overwrite(Bytes::from_static(b"replacement"));
448 check!(store.head(&key).await.unwrap().size == 3);
449 check!(
450 store
451 .get(&key)
452 .await
453 .unwrap()
454 .bytes()
455 .await
456 .unwrap()
457 .as_ref()
458 == b"replacement"
459 );
460 check!(store.head(&key).await.unwrap().size == 11);
461 check!(store.attempts(StoreOp::Get) == 3);
462 }
463
464 #[yare::parameterized(
468 never = { 0, [false, false, false, false] },
469 one_in_four = { 250, [false, false, false, true] },
470 every_other = { 500, [false, true, false, true] },
471 always = { 1_000, [true, true, true, true] },
472 )]
473 fn fail_rates_pick_the_same_calls_every_run(rate: u32, want: [bool; 4]) {
474 let fault = OpFault::rate(FaultKind::Throttled, rate);
475 let got: [bool; 4] = array::from_fn(|n| fault.fails_call(n as u64));
476 check!(got == want, "rate {rate}");
477 }
478
479 #[tokio::test]
483 async fn a_throttled_put_fails_and_writes_nothing() {
484 let store =
485 memory(FaultPolicy::none().with(StoreOp::Put, OpFault::always(FaultKind::Throttled)));
486 let path = Path::from("seg.log");
487
488 let err = store
489 .put(&path, PutPayload::from(b"bytes".to_vec()))
490 .await
491 .unwrap_err();
492
493 check!(err.to_string().contains("503 SlowDown"));
494 assert!(store.get(&path).await.is_err());
495 check!(store.attempts(StoreOp::Put) == 1);
496 }
497
498 #[tokio::test]
502 async fn faults_are_scoped_to_the_named_operation() {
503 let store =
504 memory(FaultPolicy::none().with(StoreOp::Get, OpFault::always(FaultKind::NotFound)));
505 let path = Path::from("seg.log");
506
507 store
508 .put(&path, PutPayload::from(b"bytes".to_vec()))
509 .await
510 .unwrap();
511 let err = store.get(&path).await.unwrap_err();
512
513 check!(matches!(err, object_store::Error::NotFound { .. }));
514 check!(store.attempts(StoreOp::Put) == 1);
515 check!(store.attempts(StoreOp::Get) == 1);
516 }
517
518 #[tokio::test(start_paused = true)]
521 async fn a_stall_succeeds_but_only_after_the_injected_latency() {
522 let store =
523 memory(FaultPolicy::none().with(StoreOp::Put, OpFault::stall(Duration::from_secs(30))));
524 let path = Path::from("seg.log");
525
526 let started = Instant::now();
527 store
528 .put(&path, PutPayload::from(b"bytes".to_vec()))
529 .await
530 .unwrap();
531
532 check!(started.elapsed() >= Duration::from_secs(30));
533 assert!(store.get(&path).await.is_ok());
534 }
535
536 #[tokio::test]
539 async fn an_empty_policy_passes_everything_through() {
540 let store = memory(FaultPolicy::none());
541 let path = Path::from("seg.log");
542
543 let got = round_trip(&store, &path, b"bytes").await;
544
545 check!(&got[..] == b"bytes");
546 check!(store.attempts(StoreOp::Put) == 1);
547 }
548
549 #[tokio::test]
552 async fn with_all_fails_listing_and_deleting_too() {
553 let store = memory(FaultPolicy::none().with_all(OpFault::always(FaultKind::ServerError)));
554
555 let listed = store.list(None).collect::<Vec<_>>().await;
556 let deleted = store.delete(&Path::from("seg.log")).await;
557
558 check!(listed.len() == 1);
559 check!(listed[0].is_err());
560 check!(deleted.is_err());
561 check!(store.attempts(StoreOp::List) == 1);
562 check!(store.attempts(StoreOp::Delete) == 1);
563 }
564}