OXYGENHUB

Strata counts its requests by kind 1e0fbb47ab90

Negotiate on a repository kept in GCS still made ~250 requests each with
the range cache in place, so the cache is not missing ranges. stats() now
counts each kind of request (has, has that found nothing, get, range, put,
list, delete, reading and writing a reference), so a server can show which
its time goes to.

Refs andymccall/oxygenhub#228
Author
Andy McCall @andymccall <[email protected]>
via
agent Claude Code <[email protected]> · delegate for changes, until 2026-11-13 19:00Z · key ea5f78160088
When
· 2026-10-17 02:04 UTC
Signature
✔ signed by Claude Code
Hash
1e0fbb47ab9095d74947be7214dd0099540961dbd6eb564687669439a873c0ba
Depends on
2ddd471651cd 8d80356371c5
Depended on by
ef7d4ea6f33b 767ac6cd0545 30644f1ecb7b 87c948720343 8b53b935dd03
In bookmarks
carried-not-removed doctor-trust identical-sides keyless-tags main merge-remote-bookmark paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state
In tags
v0.10.0 v0.11.0 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
15 atoms, 0 tree operations, 1 file

Browse the tree at this change

modified crates/oxy-strata/src/lib.rs +90 −2
…
105 unchanged lines
106 106
static OPENS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
107 107
static CALLS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
108 108
static MICROS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
109 +
static BY_OP: [std::sync::atomic::AtomicU64; Op::ALL.len()] =
110 +
    [const { std::sync::atomic::AtomicU64::new(0) }; Op::ALL.len()];
109 111
112 +
/// The kinds of request a cloud store makes, counted apart so a server can
113 +
/// tell which of them its time goes to.
114 +
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115 +
pub enum Op {
116 +
    /// `has` that found the object.
117 +
    Has,
118 +
    /// `has` that did not: an absent object is never cached.
119 +
    HasMissing,
120 +
    Get,
121 +
    Range,
122 +
    Put,
123 +
    List,
124 +
    Delete,
125 +
    ReadRef,
126 +
    WriteRef,
127 +
}
128 +
129 +
impl Op {
130 +
    pub const ALL: [Op; 9] = [
131 +
        Op::Has,
132 +
        Op::HasMissing,
133 +
        Op::Get,
134 +
        Op::Range,
135 +
        Op::Put,
136 +
        Op::List,
137 +
        Op::Delete,
138 +
        Op::ReadRef,
139 +
        Op::WriteRef,
140 +
    ];
141 +
142 +
    pub fn name(self) -> &'static str {
143 +
        match self {
144 +
            Op::Has => "has",
145 +
            Op::HasMissing => "has_missing",
146 +
            Op::Get => "get",
147 +
            Op::Range => "range",
148 +
            Op::Put => "put",
149 +
            Op::List => "list",
150 +
            Op::Delete => "delete",
151 +
            Op::ReadRef => "read_ref",
152 +
            Op::WriteRef => "write_ref",
153 +
        }
154 +
    }
155 +
}
156 +
157 +
fn counted(op: Op) {
158 +
    BY_OP[op as usize].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
159 +
}
160 +
110 161
/// What the cloud stores have done since the process started: clients
111 162
/// opened, requests made and the time spent in them, for a server to show
112 163
/// (a hub's /health).
…
2 unchanged lines
115 166
    pub opens: u64,
116 167
    pub calls: u64,
117 168
    pub millis: u64,
169 +
    /// Requests by kind, in [`Op::ALL`]'s order.
170 +
    pub by_op: [u64; Op::ALL.len()],
