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 |