1use std::collections::{BTreeMap, VecDeque};
6use std::sync::Arc;
7
8use rustev_contract::remote::ExchangeRecord;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct TraceParent {
14 pub trace_id: [u8; 16],
15 pub parent_id: [u8; 8],
16 pub sampled: bool,
17}
18
19fn hex_bytes<const N: usize>(s: &str) -> Option<[u8; N]> {
20 if s.len() != N * 2
21 || !s
22 .bytes()
23 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
24 {
25 return None;
26 }
27 let mut out = [0u8; N];
28 for (i, o) in out.iter_mut().enumerate() {
29 *o = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()?;
30 }
31 Some(out)
32}
33
34pub fn parse_traceparent(s: &str) -> Option<TraceParent> {
37 let mut parts = s.split('-');
38 let version = parts.next()?;
39 let trace = parts.next()?;
40 let parent = parts.next()?;
41 let flags = parts.next()?;
42 let rest: Vec<&str> = parts.collect();
43 let v: [u8; 1] = hex_bytes(version)?;
44 if v[0] == 0xff {
45 return None;
46 }
47 if v[0] == 0 && !rest.is_empty() {
49 return None;
50 }
51 let trace_id: [u8; 16] = hex_bytes(trace)?;
52 let parent_id: [u8; 8] = hex_bytes(parent)?;
53 let f: [u8; 1] = hex_bytes(flags)?;
54 if trace_id.iter().all(|b| *b == 0) || parent_id.iter().all(|b| *b == 0) {
55 return None;
56 }
57 Some(TraceParent {
58 trace_id,
59 parent_id,
60 sampled: f[0] & 1 == 1,
61 })
62}
63
64#[derive(Debug, Clone)]
66pub struct Context {
67 pub trace_id: [u8; 16],
68 pub root_span_id: [u8; 8],
69 pub parent: Option<TraceParent>,
71 pub tracestate: Option<String>,
73 pub anchor_unix_nanos: u64,
75 registered_ms: u64,
76}
77
78#[derive(Debug)]
80pub struct ContextTable {
81 max: usize,
82 ttl_ms: u64,
83 entries: BTreeMap<String, Context>,
84}
85
86impl ContextTable {
87 pub fn new(max: usize, ttl_ms: u64) -> Self {
88 ContextTable {
89 max,
90 ttl_ms,
91 entries: BTreeMap::new(),
92 }
93 }
94
95 pub fn expire(&mut self, now_ms: u64) -> u64 {
97 let ttl = self.ttl_ms;
98 let before = self.entries.len();
99 self.entries
100 .retain(|_, c| now_ms.saturating_sub(c.registered_ms) < ttl);
101 (before - self.entries.len()) as u64
102 }
103
104 #[allow(clippy::too_many_arguments)]
106 pub fn insert(
107 &mut self,
108 decision_id: &str,
109 trace_id: [u8; 16],
110 root_span_id: [u8; 8],
111 parent: Option<TraceParent>,
112 tracestate: Option<String>,
113 anchor_unix_nanos: u64,
114 now_ms: u64,
115 ) -> bool {
116 if !self.entries.contains_key(decision_id) && self.entries.len() >= self.max {
117 return false;
118 }
119 self.entries.insert(
120 decision_id.to_string(),
121 Context {
122 trace_id,
123 root_span_id,
124 parent,
125 tracestate: if parent.is_some() { tracestate } else { None },
126 anchor_unix_nanos,
127 registered_ms: now_ms,
128 },
129 );
130 true
131 }
132
133 pub fn take(&mut self, decision_id: &str) -> Option<Context> {
134 self.entries.remove(decision_id)
135 }
136
137 pub fn len(&self) -> usize {
138 self.entries.len()
139 }
140
141 pub fn is_empty(&self) -> bool {
142 self.entries.is_empty()
143 }
144}
145
146pub const EXPORTED_MEMORY: usize = 4096;
149
150#[derive(Debug)]
154pub struct JoinBuffer {
155 max: usize,
156 ttl_ms: u64,
157 records: BTreeMap<String, (Arc<ExchangeRecord>, u64)>,
158 exported: VecDeque<String>,
159 exported_set: BTreeMap<String, ()>,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum Joined {
165 Buffered,
167 Unjoined,
170}
171
172impl JoinBuffer {
173 pub fn new(max: usize, ttl_ms: u64) -> Self {
174 JoinBuffer {
175 max,
176 ttl_ms,
177 records: BTreeMap::new(),
178 exported: VecDeque::new(),
179 exported_set: BTreeMap::new(),
180 }
181 }
182
183 pub fn expire(&mut self, now_ms: u64) -> u64 {
185 let ttl = self.ttl_ms;
186 let mut gone = std::collections::BTreeSet::new();
187 self.records.retain(|_, (x, at)| {
188 let keep = now_ms.saturating_sub(*at) < ttl;
189 if !keep {
190 gone.insert(Arc::as_ptr(x) as usize);
191 }
192 keep
193 });
194 gone.len() as u64
195 }
196
197 fn distinct(&self) -> usize {
198 let mut s = std::collections::BTreeSet::new();
199 for (x, _) in self.records.values() {
200 s.insert(Arc::as_ptr(x) as usize);
201 }
202 s.len()
203 }
204
205 pub fn insert(&mut self, x: ExchangeRecord, now_ms: u64) -> Joined {
206 if x.members
207 .iter()
208 .any(|m| self.exported_set.contains_key(&m.attempt_id))
209 || x.members.is_empty()
210 || self.distinct() >= self.max
211 {
212 return Joined::Unjoined;
213 }
214 let x = Arc::new(x);
215 for m in &x.members {
216 self.records
217 .insert(m.attempt_id.clone(), (x.clone(), now_ms));
218 }
219 Joined::Buffered
220 }
221
222 pub fn take(&mut self, attempt_ids: &[&str]) -> BTreeMap<String, Arc<ExchangeRecord>> {
224 let mut out = BTreeMap::new();
225 for id in attempt_ids {
226 if let Some((x, _)) = self.records.remove(*id) {
227 out.insert(id.to_string(), x);
228 }
229 if self.exported_set.insert(id.to_string(), ()).is_none() {
230 self.exported.push_back(id.to_string());
231 while self.exported.len() > self.max.max(EXPORTED_MEMORY) {
232 if let Some(old) = self.exported.pop_front() {
233 self.exported_set.remove(&old);
234 }
235 }
236 }
237 }
238 out
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn traceparent_parsing_follows_w3c() {
248 let ok = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
249 let p = parse_traceparent(ok).unwrap();
250 assert!(p.sampled);
251 assert_eq!(
252 p.parent_id,
253 [0xb7, 0xad, 0x6b, 0x71, 0x69, 0x20, 0x33, 0x31]
254 );
255 for bad in [
256 "",
257 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331",
258 "ff-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
259 "00-00000000000000000000000000000000-b7ad6b7169203331-01",
260 "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01",
261 "00-0AF7651916CD43DD8448EB211C80319C-b7ad6b7169203331-01",
262 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01-x",
263 "0-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
264 ] {
265 assert!(parse_traceparent(bad).is_none(), "{bad}");
266 }
267 assert!(
269 parse_traceparent("01-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00-x")
270 .is_some_and(|p| !p.sampled)
271 );
272 }
273}