1use std::sync::atomic::{AtomicBool, Ordering};
18use std::sync::{Arc, Mutex};
19use std::time::Duration;
20
21use async_trait::async_trait;
22use ciborium::Value as CborValue;
23use fake::rand::rngs::StdRng;
24use fake::rand::{RngExt as _, SeedableRng as _};
25use indexmap::IndexMap;
26use tokio::time::Instant;
27use vantage_core::{Result, error};
28use vantage_types::Record;
29use vantage_vista::Vista;
30use vantage_vista::capabilities::VistaCapabilities;
31use vantage_vista::column::Column;
32use vantage_vista::reference::Reference;
33use vantage_vista::source::TableShell;
34
35#[derive(Clone, Copy, Debug)]
37pub struct Latency {
38 pub min: Duration,
39 pub max: Duration,
40}
41
42impl Latency {
43 pub fn fixed(d: Duration) -> Self {
44 Self { min: d, max: d }
45 }
46
47 pub fn between(min: Duration, max: Duration) -> Self {
48 Self {
49 min,
50 max: max.max(min),
51 }
52 }
53
54 fn draw(&self, rng: &mut StdRng) -> Duration {
55 if self.max <= self.min {
56 return self.min;
57 }
58 let spread = (self.max - self.min).as_millis() as u64;
59 self.min + Duration::from_millis(rng.random_range(0..=spread))
60 }
61}
62
63#[derive(Clone, Debug, Default)]
67pub struct LatencyModel {
68 pub list: Option<Latency>,
69 pub get: Option<Latency>,
70 pub window: Option<Latency>,
71 pub count: Option<Latency>,
72 pub search_extra: Option<Latency>,
75}
76
77#[derive(Clone, Copy, Debug)]
81pub struct Offline {
82 pub down: Duration,
83 pub period: Duration,
84}
85
86#[derive(Clone, Debug, Default)]
88pub struct FaultSchedule {
89 pub error_rate: f64,
91 pub offline: Option<Offline>,
92 pub cursor_expiry: Option<Duration>,
94 pub total_lie: i64,
96 pub boundary_skew: bool,
99}
100
101#[derive(Clone, Copy, Debug)]
106pub struct ExtraFields {
107 pub count: usize,
108 pub size: usize,
109}
110
111#[derive(Clone, Debug)]
113pub struct BackendShape {
114 pub capabilities: VistaCapabilities,
116 pub page_size: usize,
119 pub latency: LatencyModel,
120 pub faults: FaultSchedule,
121 pub extra_fields: Option<ExtraFields>,
122 pub weirdness: f64,
124 pub seed: Option<u64>,
127}
128
129impl Default for BackendShape {
130 fn default() -> Self {
133 Self {
134 capabilities: VistaCapabilities {
135 can_count: true,
136 can_insert: true,
137 can_update: true,
138 can_delete: true,
139 can_order: true,
140 can_search: true,
141 ..VistaCapabilities::default()
142 },
143 page_size: 25,
144 latency: LatencyModel::default(),
145 faults: FaultSchedule::default(),
146 extra_fields: None,
147 weirdness: 0.0,
148 seed: None,
149 }
150 }
151}
152
153#[derive(Clone, Copy)]
155enum OpClass {
156 List,
157 Get,
158 Window,
159 Count,
160}
161
162pub struct ShapedShell {
165 inner: Box<dyn TableShell>,
166 shape: Arc<BackendShape>,
167 rng: Arc<Mutex<StdRng>>,
170 epoch: Instant,
172 page_size: usize,
174 searching: Arc<AtomicBool>,
178}
179
180impl ShapedShell {
181 pub fn new(inner: Box<dyn TableShell>, shape: BackendShape) -> Self {
182 let rng = match shape.seed {
185 Some(seed) => StdRng::seed_from_u64(seed ^ 0x5AAD_F00D_u64),
186 None => crate::value_gen::entropy_rng(),
187 };
188 let page_size = shape.page_size.max(1);
189 Self {
190 inner,
191 shape: Arc::new(shape),
192 rng: Arc::new(Mutex::new(rng)),
193 epoch: Instant::now(),
194 page_size,
195 searching: Arc::new(AtomicBool::new(false)),
196 }
197 }
198
199 async fn toll(&self, class: OpClass) -> Result<()> {
206 if let Some(off) = self.shape.faults.offline {
207 let elapsed = self.epoch.elapsed();
208 let period = off.period.max(off.down);
209 let into_period =
210 Duration::from_nanos((elapsed.as_nanos() % period.as_nanos().max(1)) as u64);
211 if into_period >= period.saturating_sub(off.down) {
212 return Err(error!("shaped source is offline (scheduled outage)"));
213 }
214 }
215
216 let (delay, failed) = {
217 let rng = &mut *self.rng.lock().unwrap();
218 let band = match class {
219 OpClass::List => self.shape.latency.list,
220 OpClass::Get => self.shape.latency.get,
221 OpClass::Window => self.shape.latency.window,
222 OpClass::Count => self.shape.latency.count,
223 };
224 let mut delay = band.map(|b| b.draw(rng)).unwrap_or_default();
225 if matches!(class, OpClass::List | OpClass::Window)
226 && self.searching.load(Ordering::Relaxed)
227 && let Some(extra) = self.shape.latency.search_extra
228 {
229 delay += extra.draw(rng);
230 }
231 let failed = self.shape.faults.error_rate > 0.0
232 && rng.random_range(0.0..1.0) < self.shape.faults.error_rate;
233 (delay, failed)
234 };
235
236 if !delay.is_zero() {
237 tokio::time::sleep(delay).await;
238 }
239 if failed {
240 return Err(error!("shaped source request failed (injected fault)"));
241 }
242 Ok(())
243 }
244
245 fn skewed_offset(&self, offset: usize) -> usize {
248 if !self.shape.faults.boundary_skew || offset == 0 {
249 return offset;
250 }
251 let draw: f64 = self.rng.lock().unwrap().random_range(0.0..1.0);
252 if draw < 0.2 {
253 offset - 1 } else if draw < 0.4 {
255 offset + 1 } else {
257 offset
258 }
259 }
260
261 async fn lied_total(&self, vista: &Vista) -> Result<i64> {
263 let truth = self.inner.get_vista_count(vista).await?;
264 Ok((truth + self.shape.faults.total_lie).max(0))
265 }
266
267 fn cursor_token(&self, offset: usize) -> CborValue {
268 CborValue::Array(vec![
269 CborValue::Integer((offset as i64).into()),
270 CborValue::Integer((self.epoch.elapsed().as_millis() as i64).into()),
271 ])
272 }
273
274 fn decode_cursor(&self, token: &CborValue) -> Result<usize> {
275 let CborValue::Array(parts) = token else {
276 return Err(error!("shaped source: malformed cursor token"));
277 };
278 let (Some(CborValue::Integer(offset)), Some(CborValue::Integer(issued_ms))) =
279 (parts.first(), parts.get(1))
280 else {
281 return Err(error!("shaped source: malformed cursor token"));
282 };
283 if let Some(expiry) = self.shape.faults.cursor_expiry {
284 let issued = Duration::from_millis(i128::from(*issued_ms).max(0) as u64);
285 if self.epoch.elapsed().saturating_sub(issued) > expiry {
286 return Err(error!("shaped source: cursor token expired"));
287 }
288 }
289 Ok(i128::from(*offset).max(0) as usize)
290 }
291
292 fn gate(&self, allowed: bool, method: &str, capability: &str) -> Result<()> {
293 if allowed {
294 Ok(())
295 } else {
296 Err(self.default_error(method, capability))
297 }
298 }
299}
300
301#[async_trait]
302#[allow(clippy::ptr_arg)]
303impl TableShell for ShapedShell {
304 fn columns(&self) -> &IndexMap<String, Column> {
305 self.inner.columns()
306 }
307
308 fn references(&self) -> &IndexMap<String, Reference> {
309 self.inner.references()
310 }
311
312 fn id_column(&self) -> Option<&str> {
313 self.inner.id_column()
314 }
315
316 fn capabilities(&self) -> &VistaCapabilities {
317 &self.shape.capabilities
318 }
319
320 fn driver_name(&self) -> &'static str {
321 "faker-shaped"
322 }
323
324 async fn list_vista_values(
325 &self,
326 vista: &Vista,
327 ) -> Result<IndexMap<String, Record<CborValue>>> {
328 tracing::debug!(target: "vantage_faker::shape", op = "list", "request");
329 self.toll(OpClass::List).await?;
330 self.inner.list_vista_values(vista).await
331 }
332
333 async fn get_vista_value(
334 &self,
335 vista: &Vista,
336 id: &String,
337 ) -> Result<Option<Record<CborValue>>> {
338 tracing::debug!(target: "vantage_faker::shape", op = "get", id = %id, "request");
339 self.toll(OpClass::Get).await?;
340 self.inner.get_vista_value(vista, id).await
341 }
342
343 async fn get_vista_some_value(
344 &self,
345 vista: &Vista,
346 ) -> Result<Option<(String, Record<CborValue>)>> {
347 self.toll(OpClass::Get).await?;
348 self.inner.get_vista_some_value(vista).await
349 }
350
351 async fn fetch_window(
352 &self,
353 vista: &Vista,
354 offset: usize,
355 limit: usize,
356 ) -> Result<Vec<(String, Record<CborValue>)>> {
357 self.gate(
358 self.shape.capabilities.can_fetch_window,
359 "fetch_window",
360 "can_fetch_window",
361 )?;
362 tracing::debug!(target: "vantage_faker::shape", op = "window", offset, limit, "request");
363 self.toll(OpClass::Window).await?;
364 let offset = self.skewed_offset(offset);
365 self.inner.fetch_window(vista, offset, limit).await
366 }
367
368 async fn fetch_window_counted(
371 &self,
372 vista: &Vista,
373 offset: usize,
374 limit: usize,
375 ) -> Result<(Vec<(String, Record<CborValue>)>, Option<i64>)> {
376 let rows = self.fetch_window(vista, offset, limit).await?;
377 let total = if self.shape.capabilities.can_count {
378 Some(self.lied_total(vista).await?)
379 } else {
380 None
381 };
382 Ok((rows, total))
383 }
384
385 async fn fetch_page(
386 &self,
387 vista: &Vista,
388 page: usize,
389 ) -> Result<Vec<(String, Record<CborValue>)>> {
390 self.gate(
391 self.shape.capabilities.can_fetch_page,
392 "fetch_page",
393 "can_fetch_page",
394 )?;
395 self.toll(OpClass::Window).await?;
396 let offset = self.skewed_offset(page.saturating_sub(1) * self.page_size);
397 self.inner.fetch_window(vista, offset, self.page_size).await
398 }
399
400 async fn fetch_next(
401 &self,
402 vista: &Vista,
403 token: Option<CborValue>,
404 ) -> Result<(Vec<(String, Record<CborValue>)>, Option<CborValue>)> {
405 self.gate(
406 self.shape.capabilities.can_fetch_next,
407 "fetch_next",
408 "can_fetch_next",
409 )?;
410 self.toll(OpClass::Window).await?;
411 let offset = match &token {
412 None => 0,
413 Some(t) => self.decode_cursor(t)?,
414 };
415 let offset = self.skewed_offset(offset);
416 let rows = self
417 .inner
418 .fetch_window(vista, offset, self.page_size)
419 .await?;
420 let next = (rows.len() == self.page_size).then(|| self.cursor_token(offset + rows.len()));
421 Ok((rows, next))
422 }
423
424 async fn get_vista_count(&self, vista: &Vista) -> Result<i64> {
425 self.gate(
426 self.shape.capabilities.can_count,
427 "get_vista_count",
428 "can_count",
429 )?;
430 tracing::debug!(target: "vantage_faker::shape", op = "count", "request");
431 self.toll(OpClass::Count).await?;
432 self.lied_total(vista).await
433 }
434
435 async fn insert_vista_value(
439 &self,
440 vista: &Vista,
441 id: &String,
442 record: &Record<CborValue>,
443 ) -> Result<Record<CborValue>> {
444 self.gate(
445 self.shape.capabilities.can_insert,
446 "insert_vista_value",
447 "can_insert",
448 )?;
449 self.inner.insert_vista_value(vista, id, record).await
450 }
451
452 async fn replace_vista_value(
453 &self,
454 vista: &Vista,
455 id: &String,
456 record: &Record<CborValue>,
457 ) -> Result<Record<CborValue>> {
458 self.gate(
459 self.shape.capabilities.can_update,
460 "replace_vista_value",
461 "can_update",
462 )?;
463 self.inner.replace_vista_value(vista, id, record).await
464 }
465
466 async fn patch_vista_value(
467 &self,
468 vista: &Vista,
469 id: &String,
470 partial: &Record<CborValue>,
471 ) -> Result<Record<CborValue>> {
472 self.gate(
473 self.shape.capabilities.can_update,
474 "patch_vista_value",
475 "can_update",
476 )?;
477 self.inner.patch_vista_value(vista, id, partial).await
478 }
479
480 async fn delete_vista_value(&self, vista: &Vista, id: &String) -> Result<()> {
481 self.gate(
482 self.shape.capabilities.can_delete,
483 "delete_vista_value",
484 "can_delete",
485 )?;
486 self.inner.delete_vista_value(vista, id).await
487 }
488
489 async fn delete_vista_all_values(&self, vista: &Vista) -> Result<()> {
490 self.gate(
491 self.shape.capabilities.can_delete,
492 "delete_vista_all_values",
493 "can_delete",
494 )?;
495 self.inner.delete_vista_all_values(vista).await
496 }
497
498 async fn insert_vista_return_id_value(
499 &self,
500 vista: &Vista,
501 record: &Record<CborValue>,
502 ) -> Result<String> {
503 self.gate(
504 self.shape.capabilities.can_insert,
505 "insert_vista_return_id_value",
506 "can_insert",
507 )?;
508 self.inner.insert_vista_return_id_value(vista, record).await
509 }
510
511 fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
514 self.inner.add_eq_condition(field, value)
517 }
518
519 fn add_search(&mut self, text: &str) -> Result<()> {
520 self.gate(
521 self.shape.capabilities.can_search,
522 "add_search",
523 "can_search",
524 )?;
525 self.inner.add_search(text)?;
526 self.searching.store(true, Ordering::Relaxed);
527 Ok(())
528 }
529
530 fn clear_search(&mut self) -> Result<()> {
531 self.gate(
532 self.shape.capabilities.can_search,
533 "clear_search",
534 "can_search",
535 )?;
536 self.inner.clear_search()?;
537 self.searching.store(false, Ordering::Relaxed);
538 Ok(())
539 }
540
541 fn add_order(&mut self, field: &str, dir: vantage_vista::sort::SortDirection) -> Result<()> {
542 self.gate(self.shape.capabilities.can_order, "add_order", "can_order")?;
543 self.inner.add_order(field, dir)
544 }
545
546 fn clear_orders(&mut self) -> Result<()> {
547 self.gate(
548 self.shape.capabilities.can_order,
549 "clear_orders",
550 "can_order",
551 )?;
552 self.inner.clear_orders()
553 }
554
555 fn set_page_size(&mut self, size: usize) -> Result<()> {
556 self.gate(
557 self.shape.capabilities.can_set_page_size,
558 "set_page_size",
559 "can_set_page_size",
560 )?;
561 self.page_size = size.max(1);
562 Ok(())
563 }
564
565 fn clone_shell(&self) -> Option<Box<dyn TableShell>> {
569 let inner = self.inner.clone_shell()?;
570 Some(Box::new(Self {
571 inner,
572 shape: self.shape.clone(),
573 rng: self.rng.clone(),
574 epoch: self.epoch,
575 page_size: self.page_size,
576 searching: Arc::new(AtomicBool::new(false)),
577 }))
578 }
579}