118 171
}
119 172
173 +
impl Stats {
174 +
    /// Each kind of request with how many were made.
175 +
    pub fn ops(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
176 +
        Op::ALL
177 +
            .iter()
178 +
            .map(|op| (op.name(), self.by_op[*op as usize]))
179 +
    }
180 +
}
181 +
120 182
pub fn stats() -> Stats {
121 183
    use std::sync::atomic::Ordering::Relaxed;
122 184
    Stats {
123 185
        opens: OPENS.load(Relaxed),
124 186
        calls: CALLS.load(Relaxed),
125 187
        millis: MICROS.load(Relaxed) / 1000,
188 +
        by_op: std::array::from_fn(|i| BY_OP[i].load(Relaxed)),
126 189
    }
127 190
}
128 191
…
410 unchanged lines
539 602
    fn has(&self, key: &str) -> Result<bool> {
540 603
        let path = self.path(key)?;
541 604
        match self.block(self.inner.head(&path)) {
542 -
            Ok(_) => Ok(true),
543 -
            Err(object_store::Error::NotFound { .. }) => Ok(false),
605 +
            Ok(_) => {
606 +
                counted(Op::Has);
607 +
                Ok(true)
608 +
            }
609 +
            Err(object_store::Error::NotFound { .. }) => {
610 +
                counted(Op::HasMissing);
611 +
                Ok(false)
612 +
            }
544 613
            Err(e) => Err(self.map(e, key)),
545 614
        }
546 615
    }
547 616
548 617
    fn get(&self, key: &str) -> Result<Vec<u8>> {
549 618
        let path = self.path(key)?;
619 +
        counted(Op::Get);
550 620
        self.block(async {
551 621
            let r = self.inner.get(&path).await?;
552 622
            r.bytes().await
…
14 unchanged lines
567 637
            return Ok(Vec::new());
568 638
        }
569 639
        let want = (range.end - range.start) as usize;
640 +
        counted(Op::Range);
570 641
        let bytes = self
571 642
            .block(self.inner.get_range(&path, range.clone()))
572 643
            .map_err(|e| self.map(e, key))?;
…
14 unchanged lines
587 658
            mode: PutMode::Create,
588 659
            ..Default::default()
589 660
        };
661 +
        counted(Op::Put);
590 662
        self.block(
591 663
            self.inner
592 664
                .put_opts(&path, PutPayload::from(bytes.to_vec()), opts),
…
15 unchanged lines
608 680
        let full_dir = full_dir
609 681
            .map(|d| ObjectPath::parse(&d).map_err(|e| Error::Backend(format!("{d}: {e}"))))
610 682
            .transpose()?;
683 +
        counted(Op::List);
611 684
        let metas: Vec<object_store::ObjectMeta> = self
612 685
            .block(self.inner.list(full_dir.as_ref()).try_collect())
613 686
            .map_err(|e| self.map(e, prefix))?;
…
20 unchanged lines
634 707
635 708
    fn delete(&self, key: &str) -> Result<()> {
636 709
        let path = self.path(key)?;
710 +
        counted(Op::Delete);
637 711
        match self.block(self.inner.delete(&path)) {
638 712
            Ok(()) | Err(object_store::Error::NotFound { .. }) => Ok(()),
639 713
            Err(e) => Err(self.map(e, key)),
…
2 unchanged lines
642 716
643 717
    fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> {
644 718
        let path = self.path(key)?;
719 +
        counted(Op::ReadRef);
645 720
        let got = self.block(async {
646 721
            let r = self.inner.get_opts(&path, GetOptions::default()).await?;
647 722
            let meta = r.meta.clone();
…
20 unchanged lines
668 743
            mode,
669 744
            ..Default::default()
670 745
        };
746 +
        counted(Op::WriteRef);
671 747
        let put = self
672 748
            .block(
673 749
                self.inner
…
11 unchanged lines
685 761
686 762
#[cfg(test)]
687 763
mod tests {
764 +
765 +
    /// Every kind of request is reported, once and by its own name.
766 +
    #[test]
767 +
    fn stats_name_every_kind_of_request() {
768 +
        let names: Vec<&str> = super::stats().ops().map(|(n, _)| n).collect();
769 +
        assert_eq!(names.len(), super::Op::ALL.len());
770 +
        let mut unique = names.clone();
771 +
        unique.sort();
772 +
        unique.dedup();
773 +
        assert_eq!(unique.len(), names.len());
774 +
        assert!(names.contains(&"has_missing") && names.contains(&"range"));
775 +
    }
688 776
689 777
    /// One client per store and credentials (oxygenhub#234): opening the
690 778
    /// same again shares it, and only a new one counts as opened.
…
109 unchanged lines