Skip to main content

statsig_rust/sdk_event_emitter/
event_emitter.rs

1use 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        // Callback execution has completed and this thread holds no listener-map
187        // guard. Wait for the shard so expired one-shot listeners cannot be
188        // retained indefinitely under sustained contention.
189        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}