Skip to main content

coven_storage/
request_tally.rs

1//! What one connection has asked its provider: for each kind of request, how
2//! many were sent, the body bytes they carried and the time spent waiting on
3//! the provider. A caller reads the totals before and after a step and logs
4//! the difference.
5
6use coven_foundation::clock::ClockRef;
7use coven_foundation::locks::lock_unpoisoned;
8use std::{fmt, sync::Mutex, time::Duration};
9
10/// A kind of provider request.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum RequestKind {
13    /// An exact metadata lookup.
14    Status,
15    /// A whole-object or ranged read, the location marker's included.
16    Read,
17    /// A single-request object creation, the location marker's included.
18    Create,
19    /// A conditional single-request floor replacement.
20    Replace,
21    /// One listing, across its pages.
22    List,
23    /// One deletion.
24    Delete,
25    /// A step of a recorded upload session: begin, part, resume, finish or abort.
26    Upload,
27    /// An account, sharing or joining request.
28    Account,
29}
30
31impl RequestKind {
32    const ALL: [Self; 8] = [
33        Self::Status,
34        Self::Read,
35        Self::Create,
36        Self::Replace,
37        Self::List,
38        Self::Delete,
39        Self::Upload,
40        Self::Account,
41    ];
42
43    fn index(self) -> usize {
44        self as usize
45    }
46
47    fn name(self) -> &'static str {
48        match self {
49            Self::Status => "status",
50            Self::Read => "read",
51            Self::Create => "create",
52            Self::Replace => "replace",
53            Self::List => "list",
54            Self::Delete => "delete",
55            Self::Upload => "upload",
56            Self::Account => "account",
57        }
58    }
59}
60
61/// One kind's requests: how many were sent, the body bytes sent or received,
62/// and the time from sending each until its response, plus the time spent
63/// waiting for a read's body.
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
65pub struct RequestTotal {
66    /// Requests sent, failed ones included.
67    pub count: u64,
68    /// Body bytes sent by writes or received by reads.
69    pub bytes: u64,
70    /// Time spent waiting on the provider.
71    pub time: Duration,
72}
73
74/// Every kind's totals since the connection was made.
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76pub struct RequestCounts([RequestTotal; RequestKind::ALL.len()]);
77
78impl RequestCounts {
79    /// The totals of one kind.
80    pub fn kind(&self, kind: RequestKind) -> RequestTotal {
81        self.0[kind.index()]
82    }
83
84    /// Every kind's totals added together.
85    pub fn total(&self) -> RequestTotal {
86        self.0.iter().fold(RequestTotal::default(), |sum, kind| RequestTotal {
87            count: sum.count + kind.count,
88            bytes: sum.bytes + kind.bytes,
89            time: sum.time + kind.time,
90        })
91    }
92
93    /// The requests made after `earlier`, a reading of the same connection.
94    pub fn since(&self, earlier: &Self) -> Self {
95        let mut since = Self::default();
96        for kind in RequestKind::ALL {
97            let (now, then) = (self.kind(kind), earlier.kind(kind));
98            since.0[kind.index()] = RequestTotal {
99                count: now.count.saturating_sub(then.count),
100                bytes: now.bytes.saturating_sub(then.bytes),
101                time: now.time.saturating_sub(then.time),
102            };
103        }
104        since
105    }
106}
107
108/// Each kind that sent a request, as `read 41 (182044 B, 1.2s)`, or `none`.
109impl fmt::Display for RequestCounts {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        let mut sent = RequestKind::ALL
112            .into_iter()
113            .filter(|kind| self.kind(*kind).count > 0 || self.kind(*kind).bytes > 0)
114            .peekable();
115        if sent.peek().is_none() {
116            return f.write_str("none");
117        }
118        for (index, kind) in sent.enumerate() {
119            let total = self.kind(kind);
120            if index > 0 {
121                f.write_str(", ")?;
122            }
123            write!(f, "{} {} ({} B, {:?})", kind.name(), total.count, total.bytes, total.time)?;
124        }
125        Ok(())
126    }
127}
128
129/// The running totals of one connection, timed by its clock.
130pub(crate) struct RequestTally {
131    clock: ClockRef,
132    totals: Mutex<RequestCounts>,
133}
134
135impl RequestTally {
136    pub(crate) fn new(clock: ClockRef) -> Self {
137        Self { clock, totals: Mutex::new(RequestCounts::default()) }
138    }
139
140    pub(crate) fn counts(&self) -> RequestCounts {
141        *lock_unpoisoned(&self.totals)
142    }
143
144    /// Send one request of `kind` carrying `bytes`, counting it whether it
145    /// succeeds or fails.
146    pub(crate) async fn send<T>(
147        &self,
148        kind: RequestKind,
149        bytes: u64,
150        request: impl std::future::Future<Output = T>,
151    ) -> T {
152        let started = self.clock.elapsed();
153        let result = request.await;
154        self.add(kind, 1, bytes, self.clock.elapsed().saturating_sub(started));
155        result
156    }
157
158    /// Wait for a read's body bytes, counting them and the wait as reading.
159    pub(crate) async fn receive<T>(
160        &self,
161        read: impl std::future::Future<Output = Result<usize, T>>,
162    ) -> Result<usize, T> {
163        let started = self.clock.elapsed();
164        let result = read.await;
165        let received = result.as_ref().map_or(0, |length| *length as u64);
166        self.add(RequestKind::Read, 0, received, self.clock.elapsed().saturating_sub(started));
167        result
168    }
169
170    /// Count a read's bytes that arrived with its response.
171    pub(crate) fn received(&self, bytes: u64) {
172        self.add(RequestKind::Read, 0, bytes, Duration::ZERO);
173    }
174
175    fn add(&self, kind: RequestKind, count: u64, bytes: u64, time: Duration) {
176        let mut totals = lock_unpoisoned(&self.totals);
177        let total = &mut totals.0[kind.index()];
178        total.count += count;
179        total.bytes += bytes;
180        total.time += time;
181    }
182}
183
184#[cfg(test)]
185#[path = "request_tally_tests.rs"]
186mod tests;