Skip to main content

opendal_layer_tail_cut/
lib.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18#![doc = include_str!("../README.md")]
19#![cfg_attr(docsrs, feature(doc_cfg))]
20#![cfg_attr(docsrs, doc(auto_cfg))]
21#![deny(missing_docs)]
22use std::fmt::Debug;
23use std::sync::Arc;
24use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
25
26use opendal_core::raw::*;
27use opendal_core::*;
28
29/// Builder for TailCutLayer.
30///
31/// Use this to configure the layer, then call `build()` to create a layer
32/// that can be cloned and shared across multiple operators.
33///
34/// # Examples
35///
36/// ```no_run
37/// # use std::time::Duration;
38/// #
39/// # use opendal_core::services;
40/// # use opendal_core::Operator;
41/// # use opendal_core::Result;
42/// # use opendal_layer_tail_cut::TailCutLayer;
43/// #
44/// # fn main() -> Result<()> {
45/// let layer = TailCutLayer::builder()
46///     .percentile(95)
47///     .window(Duration::from_secs(60))
48///     .build();
49///
50/// let op = Operator::new(services::Memory::default())?
51///     .layer(layer);
52/// # Ok(())
53/// # }
54/// ```
55#[derive(Clone)]
56pub struct TailCutLayerBuilder {
57    percentile: u8,
58    safety_factor: f64,
59    window: Duration,
60    min_samples: usize,
61    min_deadline: Duration,
62    max_deadline: Duration,
63}
64
65impl Default for TailCutLayerBuilder {
66    fn default() -> Self {
67        Self {
68            percentile: 95,
69            safety_factor: 1.3,
70            window: Duration::from_secs(60),
71            min_samples: 200,
72            min_deadline: Duration::from_millis(500),
73            max_deadline: Duration::from_secs(30),
74        }
75    }
76}
77
78impl TailCutLayerBuilder {
79    /// Create a new [`TailCutLayerBuilder`] with default settings.
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Set the percentile threshold (e.g., 95 for P95, 99 for P99).
85    ///
86    /// Requests slower than this percentile × safety_factor will be cancelled.
87    ///
88    /// Default: 95
89    ///
90    /// # Panics
91    ///
92    /// Panics if percentile is not between 50 and 99.
93    pub fn percentile(mut self, percentile: u8) -> Self {
94        assert!(
95            (50..=99).contains(&percentile),
96            "percentile must be between 50 and 99"
97        );
98        self.percentile = percentile;
99        self
100    }
101
102    /// Set the safety factor multiplier.
103    ///
104    /// The actual deadline is calculated as: P{percentile} × safety_factor.
105    /// A higher value reduces false positives but may miss some long tails.
106    ///
107    /// Default: 1.3 (30% buffer)
108    ///
109    /// # Panics
110    ///
111    /// Panics if factor is not between 1.0 and 5.0.
112    pub fn safety_factor(mut self, factor: f64) -> Self {
113        assert!(
114            (1.0..=5.0).contains(&factor),
115            "safety_factor must be between 1.0 and 5.0"
116        );
117        self.safety_factor = factor;
118        self
119    }
120
121    /// Set the sliding window duration for statistics collection.
122    ///
123    /// Longer windows provide more stable statistics but react slower to changes.
124    /// Shorter windows adapt faster but may be more noisy.
125    ///
126    /// Default: 60 seconds
127    ///
128    /// # Panics
129    ///
130    /// Panics if window is greater than 120 seconds.
131    pub fn window(mut self, window: Duration) -> Self {
132        assert!(
133            window <= Duration::from_secs(120),
134            "window must be <= 120 seconds"
135        );
136        self.window = window;
137        self
138    }
139
140    /// Set the minimum number of samples required before enabling adaptive cancellation.
141    ///
142    /// During cold start (when sample count < min_samples), the layer will not
143    /// cancel any requests to avoid false positives.
144    ///
145    /// Default: 200
146    pub fn min_samples(mut self, min_samples: usize) -> Self {
147        self.min_samples = min_samples;
148        self
149    }
150
151    /// Set the minimum deadline (floor).
152    ///
153    /// Even if calculated deadline is shorter, it will be clamped to this value.
154    /// This prevents overly aggressive cancellation on very fast backends.
155    ///
156    /// Default: 500ms
157    pub fn min_deadline(mut self, deadline: Duration) -> Self {
158        self.min_deadline = deadline;
159        self
160    }
161
162    /// Set the maximum deadline (ceiling).
163    ///
164    /// Even if calculated deadline is longer, it will be clamped to this value.
165    /// This acts as a safety fallback timeout.
166    ///
167    /// Default: 30s
168    pub fn max_deadline(mut self, deadline: Duration) -> Self {
169        self.max_deadline = deadline;
170        self
171    }
172
173    /// Build the layer.
174    ///
175    /// The returned layer can be cloned to share statistics across operators.
176    ///
177    /// # Examples
178    ///
179    /// ```no_run
180    /// # use std::time::Duration;
181    /// #
182    /// # use opendal_core::services;
183    /// # use opendal_core::Operator;
184    /// # use opendal_core::Result;
185    /// # use opendal_layer_tail_cut::TailCutLayer;
186    /// #
187    /// # fn main() -> Result<()> {
188    /// let layer = TailCutLayer::builder()
189    ///     .percentile(95)
190    ///     .window(Duration::from_secs(60))
191    ///     .build();
192    ///
193    /// // Share the layer across operators
194    /// let op1 = Operator::new(services::Memory::default())?
195    ///     .layer(layer.clone());
196    ///
197    /// let op2 = Operator::new(services::Memory::default())?
198    ///     .layer(layer.clone());
199    /// // op1 and op2 share the same statistics
200    /// # Ok(())
201    /// # }
202    /// ```
203    pub fn build(self) -> TailCutLayer {
204        TailCutLayer {
205            config: Arc::new(TailCutConfig {
206                percentile: self.percentile,
207                safety_factor: self.safety_factor,
208                window: self.window,
209                min_samples: self.min_samples,
210                min_deadline: self.min_deadline,
211                max_deadline: self.max_deadline,
212            }),
213            stats: Arc::new(TailCutStats::new()),
214        }
215    }
216}
217
218/// Configuration for TailCutLayer (immutable).
219#[derive(Debug)]
220struct TailCutConfig {
221    percentile: u8,
222    safety_factor: f64,
223    window: Duration,
224    min_samples: usize,
225    min_deadline: Duration,
226    max_deadline: Duration,
227}
228
229/// `TailCutLayer` monitors request latency and cancels requests that run
230/// significantly slower than the historical baseline (for example, slower than
231/// P95).
232///
233/// Create this layer with [`TailCutLayer::builder()`]. Clone it to share
234/// statistics across multiple operators.
235///
236/// # Examples
237///
238/// ```no_run
239/// # use std::time::Duration;
240/// #
241/// # use opendal_core::services;
242/// # use opendal_core::Operator;
243/// # use opendal_core::Result;
244/// # use opendal_layer_tail_cut::TailCutLayer;
245/// #
246/// # fn main() -> Result<()> {
247/// let layer = TailCutLayer::builder()
248///     .percentile(95)
249///     .safety_factor(1.3)
250///     .window(Duration::from_secs(60))
251///     .build();
252///
253/// let op = Operator::new(services::Memory::default())?
254///     .layer(layer);
255/// # Ok(())
256/// # }
257/// ```
258#[derive(Clone)]
259pub struct TailCutLayer {
260    config: Arc<TailCutConfig>,
261    stats: Arc<TailCutStats>,
262}
263
264impl Debug for TailCutLayer {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        f.debug_struct("TailCutLayer")
267            .field("config", &self.config)
268            .finish_non_exhaustive()
269    }
270}
271
272impl Default for TailCutLayer {
273    fn default() -> Self {
274        Self::builder().build()
275    }
276}
277
278impl TailCutLayer {
279    /// Create a new [`TailCutLayerBuilder`] to configure the layer.
280    pub fn builder() -> TailCutLayerBuilder {
281        TailCutLayerBuilder::default()
282    }
283
284    /// Create a new [`TailCutLayer`].
285    ///
286    /// This is equivalent to `TailCutLayer::builder().build()`.
287    pub fn new() -> Self {
288        Self::default()
289    }
290}
291
292impl Layer for TailCutLayer {
293    fn apply_service(&self, inner: Servicer) -> Servicer {
294        Arc::new(self.layer(inner))
295    }
296}
297
298impl TailCutLayer {
299    fn layer(&self, inner: Servicer) -> TailCutService {
300        TailCutService {
301            inner,
302            config: self.config.clone(),
303            stats: self.stats.clone(),
304        }
305    }
306}
307
308#[doc(hidden)]
309/// Service wrapper that applies tail-cut deadlines to operation calls.
310pub struct TailCutService {
311    inner: Servicer,
312    config: Arc<TailCutConfig>,
313    stats: Arc<TailCutStats>,
314}
315
316impl Debug for TailCutService {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        f.debug_struct("TailCutService")
319            .field("config", &self.config)
320            .finish_non_exhaustive()
321    }
322}
323
324impl TailCutService {
325    /// Calculate the deadline for a given operation and size.
326    fn calculate_deadline(&self, op: Operation, size: Option<u64>) -> Option<Duration> {
327        let op_stats = self.stats.stats_for(op);
328
329        if op_stats.total_samples(size, self.config.window) < self.config.min_samples {
330            return None;
331        }
332
333        let q = self.config.percentile as f64 / 100.0;
334        let pctl = op_stats.quantile(size, q, self.config.window)?;
335
336        let deadline = Duration::from_secs_f64(pctl.as_secs_f64() * self.config.safety_factor);
337        Some(deadline.clamp(self.config.min_deadline, self.config.max_deadline))
338    }
339
340    async fn with_deadline<F, T>(&self, op: Operation, size: Option<u64>, fut: F) -> Result<T>
341    where
342        F: Future<Output = Result<T>>,
343    {
344        let start = Instant::now();
345
346        let result = if let Some(deadline) = self.calculate_deadline(op, size) {
347            match tokio::time::timeout(deadline, fut).await {
348                Ok(res) => res,
349                Err(_) => Err(Error::new(ErrorKind::Unexpected, "cancelled by tail cut")
350                    .with_operation(op)
351                    .with_context("percentile", format!("P{}", self.config.percentile))
352                    .with_context("deadline", format!("{:?}", deadline))
353                    .set_temporary()),
354            }
355        } else {
356            fut.await
357        };
358
359        if result.is_ok() {
360            let latency = start.elapsed();
361            self.stats.stats_for(op).record(size, latency);
362        }
363
364        result
365    }
366}
367
368impl Service for TailCutService {
369    type Reader = TailCutWrapper<oio::Reader>;
370    type Writer = TailCutWrapper<oio::Writer>;
371    type Lister = TailCutWrapper<oio::Lister>;
372    type Deleter = TailCutWrapper<oio::Deleter>;
373    type Copier = TailCutWrapper<oio::Copier>;
374    type Composer = oio::Composer;
375
376    fn info(&self) -> ServiceInfo {
377        self.inner.info()
378    }
379
380    fn capability(&self) -> Capability {
381        self.inner.capability()
382    }
383
384    fn compose(&self, ctx: &OperationContext, to: &str, args: OpCompose) -> Result<Self::Composer> {
385        self.inner.compose(ctx, to, args)
386    }
387
388    async fn create_dir(
389        &self,
390        ctx: &OperationContext,
391        path: &str,
392        args: OpCreateDir,
393    ) -> Result<RpCreateDir> {
394        self.with_deadline(
395            Operation::CreateDir,
396            None,
397            self.inner.create_dir(ctx, path, args),
398        )
399        .await
400    }
401
402    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
403        self.inner
404            .read(ctx, path, args)
405            .map(|r| TailCutWrapper::new(r, None, self.config.clone(), self.stats.clone()))
406    }
407
408    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
409        self.inner
410            .write(ctx, path, args)
411            .map(|w| TailCutWrapper::new(w, None, self.config.clone(), self.stats.clone()))
412    }
413
414    fn copy(
415        &self,
416        ctx: &OperationContext,
417        from: &str,
418        to: &str,
419        args: OpCopy,
420    ) -> Result<Self::Copier> {
421        self.inner
422            .copy(ctx, from, to, args)
423            .map(|c| TailCutWrapper::new(c, None, self.config.clone(), self.stats.clone()))
424    }
425
426    async fn rename(
427        &self,
428        ctx: &OperationContext,
429        from: &str,
430        to: &str,
431        args: OpRename,
432    ) -> Result<RpRename> {
433        self.with_deadline(
434            Operation::Rename,
435            None,
436            self.inner.rename(ctx, from, to, args),
437        )
438        .await
439    }
440
441    async fn restore(
442        &self,
443        ctx: &OperationContext,
444        path: &str,
445        args: OpRestore,
446    ) -> Result<RpRestore> {
447        self.inner.restore(ctx, path, args).await
448    }
449
450    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
451        self.with_deadline(Operation::Stat, None, self.inner.stat(ctx, path, args))
452            .await
453    }
454
455    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
456        self.inner
457            .delete(ctx)
458            .map(|d| TailCutWrapper::new(d, None, self.config.clone(), self.stats.clone()))
459    }
460
461    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
462        self.inner
463            .list(ctx, path, args)
464            .map(|l| TailCutWrapper::new(l, None, self.config.clone(), self.stats.clone()))
465    }
466
467    async fn presign(
468        &self,
469        ctx: &OperationContext,
470        path: &str,
471        args: OpPresign,
472    ) -> Result<RpPresign> {
473        self.with_deadline(
474            Operation::Presign,
475            None,
476            self.inner.presign(ctx, path, args),
477        )
478        .await
479    }
480}
481
482#[doc(hidden)]
483/// Body wrapper that applies tail-cut deadlines to streaming I/O calls.
484pub struct TailCutWrapper<R> {
485    inner: R,
486    size: Option<u64>,
487    config: Arc<TailCutConfig>,
488    stats: Arc<TailCutStats>,
489}
490
491impl<R> TailCutWrapper<R> {
492    fn new(
493        inner: R,
494        size: Option<u64>,
495        config: Arc<TailCutConfig>,
496        stats: Arc<TailCutStats>,
497    ) -> Self {
498        Self {
499            inner,
500            size,
501            config,
502            stats,
503        }
504    }
505
506    fn calculate_deadline(&self, op: Operation) -> Option<Duration> {
507        self.calculate_deadline_for(op, self.size)
508    }
509
510    fn calculate_deadline_for(&self, op: Operation, size: Option<u64>) -> Option<Duration> {
511        let op_stats = self.stats.stats_for(op);
512
513        if op_stats.total_samples(size, self.config.window) < self.config.min_samples {
514            return None;
515        }
516
517        let q = self.config.percentile as f64 / 100.0;
518        let pctl = op_stats.quantile(size, q, self.config.window)?;
519
520        let deadline = Duration::from_secs_f64(pctl.as_secs_f64() * self.config.safety_factor);
521        Some(deadline.clamp(self.config.min_deadline, self.config.max_deadline))
522    }
523
524    #[inline]
525    async fn with_io_deadline<F, T>(
526        deadline: Option<Duration>,
527        percentile: u8,
528        stats: &Arc<TailCutStats>,
529        size: Option<u64>,
530        op: Operation,
531        fut: F,
532    ) -> Result<T>
533    where
534        F: std::future::Future<Output = Result<T>>,
535    {
536        let start = Instant::now();
537
538        let result = if let Some(dl) = deadline {
539            match tokio::time::timeout(dl, fut).await {
540                Ok(res) => res,
541                Err(_) => Err(
542                    Error::new(ErrorKind::Unexpected, "io cancelled by tail cut")
543                        .with_operation(op)
544                        .with_context("percentile", format!("P{}", percentile))
545                        .with_context("deadline", format!("{:?}", dl))
546                        .set_temporary(),
547                ),
548            }
549        } else {
550            fut.await
551        };
552
553        if result.is_ok() {
554            let latency = start.elapsed();
555            stats.stats_for(op).record(size, latency);
556        }
557
558        result
559    }
560}
561
562impl<R: oio::ReadStream> oio::ReadStream for TailCutWrapper<R> {
563    async fn read(&mut self) -> Result<Buffer> {
564        let deadline = self.calculate_deadline_for(Operation::Read, self.size);
565        Self::with_io_deadline(
566            deadline,
567            self.config.percentile,
568            &self.stats,
569            self.size,
570            Operation::Read,
571            self.inner.read(),
572        )
573        .await
574    }
575}
576
577impl<R: oio::Read> oio::Read for TailCutWrapper<R> {
578    async fn open(&self, range: BytesRange) -> Result<(RpRead, Box<dyn oio::ReadStreamDyn>)> {
579        let size = range.size();
580        let deadline = self.calculate_deadline_for(Operation::Read, size);
581        let (rp, stream) = Self::with_io_deadline(
582            deadline,
583            self.config.percentile,
584            &self.stats,
585            size,
586            Operation::Read,
587            self.inner.open(range),
588        )
589        .await?;
590
591        Ok((
592            rp,
593            Box::new(TailCutWrapper::new(
594                stream,
595                size,
596                self.config.clone(),
597                self.stats.clone(),
598            )) as Box<dyn oio::ReadStreamDyn>,
599        ))
600    }
601
602    async fn read(&self, range: BytesRange) -> Result<(RpRead, Buffer)> {
603        let size = range.size();
604        let deadline = self.calculate_deadline_for(Operation::Read, size);
605        Self::with_io_deadline(
606            deadline,
607            self.config.percentile,
608            &self.stats,
609            size,
610            Operation::Read,
611            self.inner.read(range),
612        )
613        .await
614    }
615}
616
617impl<R: oio::Write> oio::Write for TailCutWrapper<R> {
618    async fn write(&mut self, bs: Buffer) -> Result<()> {
619        let deadline = self.calculate_deadline(Operation::Write);
620        Self::with_io_deadline(
621            deadline,
622            self.config.percentile,
623            &self.stats,
624            self.size,
625            Operation::Write,
626            self.inner.write(bs),
627        )
628        .await
629    }
630
631    async fn copy_from(&mut self, path: &str, args: OpRead, range: BytesRange) -> Result<()> {
632        let deadline = self.calculate_deadline(Operation::Write);
633        Self::with_io_deadline(
634            deadline,
635            self.config.percentile,
636            &self.stats,
637            self.size,
638            Operation::Write,
639            self.inner.copy_from(path, args, range),
640        )
641        .await
642    }
643
644    async fn close(&mut self) -> Result<Metadata> {
645        let deadline = self.calculate_deadline(Operation::Write);
646        Self::with_io_deadline(
647            deadline,
648            self.config.percentile,
649            &self.stats,
650            self.size,
651            Operation::Write,
652            self.inner.close(),
653        )
654        .await
655    }
656
657    async fn abort(&mut self) -> Result<()> {
658        let deadline = self.calculate_deadline(Operation::Write);
659        Self::with_io_deadline(
660            deadline,
661            self.config.percentile,
662            &self.stats,
663            self.size,
664            Operation::Write,
665            self.inner.abort(),
666        )
667        .await
668    }
669}
670
671impl<R: oio::List> oio::List for TailCutWrapper<R> {
672    async fn next(&mut self) -> Result<Option<oio::Entry>> {
673        let deadline = self.calculate_deadline(Operation::List);
674        Self::with_io_deadline(
675            deadline,
676            self.config.percentile,
677            &self.stats,
678            self.size,
679            Operation::List,
680            self.inner.next(),
681        )
682        .await
683    }
684}
685
686impl<R: oio::Delete> oio::Delete for TailCutWrapper<R> {
687    async fn delete(&mut self, path: &str, args: OpDelete) -> Result<()> {
688        self.inner.delete(path, args).await
689    }
690
691    async fn close(&mut self) -> Result<()> {
692        let deadline = self.calculate_deadline(Operation::Delete);
693        Self::with_io_deadline(
694            deadline,
695            self.config.percentile,
696            &self.stats,
697            self.size,
698            Operation::Delete,
699            self.inner.close(),
700        )
701        .await
702    }
703}
704
705impl<C: oio::Copy> oio::Copy for TailCutWrapper<C> {
706    async fn next(&mut self) -> Result<Option<usize>> {
707        let deadline = self.calculate_deadline(Operation::Copy);
708        Self::with_io_deadline(
709            deadline,
710            self.config.percentile,
711            &self.stats,
712            self.size,
713            Operation::Copy,
714            self.inner.next(),
715        )
716        .await
717    }
718
719    async fn close(&mut self) -> Result<Metadata> {
720        let deadline = self.calculate_deadline(Operation::Copy);
721        Self::with_io_deadline(
722            deadline,
723            self.config.percentile,
724            &self.stats,
725            self.size,
726            Operation::Copy,
727            self.inner.close(),
728        )
729        .await
730    }
731
732    async fn abort(&mut self) -> Result<()> {
733        let deadline = self.calculate_deadline(Operation::Copy);
734        Self::with_io_deadline(
735            deadline,
736            self.config.percentile,
737            &self.stats,
738            self.size,
739            Operation::Copy,
740            self.inner.abort(),
741        )
742        .await
743    }
744}
745
746/// Statistics engine for tail cut layer.
747struct TailCutStats {
748    // Statistics for each operation type (7 operations)
749    operations: [Arc<OperationStats>; 7],
750}
751
752impl TailCutStats {
753    fn new() -> Self {
754        Self {
755            operations: std::array::from_fn(|_| Arc::new(OperationStats::new())),
756        }
757    }
758
759    fn stats_for(&self, op: Operation) -> &Arc<OperationStats> {
760        let idx = match op {
761            Operation::Read => 0,
762            Operation::Write => 1,
763            Operation::Stat => 2,
764            Operation::List => 3,
765            Operation::Delete => 4,
766            Operation::Copy => 5,
767            Operation::Rename => 6,
768            _ => 2, // fallback to Stat
769        };
770        &self.operations[idx]
771    }
772}
773
774/// Statistics for a single operation type.
775struct OperationStats {
776    buckets: Vec<SizeBucket>,
777}
778
779impl OperationStats {
780    fn new() -> Self {
781        Self {
782            buckets: vec![
783                SizeBucket::new(0, Some(4 * 1024)),                   // [0, 4KB)
784                SizeBucket::new(4 * 1024, Some(64 * 1024)),           // [4KB, 64KB)
785                SizeBucket::new(64 * 1024, Some(1024 * 1024)),        // [64KB, 1MB)
786                SizeBucket::new(1024 * 1024, Some(16 * 1024 * 1024)), // [1MB, 16MB)
787                SizeBucket::new(16 * 1024 * 1024, Some(256 * 1024 * 1024)), // [16MB, 256MB)
788                SizeBucket::new(256 * 1024 * 1024, None),             // [256MB, ∞)
789            ],
790        }
791    }
792
793    fn bucket_for(&self, size: Option<u64>) -> &SizeBucket {
794        let size = size.unwrap_or(u64::MAX);
795
796        self.buckets
797            .iter()
798            .find(|b| b.contains(size))
799            .unwrap_or(&self.buckets[self.buckets.len() - 1])
800    }
801
802    fn record(&self, size: Option<u64>, latency: Duration) {
803        self.bucket_for(size).histogram.record(latency);
804    }
805
806    fn quantile(&self, size: Option<u64>, q: f64, window: Duration) -> Option<Duration> {
807        self.bucket_for(size).histogram.quantile(q, window)
808    }
809
810    fn total_samples(&self, size: Option<u64>, window: Duration) -> usize {
811        self.bucket_for(size).histogram.total_samples(window)
812    }
813}
814
815/// Size bucket for categorizing operations by data size.
816struct SizeBucket {
817    min_size: u64,
818    max_size: Option<u64>,
819    histogram: WindowedHistogram,
820}
821
822impl SizeBucket {
823    fn new(min_size: u64, max_size: Option<u64>) -> Self {
824        Self {
825            min_size,
826            max_size,
827            histogram: WindowedHistogram::new(),
828        }
829    }
830
831    fn contains(&self, size: u64) -> bool {
832        size >= self.min_size && self.max_size.is_none_or(|max| size < max)
833    }
834}
835
836const SLICE_DURATION_MS: u64 = 10_000; // 10 seconds per slice
837const NUM_SLICES: usize = 12; // 12 slices = 120 seconds total window
838const NUM_BUCKETS: usize = 17; // 17 buckets covering 1ms to 64s
839
840/// Windowed histogram using lock-free atomic operations.
841struct WindowedHistogram {
842    slices: Box<[TimeSlice; NUM_SLICES]>,
843    current_idx: AtomicUsize,
844    last_rotate: AtomicU64,
845}
846
847impl WindowedHistogram {
848    fn new() -> Self {
849        Self {
850            slices: Box::new(std::array::from_fn(|_| TimeSlice::new())),
851            current_idx: AtomicUsize::new(0),
852            last_rotate: AtomicU64::new(Self::now_ms()),
853        }
854    }
855
856    fn record(&self, latency: Duration) {
857        self.maybe_rotate();
858
859        let bucket_idx = Self::latency_to_bucket(latency);
860        let slice_idx = self.current_idx.load(Ordering::Relaxed);
861
862        self.slices[slice_idx].buckets[bucket_idx].fetch_add(1, Ordering::Relaxed);
863    }
864
865    fn quantile(&self, q: f64, window: Duration) -> Option<Duration> {
866        debug_assert!((0.0..=1.0).contains(&q), "quantile must be in [0, 1]");
867
868        let snapshot = self.snapshot(window);
869        let total: u64 = snapshot.iter().sum();
870
871        if total == 0 {
872            return None;
873        }
874
875        let target = (total as f64 * q).ceil() as u64;
876        let mut cumsum = 0u64;
877
878        for (bucket_idx, &count) in snapshot.iter().enumerate() {
879            cumsum += count;
880            if cumsum >= target {
881                return Some(Self::bucket_to_latency(bucket_idx));
882            }
883        }
884
885        Some(Self::bucket_to_latency(NUM_BUCKETS - 1))
886    }
887
888    fn total_samples(&self, window: Duration) -> usize {
889        self.snapshot(window).iter().map(|&v| v as usize).sum()
890    }
891
892    fn snapshot(&self, window: Duration) -> [u64; NUM_BUCKETS] {
893        let mut result = [0u64; NUM_BUCKETS];
894        let now_ms = Self::now_ms();
895        let window_ms = window.as_millis() as u64;
896
897        for slice in self.slices.iter() {
898            let start = slice.start_epoch_ms.load(Ordering::Acquire);
899
900            if start > 0 && now_ms.saturating_sub(start) < window_ms + SLICE_DURATION_MS {
901                for (i, bucket) in slice.buckets.iter().enumerate() {
902                    result[i] += bucket.load(Ordering::Relaxed);
903                }
904            }
905        }
906
907        result
908    }
909
910    fn maybe_rotate(&self) {
911        let now = Self::now_ms();
912        let last_rotate = self.last_rotate.load(Ordering::Relaxed);
913
914        if now - last_rotate >= SLICE_DURATION_MS
915            && self
916                .last_rotate
917                .compare_exchange(last_rotate, now, Ordering::Release, Ordering::Relaxed)
918                .is_ok()
919        {
920            let old_idx = self.current_idx.load(Ordering::Relaxed);
921            let new_idx = (old_idx + 1) % NUM_SLICES;
922
923            let new_slice = &self.slices[new_idx];
924            new_slice.start_epoch_ms.store(now, Ordering::Release);
925            for bucket in &new_slice.buckets {
926                bucket.store(0, Ordering::Relaxed);
927            }
928
929            self.current_idx.store(new_idx, Ordering::Release);
930        }
931    }
932
933    fn latency_to_bucket(latency: Duration) -> usize {
934        let ms = latency.as_millis() as u64;
935
936        if ms == 0 {
937            return 0;
938        }
939
940        let bucket = 64 - ms.leading_zeros();
941        (bucket as usize).min(NUM_BUCKETS - 1)
942    }
943
944    fn bucket_to_latency(bucket_idx: usize) -> Duration {
945        if bucket_idx == 0 {
946            Duration::from_millis(1)
947        } else if bucket_idx >= NUM_BUCKETS - 1 {
948            Duration::from_secs(64)
949        } else {
950            Duration::from_millis(1u64 << bucket_idx)
951        }
952    }
953
954    fn now_ms() -> u64 {
955        // SAFETY: Unless System clock goes backwards before UNIX_EPOCH, this should never fail.
956        u64::try_from(Timestamp::now().into_inner().as_millisecond()).unwrap()
957    }
958}
959
960/// Time slice in the sliding window.
961struct TimeSlice {
962    // 17 buckets covering 1ms to 64s (logarithmic scale)
963    buckets: [AtomicU64; NUM_BUCKETS],
964    start_epoch_ms: AtomicU64,
965}
966
967impl TimeSlice {
968    fn new() -> Self {
969        Self {
970            buckets: std::array::from_fn(|_| AtomicU64::new(0)),
971            start_epoch_ms: AtomicU64::new(0),
972        }
973    }
974}
975
976#[cfg(test)]
977mod tests {
978    use super::*;
979
980    #[test]
981    fn test_latency_to_bucket() {
982        assert_eq!(
983            WindowedHistogram::latency_to_bucket(Duration::from_millis(0)),
984            0
985        );
986        assert_eq!(
987            WindowedHistogram::latency_to_bucket(Duration::from_millis(1)),
988            1
989        );
990        assert_eq!(
991            WindowedHistogram::latency_to_bucket(Duration::from_millis(2)),
992            2
993        );
994        assert_eq!(
995            WindowedHistogram::latency_to_bucket(Duration::from_millis(4)),
996            3
997        );
998        assert_eq!(
999            WindowedHistogram::latency_to_bucket(Duration::from_millis(8)),
1000            4
1001        );
1002        assert_eq!(
1003            WindowedHistogram::latency_to_bucket(Duration::from_millis(500)),
1004            9
1005        );
1006        assert_eq!(
1007            WindowedHistogram::latency_to_bucket(Duration::from_secs(1)),
1008            10
1009        );
1010        assert_eq!(
1011            WindowedHistogram::latency_to_bucket(Duration::from_secs(2)),
1012            11
1013        );
1014        assert_eq!(
1015            WindowedHistogram::latency_to_bucket(Duration::from_secs(64)),
1016            16
1017        );
1018        assert_eq!(
1019            WindowedHistogram::latency_to_bucket(Duration::from_secs(1000)),
1020            16
1021        );
1022    }
1023
1024    #[test]
1025    fn test_size_bucket_contains() {
1026        let bucket = SizeBucket::new(0, Some(4096));
1027        assert!(bucket.contains(0));
1028        assert!(bucket.contains(4095));
1029        assert!(!bucket.contains(4096));
1030
1031        let bucket = SizeBucket::new(4096, None);
1032        assert!(!bucket.contains(4095));
1033        assert!(bucket.contains(4096));
1034        assert!(bucket.contains(u64::MAX));
1035    }
1036
1037    #[tokio::test]
1038    async fn test_histogram_basic() {
1039        let hist = WindowedHistogram::new();
1040        let now = WindowedHistogram::now_ms();
1041        hist.slices[0].start_epoch_ms.store(now, Ordering::Release);
1042
1043        hist.record(Duration::from_millis(10));
1044        hist.record(Duration::from_millis(20));
1045        hist.record(Duration::from_millis(30));
1046
1047        let samples = hist.total_samples(Duration::from_secs(60));
1048        assert_eq!(samples, 3);
1049
1050        let p50 = hist.quantile(0.5, Duration::from_secs(60));
1051        assert!(p50.is_some());
1052    }
1053
1054    #[tokio::test]
1055    async fn test_tail_cut_layer_build() {
1056        let layer = TailCutLayer::builder()
1057            .percentile(95)
1058            .safety_factor(1.5)
1059            .window(Duration::from_secs(60))
1060            .min_samples(100)
1061            .min_deadline(Duration::from_millis(200))
1062            .max_deadline(Duration::from_secs(20))
1063            .build();
1064
1065        assert_eq!(layer.config.percentile, 95);
1066        assert_eq!(layer.config.safety_factor, 1.5);
1067        assert_eq!(layer.config.window, Duration::from_secs(60));
1068        assert_eq!(layer.config.min_samples, 100);
1069        assert_eq!(layer.config.min_deadline, Duration::from_millis(200));
1070        assert_eq!(layer.config.max_deadline, Duration::from_secs(20));
1071    }
1072
1073    #[tokio::test]
1074    async fn test_layer_clone_shares_stats() {
1075        let layer = TailCutLayer::new();
1076        let cloned = layer.clone();
1077
1078        assert!(Arc::ptr_eq(&layer.stats, &cloned.stats));
1079    }
1080}