statsig_rust/sdk_event_emitter/
event_emitter.rs1use crate::{
2 log_e,
3 sdk_event_emitter::{SdkEvent, SdkEventCode},
4 statsig_types::{DynamicConfig, Experiment, Layer},
5 Statsig,
6};
7use dashmap::DashMap;
8use std::{ops::Deref, sync::Arc};
9
10const TAG: &str = "SdkEventEmitter";
11
12#[derive(Clone)]
13struct Listener {
14 sub_id_value: String,
15 callback: Arc<dyn Fn(SdkEvent) + Send + Sync>,
16}
17
18#[derive(Clone)]
19struct InternalListener {
20 id: String,
21 callback: Arc<dyn Fn(SdkEvent) -> bool + Send + Sync>,
22}
23
24#[derive(Clone)]
25pub struct SubscriptionID {
26 value: String,
27 event: String,
28}
29
30impl SubscriptionID {
31 pub fn new(event: &str) -> Self {
32 Self {
33 value: uuid::Uuid::new_v4().to_string(),
34 event: event.to_string(),
35 }
36 }
37
38 pub fn error() -> Self {
39 Self {
40 value: "ERROR".to_string(),
41 event: "ERROR".to_string(),
42 }
43 }
44
45 pub fn decode(s: &str) -> Option<Self> {
46 let parts: Vec<&str> = s.split('@').collect();
47 if parts.len() != 2 {
48 return None;
49 }
50
51 Some(Self {
52 value: parts[0].to_string(),
53 event: parts[1].to_string(),
54 })
55 }
56
57 pub fn encode(self) -> String {
58 let mut encoded = self.value;
59 encoded.push('@');
60 encoded.push_str(&self.event);
61 encoded
62 }
63}
64
65#[derive(Default)]
66pub struct SdkEventEmitter {
67 listeners: DashMap<u8, Vec<Listener>>,
68 internal_listeners: DashMap<u8, Vec<InternalListener>>,
69}
70
71impl SdkEventEmitter {
72 pub fn subscribe<F>(&self, event: &str, callback: F) -> SubscriptionID
73 where
74 F: Fn(SdkEvent) + Send + Sync + 'static,
75 {
76 let code = SdkEventCode::from_name(event).as_raw();
77 if code == 0 {
78 log_e!(TAG, "Invalid event name: {}", event);
79 return SubscriptionID::error();
80 }
81
82 let sub_id = SubscriptionID::new(event);
83
84 self.listeners.entry(code).or_default().push(Listener {
85 sub_id_value: sub_id.value.clone(),
86 callback: Arc::new(callback),
87 });
88
89 sub_id
90 }
91
92 pub fn subscribe_internal<F>(&self, event: &str, callback: F)
93 where
94 F: Fn(SdkEvent) -> bool + Send + Sync + 'static,
95 {
96 let code = SdkEventCode::from_name(event).as_raw();
97 if code == 0 {
98 log_e!(TAG, "Invalid internal event name: {}", event);
99 return;
100 }
101
102 self.internal_listeners
103 .entry(code)
104 .or_default()
105 .push(InternalListener {
106 id: uuid::Uuid::new_v4().to_string(),
107 callback: Arc::new(callback),
108 });
109 }
110
111 pub fn unsubscribe(&self, event: &str) {
112 let code = SdkEventCode::from_name(event).as_raw();
113 self.listeners.remove(&code);
114 }
115
116 pub fn unsubscribe_by_id(&self, subscription_id: &SubscriptionID) {
117 let code = SdkEventCode::from_name(&subscription_id.event).as_raw();
118 let mut listeners = match self.listeners.get_mut(&code) {
119 Some(listeners) => listeners,
120 None => return,
121 };
122
123 listeners.retain(|listener| listener.sub_id_value != subscription_id.value);
124 }
125
126 pub fn unsubscribe_all(&self) {
127 self.listeners.clear();
128 }
129
130 pub(crate) fn emit(&self, event: SdkEvent) {
131 let all_code = SdkEventCode::from_name(SdkEvent::ALL).as_raw();
132 let event_code = event.get_code().as_raw();
133
134 let all_listeners = self.snapshot_listeners(all_code);
135 let all_internal_listeners = self.snapshot_internal_listeners(all_code);
136 let event_listeners = self.snapshot_listeners(event_code);
137 let event_internal_listeners = self.snapshot_internal_listeners(event_code);
138
139 Self::emit_to_listeners(&event, &all_listeners);
140 self.emit_to_internal_listeners(&event, all_code, &all_internal_listeners);
141 Self::emit_to_listeners(&event, &event_listeners);
142 self.emit_to_internal_listeners(&event, event_code, &event_internal_listeners);
143 }
144
145 fn snapshot_listeners(&self, code: u8) -> Vec<Listener> {
146 self.listeners
147 .get(&code)
148 .map(|listeners| listeners.value().clone())
149 .unwrap_or_default()
150 }
151
152 fn snapshot_internal_listeners(&self, code: u8) -> Vec<InternalListener> {
153 self.internal_listeners
154 .get(&code)
155 .map(|listeners| listeners.value().clone())
156 .unwrap_or_default()
157 }
158
159 fn emit_to_listeners(event: &SdkEvent, listeners: &[Listener]) {
160 listeners
161 .iter()
162 .for_each(|listener| (listener.callback)(event.clone()));
163 }
164
165 fn emit_to_internal_listeners(
166 &self,
167 event: &SdkEvent,
168 code: u8,
169 listeners: &[InternalListener],
170 ) {
171 let expired_ids = listeners
172 .iter()
173 .filter_map(|listener| {
174 if (listener.callback)(event.clone()) {
175 None
176 } else {
177 Some(listener.id.as_str())
178 }
179 })
180 .collect::<Vec<_>>();
181
182 if expired_ids.is_empty() {
183 return;
184 }
185
186 if let Some(mut listeners) = self.internal_listeners.get_mut(&code) {
190 listeners.retain(|listener| !expired_ids.contains(&listener.id.as_str()));
191 }
192 }
193}
194
195impl Deref for Statsig {
196 type Target = SdkEventEmitter;
197
198 fn deref(&self) -> &Self::Target {
199 &self.event_emitter
200 }
201}
202
203impl Statsig {
204 pub(crate) fn emit_gate_evaluated(
205 &self,
206 gate_name: &str,
207 rule_id: &str,
208 value: bool,
209 reason: &str,
210 ) {
211 self.emit(SdkEvent::GateEvaluated {
212 gate_name,
213 rule_id,
214 value,
215 reason,
216 });
217 }
218
219 pub(crate) fn emit_dynamic_config_evaluated(&self, config: &DynamicConfig) {
220 self.emit(SdkEvent::DynamicConfigEvaluated {
221 config_name: config.name.as_str(),
222 reason: config.details.reason.as_str(),
223 rule_id: Some(config.rule_id.as_str()),
224 value: config.__evaluation.as_ref().map(|e| &e.value),
225 });
226 }
227
228 pub(crate) fn emit_experiment_evaluated(&self, experiment: &Experiment) {
229 self.emit(SdkEvent::ExperimentEvaluated {
230 experiment_name: experiment.name.as_str(),
231 reason: experiment.details.reason.as_str(),
232 rule_id: Some(experiment.rule_id.as_str()),
233 value: experiment.__evaluation.as_ref().map(|e| &e.value),
234 group_name: experiment.group_name.as_deref(),
235 });
236 }
237
238 pub(crate) fn emit_layer_evaluated(&self, layer: &Layer) {
239 self.emit(SdkEvent::LayerEvaluated {
240 layer_name: layer.name.as_str(),
241 reason: layer.details.reason.as_str(),
242 rule_id: Some(layer.rule_id.as_str()),
243 });
244 }
245}
246
247#[cfg(feature = "ffi-support")]
248impl Statsig {
249 pub(crate) fn emit_gate_evaluated_parts(
250 &self,
251 gate_name: &str,
252 reason: &str,
253 eval_result: Option<&crate::evaluation::evaluator_result::EvaluatorResult>,
254 ) {
255 let mut rule_id = None;
256 let mut value = false;
257
258 if let Some(eval) = eval_result {
259 rule_id = eval.rule_id.as_ref().map(|r| r.as_str());
260 value = eval.bool_value;
261 }
262
263 self.emit(SdkEvent::GateEvaluated {
264 gate_name,
265 rule_id: rule_id.unwrap_or_default(),
266 value,
267 reason,
268 });
269 }
270
271 pub(crate) fn emit_dynamic_config_evaluated_parts(
272 &self,
273 config_name: &str,
274 reason: &str,
275 eval_result: Option<&crate::evaluation::evaluator_result::EvaluatorResult>,
276 ) {
277 let mut rule_id = None;
278 let mut value = None;
279
280 if let Some(eval) = eval_result {
281 rule_id = eval.rule_id.as_ref().map(|r| r.as_str());
282 value = eval.json_value.as_ref();
283 }
284
285 self.emit(SdkEvent::DynamicConfigEvaluated {
286 config_name,
287 reason,
288 rule_id,
289 value,
290 });
291 }
292
293 pub(crate) fn emit_experiment_evaluated_parts(
294 &self,
295 experiment_name: &str,
296 reason: &str,
297 eval_result: Option<&crate::evaluation::evaluator_result::EvaluatorResult>,
298 ) {
299 let mut rule_id = None;
300 let mut value = None;
301 let mut group_name = None;
302
303 if let Some(eval) = eval_result {
304 rule_id = eval.rule_id.as_ref().map(|r| r.as_str());
305 value = eval.json_value.as_ref();
306 group_name = eval.group_name.as_ref().map(|g| g.as_str());
307 }
308
309 self.emit(SdkEvent::ExperimentEvaluated {
310 experiment_name,
311 reason,
312 rule_id,
313 value,
314 group_name,
315 });
316 }
317
318 pub(crate) fn emit_layer_evaluated_parts(
319 &self,
320 layer_name: &str,
321 reason: &str,
322 eval_result: Option<&crate::evaluation::evaluator_result::EvaluatorResult>,
323 ) {
324 let mut rule_id = None;
325
326 if let Some(eval) = eval_result {
327 rule_id = eval.rule_id.as_ref().map(|r| r.as_str());
328 }
329
330 self.emit(SdkEvent::LayerEvaluated {
331 layer_name,
332 reason,
333 rule_id,
334 });
335 }
336}
337
338#[cfg(test)]
339mod cleanup_tests {
340 use super::*;
341 use std::{
342 sync::{
343 atomic::{AtomicUsize, Ordering},
344 mpsc, Arc, Barrier,
345 },
346 thread,
347 time::Duration,
348 };
349
350 #[test]
351 fn contended_cleanup_waits_and_prunes_dead_internal_listener() {
352 let emitter = Arc::new(SdkEventEmitter::default());
353 let callback_count = Arc::new(AtomicUsize::new(0));
354 let callback_started = Arc::new(Barrier::new(2));
355 let callback_can_return = Arc::new(Barrier::new(2));
356
357 let callback_count_clone = Arc::clone(&callback_count);
358 let callback_started_clone = Arc::clone(&callback_started);
359 let callback_can_return_clone = Arc::clone(&callback_can_return);
360 emitter.subscribe_internal(SdkEvent::GATE_EVALUATED, move |_| {
361 let invocation = callback_count_clone.fetch_add(1, Ordering::SeqCst);
362 if invocation == 0 {
363 callback_started_clone.wait();
364 callback_can_return_clone.wait();
365 }
366 false
367 });
368
369 let emitter_clone = Arc::clone(&emitter);
370 let (emit_done_tx, emit_done_rx) = mpsc::channel();
371 let emit_thread = thread::spawn(move || {
372 emitter_clone.emit(SdkEvent::GateEvaluated {
373 gate_name: "test_gate",
374 rule_id: "test_rule_id",
375 value: true,
376 reason: "test_reason",
377 });
378 emit_done_tx
379 .send(())
380 .expect("emit receiver should remain open");
381 });
382
383 callback_started.wait();
384 let code = SdkEventCode::GateEvaluated.as_raw();
385 let shard_guard = emitter
386 .internal_listeners
387 .get_mut(&code)
388 .expect("internal listener should exist");
389 callback_can_return.wait();
390
391 assert!(
392 emit_done_rx
393 .recv_timeout(Duration::from_millis(250))
394 .is_err(),
395 "emit should wait for contended listener cleanup"
396 );
397
398 drop(shard_guard);
399 emit_done_rx
400 .recv_timeout(Duration::from_secs(2))
401 .expect("emit should finish after the shard lock is released");
402 emit_thread.join().expect("emit thread should not panic");
403
404 emitter.emit(SdkEvent::GateEvaluated {
405 gate_name: "test_gate",
406 rule_id: "test_rule_id",
407 value: true,
408 reason: "test_reason",
409 });
410 assert_eq!(callback_count.load(Ordering::SeqCst), 1);
411 }
412}