1use alloc::sync::Arc;
25use bytes::Bytes;
26use core::future::Future;
27use core::time::Duration;
28use portable_atomic::{AtomicU64, Ordering};
29
30use crate::pipe::SendPipe;
31use crate::pipe::capabilities::Clock;
32use crate::pipe::clock::TimeClock;
33use crate::pipe::primitives::Pipe;
34use crate::pipe::when::When;
35use serde::{Deserialize, Serialize};
36
37use crate::pipe::handler::{PipeHandle, ThreadLocalPipeHandle};
38use crate::pipe::request::{Request, Response};
39use proxima_core::ProximaError;
40
41#[cfg(feature = "std")]
42use crate::pipe::handler::into_handle;
43#[cfg(feature = "std")]
44use crate::pipe::pipe_factory::PipeFactory;
45#[cfg(feature = "std")]
46use serde_json::Value;
47#[cfg(feature = "std")]
48use std::pin::Pin;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(tag = "kind", rename_all = "snake_case")]
53pub enum Dist {
54 Const { ms: u64 },
55 Range { min_ms: u64, max_ms: u64, seed: u64 },
56}
57
58impl Dist {
59 #[must_use]
60 pub fn sample(&self, call_index: u64) -> Duration {
61 match self {
62 Dist::Const { ms } => Duration::from_millis(*ms),
63 Dist::Range {
64 min_ms,
65 max_ms,
66 seed,
67 } => {
68 let (low, high) = if min_ms <= max_ms {
69 (*min_ms, *max_ms)
70 } else {
71 (*max_ms, *min_ms)
72 };
73 let mut rng = fastrand::Rng::with_seed(seed.wrapping_add(call_index));
74 Duration::from_millis(rng.u64(low..=high))
75 }
76 }
77 }
78}
79
80impl Default for Dist {
81 fn default() -> Self {
82 Dist::Const { ms: 0 }
83 }
84}
85
86pub struct Delay<Inner = PipeHandle, Clk = TimeClock> {
89 pub inner: Inner,
90 dist: Dist,
91 clock: Clk,
92 when: Option<When>,
93 calls: Arc<AtomicU64>,
94}
95
96impl<Inner> Delay<Inner, TimeClock> {
97 #[must_use]
98 pub fn new(inner: Inner, dist: Dist) -> Self {
99 Self::with_clock(inner, dist, TimeClock)
100 }
101}
102
103impl<Inner, Clk> Delay<Inner, Clk> {
104 #[must_use]
108 pub fn with_clock(inner: Inner, dist: Dist, clock: Clk) -> Self {
109 Self {
110 inner,
111 dist,
112 clock,
113 when: None,
114 calls: Arc::new(AtomicU64::new(0)),
115 }
116 }
117
118 #[must_use]
119 pub fn with_when(mut self, when: When) -> Self {
120 self.when = Some(when);
121 self
122 }
123
124 #[must_use]
125 pub fn dist(&self) -> Dist {
126 self.dist
127 }
128
129 #[must_use]
130 pub fn when(&self) -> Option<When> {
131 self.when
132 }
133
134 fn wait_for(&self, call_index: u64) -> Duration {
135 match self.when {
136 Some(gate) if !gate.fires(call_index) => Duration::ZERO,
137 _ => self.dist.sample(call_index),
138 }
139 }
140}
141
142impl<Inner, Clk> SendPipe for Delay<Inner, Clk>
143where
144 Inner: SendPipe + Clone + Send + Sync + 'static,
145 Inner::In: Send,
146 Clk: Clock + Clone + Send + Sync + 'static,
147 Clk::Delay: Send,
148{
149 type In = Inner::In;
150 type Out = Inner::Out;
151 type Err = Inner::Err;
152
153 fn call(
154 &self,
155 input: Inner::In,
156 ) -> impl Future<Output = Result<Inner::Out, Inner::Err>> + Send {
157 let call_index = self.calls.fetch_add(1, Ordering::Relaxed);
158 let wait = self.wait_for(call_index);
159 let sleep = self.clock.delay(wait);
160 let inner = self.inner.clone();
161 async move {
162 sleep.await;
163 SendPipe::call(&inner, input).await
164 }
165 }
166}
167
168impl<Clk> Pipe for Delay<ThreadLocalPipeHandle, Clk>
169where
170 ThreadLocalPipeHandle: Pipe<In = Request<Bytes>, Out = Response<Bytes>, Err = ProximaError>,
171 Clk: Clock + Clone + 'static,
172{
173 type In = Request<Bytes>;
174 type Out = Response<Bytes>;
175 type Err = ProximaError;
176
177 fn call(
178 &self,
179 input: Request<Bytes>,
180 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> {
181 let call_index = self.calls.fetch_add(1, Ordering::Relaxed);
182 let wait = self.wait_for(call_index);
183 let sleep = self.clock.delay(wait);
184 let inner = self.inner.clone();
185 async move {
186 sleep.await;
187 Pipe::call(&inner, input).await
188 }
189 }
190}
191
192#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
195pub struct DelayConfig {
196 pub dist: Dist,
197}
198
199impl DelayConfig {
200 #[must_use]
201 pub fn into_delay(self, inner: PipeHandle) -> Delay<PipeHandle, TimeClock> {
202 Delay::new(inner, self.dist)
203 }
204}
205
206impl Delay<PipeHandle, TimeClock> {
207 #[must_use]
208 pub fn to_config(&self) -> DelayConfig {
209 DelayConfig { dist: self.dist }
210 }
211
212 #[cfg(feature = "std")]
213 pub fn from_spec(inner: PipeHandle, value: &Value) -> Result<Self, ProximaError> {
214 let config: DelayConfig = serde_json::from_value(value.clone())
215 .map_err(|err| ProximaError::Config(format!("delay config: {err}")))?;
216 Ok(config.into_delay(inner))
217 }
218}
219
220#[cfg(feature = "std")]
221pub struct DelayFactory;
222
223#[cfg(feature = "std")]
224impl PipeFactory for DelayFactory {
225 fn name(&self) -> &str {
226 "delay"
227 }
228
229 fn build(
230 &self,
231 spec: &Value,
232 inner: Option<PipeHandle>,
233 ) -> Pin<Box<dyn Future<Output = Result<PipeHandle, ProximaError>> + Send + '_>> {
234 let spec = spec.clone();
235 Box::pin(async move {
236 let inner =
237 inner.ok_or_else(|| ProximaError::Config("delay requires an inner pipe".into()))?;
238 let delay = Delay::from_spec(inner, &spec)?;
239 Ok(into_handle(delay))
240 })
241 }
242}
243
244#[cfg(test)]
245#[allow(clippy::unwrap_used, clippy::expect_used)]
246mod tests {
247 use std::future::Future;
248 use std::sync::atomic::AtomicBool;
249 use std::task::{Context, Poll};
250
251 use futures::task::noop_waker;
252
253 use super::*;
254 use crate::pipe::clock::testing::MockClock;
255 use crate::pipe::handler::into_handle;
256
257 #[derive(Clone)]
258 struct RanFlag {
259 ran: Arc<AtomicBool>,
260 }
261
262 impl SendPipe for RanFlag {
263 type In = Request<Bytes>;
264 type Out = Response<Bytes>;
265 type Err = ProximaError;
266
267 fn call(
268 &self,
269 _request: Request<Bytes>,
270 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
271 let ran = self.ran.clone();
272 async move {
273 ran.store(true, Ordering::SeqCst);
274 Ok(Response::ok("ok"))
275 }
276 }
277 }
278
279 fn build_request() -> Request<Bytes> {
280 Request::builder()
281 .method("GET")
282 .path("/v1/x")
283 .body("payload")
284 .build()
285 .expect("builder")
286 }
287
288 #[test]
289 fn delay_waits_for_the_configured_duration_before_running_inner() {
290 let clock = MockClock::new();
291 let ran = Arc::new(AtomicBool::new(false));
292 let inner = RanFlag { ran: ran.clone() };
293 let delay = Delay::with_clock(inner, Dist::Const { ms: 100 }, clock.clone());
294
295 let waker = noop_waker();
296 let mut context = Context::from_waker(&waker);
297 let mut call = Box::pin(SendPipe::call(&delay, build_request()));
298
299 assert!(
300 matches!(call.as_mut().poll(&mut context), Poll::Pending),
301 "delay parks before its deadline"
302 );
303 assert!(
304 !ran.load(Ordering::SeqCst),
305 "inner pipe must not run while the delay is pending"
306 );
307
308 clock.advance(Duration::from_millis(99));
309 assert!(
310 matches!(call.as_mut().poll(&mut context), Poll::Pending),
311 "still parked one ms short of the deadline"
312 );
313 assert!(!ran.load(Ordering::SeqCst), "inner still has not run");
314
315 clock.advance(Duration::from_millis(1));
316 match call.as_mut().poll(&mut context) {
317 Poll::Ready(result) => {
318 let response = result.expect("call ok");
319 assert_eq!(response.status, 200);
320 }
321 Poll::Pending => panic!("delay must complete once the mock clock crosses the deadline"),
322 }
323 assert!(
324 ran.load(Ordering::SeqCst),
325 "inner pipe ran after the delay elapsed"
326 );
327 }
328
329 #[test]
330 fn zero_delay_runs_inner_on_first_poll() {
331 let clock = MockClock::new();
332 let ran = Arc::new(AtomicBool::new(false));
333 let inner = RanFlag { ran: ran.clone() };
334 let delay = Delay::with_clock(inner, Dist::Const { ms: 0 }, clock);
335
336 let waker = noop_waker();
337 let mut context = Context::from_waker(&waker);
338 let mut call = Box::pin(SendPipe::call(&delay, build_request()));
339
340 match call.as_mut().poll(&mut context) {
341 Poll::Ready(result) => assert_eq!(result.expect("call ok").status, 200),
342 Poll::Pending => panic!("zero delay must not park"),
343 }
344 assert!(ran.load(Ordering::SeqCst), "inner ran immediately");
345 }
346
347 #[test]
348 fn range_sample_is_deterministic_and_within_bounds() {
349 let dist = Dist::Range {
350 min_ms: 10,
351 max_ms: 50,
352 seed: 0xC0FFEE,
353 };
354 let first: Vec<Duration> = (0..256).map(|index| dist.sample(index)).collect();
355 let second: Vec<Duration> = (0..256).map(|index| dist.sample(index)).collect();
356 assert_eq!(
357 first, second,
358 "same (seed, bounds) yields an identical sample sequence"
359 );
360 for sample in first {
361 assert!(
362 sample >= Duration::from_millis(10) && sample <= Duration::from_millis(50),
363 "sample {sample:?} stays within [10ms, 50ms]"
364 );
365 }
366 }
367
368 #[test]
369 fn const_sample_ignores_the_call_index() {
370 let dist = Dist::Const { ms: 42 };
371 for index in 0..64 {
372 assert_eq!(
373 dist.sample(index),
374 Duration::from_millis(42),
375 "const delay is index-independent"
376 );
377 }
378 }
379
380 #[test]
381 fn config_builder_round_trip_parity() {
382 fn echo_pipe() -> PipeHandle {
383 struct EchoPipe;
384 impl SendPipe for EchoPipe {
385 type In = Request<Bytes>;
386 type Out = Response<Bytes>;
387 type Err = ProximaError;
388 fn call(
389 &self,
390 _request: Request<Bytes>,
391 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send
392 {
393 async move { Ok(Response::ok("ok")) }
394 }
395 }
396 into_handle(EchoPipe)
397 }
398
399 let config = DelayConfig {
400 dist: Dist::Range {
401 min_ms: 5,
402 max_ms: 25,
403 seed: 7,
404 },
405 };
406
407 let delay = config.into_delay(echo_pipe());
408 let back = delay.to_config();
409 let json = serde_json::to_value(config).expect("serialize");
410 let parsed: DelayConfig = serde_json::from_value(json.clone()).expect("deserialize");
411
412 assert_eq!(
413 back, config,
414 "builder projects back to the originating config"
415 );
416 assert_eq!(parsed, config, "serde round-trip is lossless");
417 assert_eq!(
418 json,
419 serde_json::json!({ "dist": { "kind": "range", "min_ms": 5, "max_ms": 25, "seed": 7 } }),
420 "Dist serializes as a tagged object"
421 );
422 }
423
424 #[test]
425 fn from_spec_parses_const_dist() {
426 fn echo_pipe() -> PipeHandle {
427 struct EchoPipe;
428 impl SendPipe for EchoPipe {
429 type In = Request<Bytes>;
430 type Out = Response<Bytes>;
431 type Err = ProximaError;
432 fn call(
433 &self,
434 _request: Request<Bytes>,
435 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send
436 {
437 async move { Ok(Response::ok("ok")) }
438 }
439 }
440 into_handle(EchoPipe)
441 }
442
443 let value = serde_json::json!({ "dist": { "kind": "const", "ms": 250 } });
444 let delay = Delay::from_spec(echo_pipe(), &value).expect("from spec");
445 assert_eq!(delay.dist(), Dist::Const { ms: 250 });
446 }
447
448 #[derive(Clone, PartialEq, Debug)]
449 struct CountEvent(u64);
450
451 #[derive(Clone)]
452 struct EventSink;
453
454 impl SendPipe for EventSink {
455 type In = CountEvent;
456 type Out = CountEvent;
457 type Err = ProximaError;
458
459 fn call(
460 &self,
461 input: CountEvent,
462 ) -> impl Future<Output = Result<CountEvent, ProximaError>> + Send {
463 async move { Ok(CountEvent(input.0 + 1)) }
464 }
465 }
466
467 #[test]
468 fn delay_is_generic_over_a_non_http_payload() {
469 let clock = MockClock::new();
470 let delay = Delay::with_clock(EventSink, Dist::Const { ms: 30 }, clock.clone());
471
472 let waker = noop_waker();
473 let mut context = Context::from_waker(&waker);
474 let mut call = Box::pin(SendPipe::call(&delay, CountEvent(41)));
475
476 assert!(
477 matches!(call.as_mut().poll(&mut context), Poll::Pending),
478 "event call parks before the deadline"
479 );
480 clock.advance(Duration::from_millis(30));
481 match call.as_mut().poll(&mut context) {
482 Poll::Ready(result) => assert_eq!(result.expect("event call"), CountEvent(42)),
483 Poll::Pending => panic!("event call must complete once the deadline passes"),
484 }
485 }
486
487 #[test]
488 fn gated_delay_skips_sleep_on_a_miss_and_parks_on_a_fire() {
489 let gate = When::prob(1.0).seed(1);
490 let clock = MockClock::new();
491 let ran = Arc::new(AtomicBool::new(false));
492 let delay = Delay::with_clock(
493 RanFlag { ran: ran.clone() },
494 Dist::Const { ms: 50 },
495 clock.clone(),
496 )
497 .with_when(gate);
498
499 let waker = noop_waker();
500 let mut context = Context::from_waker(&waker);
501 let mut call = Box::pin(SendPipe::call(&delay, build_request()));
502
503 assert!(
504 matches!(call.as_mut().poll(&mut context), Poll::Pending),
505 "fired gate parks the delay"
506 );
507 assert!(
508 !ran.load(Ordering::SeqCst),
509 "inner has not run while parked"
510 );
511 clock.advance(Duration::from_millis(50));
512 assert!(
513 matches!(call.as_mut().poll(&mut context), Poll::Ready(_)),
514 "delay completes after the deadline"
515 );
516 assert!(
517 ran.load(Ordering::SeqCst),
518 "inner ran after the gated delay elapsed"
519 );
520 }
521
522 #[test]
523 fn gated_delay_with_a_never_firing_gate_runs_inner_immediately() {
524 let gate = When::prob(0.0).seed(1);
525 let clock = MockClock::new();
526 let ran = Arc::new(AtomicBool::new(false));
527 let delay = Delay::with_clock(
528 RanFlag { ran: ran.clone() },
529 Dist::Const { ms: 9_999 },
530 clock,
531 )
532 .with_when(gate);
533
534 let waker = noop_waker();
535 let mut context = Context::from_waker(&waker);
536 let mut call = Box::pin(SendPipe::call(&delay, build_request()));
537
538 match call.as_mut().poll(&mut context) {
539 Poll::Ready(result) => assert_eq!(result.expect("call ok").status, 200),
540 Poll::Pending => panic!("a never-firing gate must skip the sleep"),
541 }
542 assert!(ran.load(Ordering::SeqCst), "inner ran without any wait");
543 }
544}