Skip to main content

aft/search_b2/
embed_counter.rs

1//! Request-scoped observations for semantic query embeddings.
2
3use std::collections::HashMap;
4use std::marker::PhantomData;
5use std::rc::Rc;
6use std::sync::{Mutex, OnceLock};
7use std::thread::ThreadId;
8
9use serde::{Deserialize, Serialize};
10
11/// Model identity used by the offline search-quality embedding fixture.
12pub const FIXTURE_PROVIDER_MODEL: &str = "aft-search-fixture-v1";
13
14#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
15pub struct EmbedCounts {
16    pub requested: u64,
17    pub cache_hits: u64,
18    pub live_calls: u64,
19}
20
21impl EmbedCounts {
22    fn add_assign(&mut self, observation: Self) {
23        self.requested = self.requested.saturating_add(observation.requested);
24        self.cache_hits = self.cache_hits.saturating_add(observation.cache_hits);
25        self.live_calls = self.live_calls.saturating_add(observation.live_calls);
26    }
27}
28
29#[derive(Clone, Debug)]
30struct EmbedAttribution {
31    request_id: String,
32}
33
34#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
35enum ExecutionId {
36    Task(tokio::task::Id),
37    Thread(ThreadId),
38}
39
40fn current_execution() -> ExecutionId {
41    tokio::task::try_id()
42        .map(ExecutionId::Task)
43        .unwrap_or_else(|| ExecutionId::Thread(std::thread::current().id()))
44}
45
46fn active_attributions() -> &'static Mutex<HashMap<ExecutionId, Vec<EmbedAttribution>>> {
47    static ACTIVE: OnceLock<Mutex<HashMap<ExecutionId, Vec<EmbedAttribution>>>> = OnceLock::new();
48    ACTIVE.get_or_init(|| Mutex::new(HashMap::new()))
49}
50
51fn observations() -> &'static Mutex<HashMap<String, EmbedCounts>> {
52    static OBSERVATIONS: OnceLock<Mutex<HashMap<String, EmbedCounts>>> = OnceLock::new();
53    OBSERVATIONS.get_or_init(|| Mutex::new(HashMap::new()))
54}
55
56/// Keeps embedding observations attributed to one request task.
57///
58/// Calls made outside a Tokio task use the current executor thread instead,
59/// which keeps the synchronous CLI and unit-test entry points isolated too.
60#[must_use]
61pub struct Guard {
62    execution: ExecutionId,
63    request_id: String,
64    _not_send: PhantomData<Rc<()>>,
65}
66
67/// Installs attribution for a request until the returned guard is dropped.
68pub fn install(request_id: impl Into<String>) -> Guard {
69    let request_id = request_id.into();
70    let execution = current_execution();
71    observations()
72        .lock()
73        .unwrap_or_else(std::sync::PoisonError::into_inner)
74        .insert(request_id.clone(), EmbedCounts::default());
75    active_attributions()
76        .lock()
77        .unwrap_or_else(std::sync::PoisonError::into_inner)
78        .entry(execution)
79        .or_default()
80        .push(EmbedAttribution {
81            request_id: request_id.clone(),
82        });
83    Guard {
84        execution,
85        request_id,
86        _not_send: PhantomData,
87    }
88}
89
90/// Adds embedding counts to the currently attributed request.
91pub fn record(counts: EmbedCounts) {
92    let execution = current_execution();
93    let request_id = active_attributions()
94        .lock()
95        .unwrap_or_else(std::sync::PoisonError::into_inner)
96        .get(&execution)
97        .and_then(|attributions| attributions.last())
98        .map(|current| current.request_id.clone());
99    let Some(request_id) = request_id else {
100        return;
101    };
102
103    observations()
104        .lock()
105        .unwrap_or_else(std::sync::PoisonError::into_inner)
106        .entry(request_id)
107        .or_default()
108        .add_assign(counts);
109}
110
111/// Returns the observations accumulated for `request_id`.
112pub fn read(request_id: &str) -> EmbedCounts {
113    observations()
114        .lock()
115        .unwrap_or_else(std::sync::PoisonError::into_inner)
116        .get(request_id)
117        .copied()
118        .unwrap_or_default()
119}
120
121impl Drop for Guard {
122    fn drop(&mut self) {
123        let mut active = active_attributions()
124            .lock()
125            .unwrap_or_else(std::sync::PoisonError::into_inner);
126        let mut remove_execution = false;
127        if let Some(attributions) = active.get_mut(&self.execution) {
128            let popped = attributions.pop();
129            debug_assert_eq!(
130                popped.as_ref().map(|current| current.request_id.as_str()),
131                Some(self.request_id.as_str())
132            );
133            remove_execution = attributions.is_empty();
134        }
135        if remove_execution {
136            active.remove(&self.execution);
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn sums_observations_and_ignores_unattributed_work() {
147        record(EmbedCounts {
148            requested: 99,
149            cache_hits: 99,
150            live_calls: 99,
151        });
152
153        let request_id = "counter-unit-sum";
154        let _guard = install(request_id);
155        record(EmbedCounts {
156            requested: 2,
157            cache_hits: 0,
158            live_calls: 1,
159        });
160        record(EmbedCounts {
161            requested: 0,
162            cache_hits: 1,
163            live_calls: 0,
164        });
165
166        assert_eq!(
167            read(request_id),
168            EmbedCounts {
169                requested: 2,
170                cache_hits: 1,
171                live_calls: 1,
172            }
173        );
174    }
175
176    #[test]
177    fn concurrent_requests_remain_isolated() {
178        let workers = (0..8)
179            .map(|index| {
180                std::thread::spawn(move || {
181                    let request_id = format!("counter-unit-concurrent-{index}");
182                    let _guard = install(&request_id);
183                    record(EmbedCounts {
184                        requested: index,
185                        cache_hits: 1,
186                        live_calls: index % 2,
187                    });
188                    (request_id.clone(), read(&request_id))
189                })
190            })
191            .collect::<Vec<_>>();
192
193        for (index, worker) in workers.into_iter().enumerate() {
194            let (request_id, counts) = worker.join().expect("counter worker");
195            assert_eq!(request_id, format!("counter-unit-concurrent-{index}"));
196            assert_eq!(counts.requested, index as u64);
197            assert_eq!(counts.cache_hits, 1);
198            assert_eq!(counts.live_calls, (index % 2) as u64);
199        }
200    }
201}