1use arc_swap::ArcSwap;
2use chrono::Utc;
3use parking_lot::{Mutex, MutexGuard};
4use std::collections::HashMap;
5use std::sync::{Arc, OnceLock};
6use std::time::Instant;
7
8use crate::data_store_interface::{DataStoreCacheKeys, DataStoreTrait, RequestPath};
9use crate::evaluation::evaluator::SpecType;
10use crate::gcir::evaluation_plan::GcirEvaluationPlan;
11use crate::global_configs::GlobalConfigs;
12use crate::hashing::HashUtil;
13use crate::id_lists_adapter::{IdList, IdListsUpdateListener};
14use crate::interned_string::InternedString;
15use crate::interned_values::InternedStore;
16use crate::macros::LOCK_TIMEOUT;
17use crate::networking::ResponseData;
18use crate::observability::observability_client_adapter::{MetricType, ObservabilityEvent};
19use crate::observability::ops_stats::{OpsStatsForInstance, OPS_STATS};
20use crate::observability::sdk_errors_observer::ErrorBoundaryEvent;
21use crate::sdk_event_emitter::{SdkEvent, SdkEventEmitter};
22use crate::specs_response::proto_specs::{deserialize_protobuf_for_store, ProtobufUpdate};
23use crate::specs_response::spec_types::{SpecsResponseFull, SpecsResponseNoUpdates};
24use crate::specs_response::specs_hash_map::{track_spec_decodes, SpecDecodeStats};
25use crate::utils::{get_loggable_sdk_key, try_release_unused_heap_memory};
26use crate::{
27 log_d, log_e, log_error_to_statsig_and_console, log_w, SpecsFormat, SpecsInfo, SpecsSource,
28 SpecsUpdate, SpecsUpdateListener, StatsigErr, StatsigOptions, StatsigRuntime,
29};
30
31#[derive(Clone)]
32pub struct SpecStoreData {
33 pub source: SpecsSource,
34 pub source_api: Option<String>,
35 pub time_received_at: Option<u64>,
36 pub snapshot: Arc<SpecsResponseFull>,
37 pub id_lists: Arc<HashMap<String, IdList>>,
38 pub gcir_evaluation_plan: Arc<OnceLock<GcirEvaluationPlan>>,
39 sync_cursor: ConfigSyncCursor,
40 spec_decode_stats: SpecDecodeStats,
42}
43
44#[derive(Clone, Default)]
45struct ConfigSyncCursor {
46 lcut: u64,
47 checksum: Option<String>,
48}
49
50impl SpecStoreData {
51 pub(crate) fn gcir_evaluation_plan(&self, hashing: &HashUtil) -> &GcirEvaluationPlan {
52 self.gcir_evaluation_plan
53 .get_or_init(|| GcirEvaluationPlan::new(self.snapshot.as_ref(), hashing))
54 }
55
56 pub(crate) fn lcut(&self) -> u64 {
57 self.sync_cursor.lcut
58 }
59
60 fn checksum(&self) -> Option<&str> {
61 self.sync_cursor.checksum.as_deref()
62 }
63}
64
65fn cursor_is_stale_or_duplicate(data: &SpecStoreData, lcut: u64, checksum: &str) -> bool {
66 lcut < data.lcut() || (lcut == data.lcut() && data.checksum() == Some(checksum))
67}
68
69fn proto_response_lcut(data: &ResponseData) -> Option<u64> {
70 data.get_header_ref("x-since-time")?.parse().ok()
71}
72
73fn proto_response_matches_cursor(data: &ResponseData, current: &SpecStoreData) -> bool {
74 let Some(lcut) = proto_response_lcut(data) else {
75 return false;
76 };
77 let Some(checksum) = data.get_header_ref("x-checksum") else {
78 return false;
79 };
80
81 lcut == current.lcut() && current.checksum() == Some(checksum)
82}
83
84const TAG: &str = stringify!(SpecStore);
85const RESPONSE_TYPE_TAG: &str = "response_type";
86const DELTAS_USED_HEADER: &str = "x-deltas-used";
87
88#[derive(Clone, Copy)]
89enum ConfigResponseType {
90 Delta,
91 Full,
92 NoUpdate,
93}
94
95impl ConfigResponseType {
96 fn from_applied_response_data(data: &ResponseData) -> Self {
97 if data.get_header_ref(DELTAS_USED_HEADER).is_some() {
98 Self::Delta
99 } else {
100 Self::Full
101 }
102 }
103
104 fn as_str(self) -> &'static str {
105 match self {
106 Self::Delta => "delta",
107 Self::Full => "full",
108 Self::NoUpdate => "no_update",
109 }
110 }
111}
112
113const CONFIG_PROTO_UPDATE_COUNT_METRIC: &str = "config_proto_update.count";
114const CONFIG_PROTO_UPDATE_LATENCY_METRIC: &str = "config_proto_update.latency";
115const CONFIG_PROTO_UPDATE_OUTCOME_TAG: &str = "outcome";
116const CONFIG_PROTO_UPDATE_CURSOR_ONLY: &str = "cursor_only";
117const CONFIG_PROTO_UPDATE_DUPLICATE: &str = "duplicate";
118const CONFIG_PROTO_UPDATE_MATERIALIZED: &str = "materialized";
119const INTERNED_MMAP_SPEC_DECODE_COUNT_METRIC: &str = "interned_mmap.spec_decode.count";
120
121pub struct SpecStore {
122 data: ArcSwap<SpecStoreData>,
123 update_lock: Mutex<()>,
124
125 data_store_keys: DataStoreCacheKeys,
126 data_store: Option<Arc<dyn DataStoreTrait>>,
127 statsig_runtime: Arc<StatsigRuntime>,
128 ops_stats: Arc<OpsStatsForInstance>,
129 global_configs: Arc<GlobalConfigs>,
130 event_emitter: Arc<SdkEventEmitter>,
131 loggable_sdk_key: String,
132}
133
134impl SpecStore {
135 #[must_use]
136 pub fn new(
137 sdk_key: &str,
138 data_store_key: String,
139 statsig_runtime: Arc<StatsigRuntime>,
140 event_emitter: Arc<SdkEventEmitter>,
141 options: Option<&StatsigOptions>,
142 ) -> SpecStore {
143 let mut data_store = None;
144 if let Some(options) = options {
145 data_store = options.data_store.clone();
146 }
147
148 let sdk_instance_id = options
149 .map(|opts| opts.get_sdk_instance_id(sdk_key))
150 .unwrap_or(sdk_key);
151
152 SpecStore {
153 data_store_keys: DataStoreCacheKeys::from_selected_key(&data_store_key),
154 data: ArcSwap::from_pointee(SpecStoreData {
155 snapshot: Arc::new(SpecsResponseFull::default()),
156 time_received_at: None,
157 source: SpecsSource::Uninitialized,
158 source_api: None,
159 id_lists: Arc::new(HashMap::new()),
160 gcir_evaluation_plan: Arc::new(OnceLock::new()),
161 sync_cursor: ConfigSyncCursor::default(),
162 spec_decode_stats: SpecDecodeStats::default(),
163 }),
164 update_lock: Mutex::new(()),
165 event_emitter,
166 data_store,
167 statsig_runtime,
168 ops_stats: OPS_STATS.get_for_instance(sdk_instance_id),
169 global_configs: GlobalConfigs::get_instance(sdk_instance_id),
170 loggable_sdk_key: get_loggable_sdk_key(sdk_key),
171 }
172 }
173
174 pub fn set_source(&self, source: SpecsSource) {
175 {
176 let Some(_update_guard) = self.try_lock_for_update("set_source") else {
177 return;
178 };
179
180 let mut next_data = self.load_data().as_ref().clone();
181 next_data.source = source.clone();
182 self.publish_data(next_data);
183 }
184
185 log_d!(TAG, "Source Changed ({:?})", source);
186 }
187
188 pub fn get_current_values(&self) -> Option<SpecsResponseFull> {
189 let data = self.load_data();
190 let json = serde_json::to_string(data.snapshot.as_ref()).ok()?;
191 let mut values = serde_json::from_str::<SpecsResponseFull>(&json).ok()?;
192 values.time = data.lcut();
193 values.checksum = data.sync_cursor.checksum.clone();
194 Some(values)
195 }
196
197 pub fn get_fields_used_for_entity(
198 &self,
199 entity_name: &str,
200 entity_type: SpecType,
201 ) -> Vec<String> {
202 let data = self.load_data();
203 let entities = match entity_type {
204 SpecType::Gate => &data.snapshot.feature_gates,
205 SpecType::DynamicConfig | SpecType::Experiment => &data.snapshot.dynamic_configs,
206 SpecType::Layer => &data.snapshot.layer_configs,
207 SpecType::ParameterStore => return vec![],
208 };
209
210 let entity_name = InternedString::from_str_ref(entity_name);
211 let entity = entities.get(&entity_name);
212
213 entity
214 .map(|entity| entity.view().fields_used())
215 .unwrap_or_default()
216 }
217
218 pub fn unperformant_keys_entity_filter(
219 &self,
220 top_level_key: &str,
221 entity_type: &str,
222 ) -> Vec<String> {
223 let data = self.load_data();
224 if top_level_key == "param_stores" {
225 match &data.snapshot.param_stores {
226 Some(param_stores) => {
227 return param_stores
228 .keys()
229 .map(|k| k.unperformant_to_string())
230 .collect()
231 }
232 None => return vec![],
233 }
234 }
235
236 let values = match top_level_key {
237 "feature_gates" => &data.snapshot.feature_gates,
238 "dynamic_configs" => &data.snapshot.dynamic_configs,
239 "layer_configs" => &data.snapshot.layer_configs,
240 _ => {
241 log_e!(TAG, "Invalid top level key: {}", top_level_key);
242 return vec![];
243 }
244 };
245
246 if entity_type == "*" {
247 return values.keys().map(|k| k.unperformant_to_string()).collect();
248 }
249
250 values
251 .iter()
252 .filter(|(_, v)| v.view().entity().as_str() == entity_type)
253 .map(|(k, _)| k.unperformant_to_string())
254 .collect()
255 }
256
257 pub fn set_values(&self, mut specs_update: SpecsUpdate) -> Result<(), StatsigErr> {
258 let update_started_at = Instant::now();
259 let locked_result = {
264 let Some(_update_guard) = self.try_lock_for_update("set_values") else {
265 return Err(StatsigErr::LockFailure(
266 "Failed to acquire spec store update lock for set_values".to_string(),
267 ));
268 };
269
270 let prep_result = self.specs_update_prep(&mut specs_update)?;
271
272 match prep_result {
273 PrepResult::HasUpdates {
274 values,
275 response_format,
276 is_delta,
277 spec_decode_stats,
278 } => {
279 let apply_result =
280 self.specs_update_apply(values, &specs_update, spec_decode_stats)?;
281 Ok(LockedSetValuesResult::Applied(
282 response_format,
283 apply_result,
284 ConfigResponseType::from_applied_response_data(&specs_update.data),
285 is_delta,
286 ))
287 }
288 PrepResult::CursorOnly { lcut, checksum } => {
289 self.specs_cursor_update_apply(lcut, checksum, &specs_update);
290 Ok(LockedSetValuesResult::NoSemanticUpdate(
291 specs_update.source.clone(),
292 specs_update.source_api.clone(),
293 ConfigResponseType::Delta,
294 ))
295 }
296 PrepResult::CurrentValuesNewer => Ok(LockedSetValuesResult::CurrentValuesNewer),
297 PrepResult::Duplicate => Ok(LockedSetValuesResult::Duplicate),
298 PrepResult::NoUpdates => Ok(LockedSetValuesResult::NoSemanticUpdate(
299 specs_update.source.clone(),
300 specs_update.source_api.clone(),
301 ConfigResponseType::NoUpdate,
302 )),
303 }
304 }
305 .map_err(|e: StatsigErr| {
306 log_error_to_statsig_and_console!(self.ops_stats, TAG, e);
307 e
308 })?;
309
310 let (response_format, apply_result, response_type, is_delta) = match locked_result {
311 LockedSetValuesResult::Applied(
312 response_format,
313 apply_result,
314 response_type,
315 is_delta,
316 ) => {
317 if is_delta {
318 self.ops_stats_log_proto_update(CONFIG_PROTO_UPDATE_MATERIALIZED);
319 }
320 (response_format, apply_result, response_type, is_delta)
321 }
322 LockedSetValuesResult::NoSemanticUpdate(source, source_api, response_type) => {
323 if matches!(response_type, ConfigResponseType::Delta) {
324 self.ops_stats_log_proto_update(CONFIG_PROTO_UPDATE_CURSOR_ONLY);
325 self.ops_stats_log_proto_update_latency(
326 CONFIG_PROTO_UPDATE_CURSOR_ONLY,
327 update_started_at.elapsed().as_secs_f64() * 1000.0,
328 );
329 }
330 self.ops_stats_log_no_update(source, source_api, response_type);
331 return Ok(());
332 }
333 LockedSetValuesResult::CurrentValuesNewer => return Ok(()),
334 LockedSetValuesResult::Duplicate => {
335 self.ops_stats_log_proto_update(CONFIG_PROTO_UPDATE_DUPLICATE);
336 return Ok(());
337 }
338 };
339
340 self.ops_stats_log_interned_mmap_spec_decode(apply_result.spec_decode_stats);
341
342 try_release_unused_heap_memory();
343
344 let notify_result = self
347 .specs_update_notify(response_format, response_type, specs_update, apply_result)
348 .map_err(|e| {
349 log_error_to_statsig_and_console!(self.ops_stats, TAG, e);
350 e
351 });
352
353 if is_delta {
354 self.ops_stats_log_proto_update_latency(
355 CONFIG_PROTO_UPDATE_MATERIALIZED,
356 update_started_at.elapsed().as_secs_f64() * 1000.0,
357 );
358 }
359
360 notify_result
361 }
362}
363
364enum PrepResult {
367 HasUpdates {
368 values: Box<SpecsResponseFull>,
369 response_format: SpecsFormat,
370 is_delta: bool,
371 spec_decode_stats: SpecDecodeStats,
372 },
373 CursorOnly {
374 lcut: u64,
375 checksum: String,
376 },
377 Duplicate,
378 NoUpdates,
379 CurrentValuesNewer,
380}
381
382enum LockedSetValuesResult {
383 Applied(SpecsFormat, ApplyResult, ConfigResponseType, bool),
384 NoSemanticUpdate(SpecsSource, Option<String>, ConfigResponseType),
385 Duplicate,
386 CurrentValuesNewer,
387}
388
389enum DeserializedSpecs {
390 Materialized {
391 values: Box<SpecsResponseFull>,
392 is_delta: bool,
393 },
394 CursorOnly {
395 lcut: u64,
396 checksum: String,
397 },
398}
399
400struct ApplyResult {
401 prev_source: SpecsSource,
402 prev_lcut: u64,
403 time_received_at: u64,
404 notification: SpecUpdateNotification,
405 spec_decode_stats: SpecDecodeStats,
406}
407
408struct SpecUpdateNotification {
409 source: SpecsSource,
410 source_api: Option<String>,
411 values: Arc<SpecsResponseFull>,
412 lcut: u64,
413 checksum: Option<String>,
414}
415
416impl SpecStore {
417 pub(crate) fn load_data(&self) -> Arc<SpecStoreData> {
418 self.data.load_full()
419 }
420
421 fn publish_data(&self, data: SpecStoreData) {
422 self.data.store(Arc::new(data));
423 }
424
425 fn try_lock_for_update(&self, operation: &str) -> Option<MutexGuard<'_, ()>> {
426 match self.update_lock.try_lock_for(LOCK_TIMEOUT) {
427 Some(guard) => Some(guard),
428 None => {
429 log_e!(
430 TAG,
431 "Failed to acquire spec store update lock for {}",
432 operation
433 );
434 None
435 }
436 }
437 }
438
439 fn specs_update_prep(&self, specs_update: &mut SpecsUpdate) -> Result<PrepResult, StatsigErr> {
440 if specs_update.has_updates == Some(false) {
441 return Ok(PrepResult::NoUpdates);
442 }
443
444 let response_format = self.get_spec_response_format(specs_update);
445 let read_data = self.load_data();
446 if matches!(response_format, SpecsFormat::Protobuf)
447 && proto_response_lcut(&specs_update.data).is_some_and(|lcut| lcut < read_data.lcut())
448 {
449 return Ok(PrepResult::CurrentValuesNewer);
450 }
451 if matches!(response_format, SpecsFormat::Protobuf)
452 && proto_response_matches_cursor(&specs_update.data, &read_data)
453 {
454 return Ok(PrepResult::Duplicate);
455 }
456 let first_deserialize_result =
458 self.deserialize_specs_data(&read_data, &response_format, &mut specs_update.data);
459
460 let first_deserialize_error = match first_deserialize_result {
461 Ok((
462 DeserializedSpecs::Materialized {
463 values: next_values,
464 is_delta,
465 },
466 spec_decode_stats,
467 )) => {
468 if self.are_current_values_newer(&read_data, &next_values) {
469 return Ok(PrepResult::CurrentValuesNewer);
470 }
471
472 if next_values.has_updates {
473 return Ok(PrepResult::HasUpdates {
474 values: next_values,
475 response_format,
476 is_delta,
477 spec_decode_stats,
478 });
479 }
480
481 None
482 }
483 Ok((DeserializedSpecs::CursorOnly { lcut, checksum }, _)) => {
484 if cursor_is_stale_or_duplicate(&read_data, lcut, &checksum) {
485 return Ok(PrepResult::CurrentValuesNewer);
486 }
487
488 return Ok(PrepResult::CursorOnly { lcut, checksum });
489 }
490 Err(e) => Some(e),
491 };
492
493 let second_deserialize_result = specs_update
495 .data
496 .deserialize_into::<SpecsResponseNoUpdates>();
497
498 let second_deserialize_error = match second_deserialize_result {
499 Ok(result) => {
500 if !result.has_updates {
501 return Ok(PrepResult::NoUpdates);
502 }
503
504 None
505 }
506 Err(e) => Some(e),
507 };
508
509 let error = first_deserialize_error
510 .or(second_deserialize_error)
511 .unwrap_or_else(|| {
512 StatsigErr::JsonParseError("SpecsResponse".to_string(), "Unknown error".to_string())
513 });
514
515 Err(error)
516 }
517
518 fn specs_update_apply(
519 &self,
520 next_values: Box<SpecsResponseFull>,
521 specs_update: &SpecsUpdate,
522 spec_decode_stats: SpecDecodeStats,
523 ) -> Result<ApplyResult, StatsigErr> {
524 self.try_update_global_configs(&next_values);
526
527 let data = self.load_data();
528 let prev_source = data.source.clone();
529 let prev_lcut = data.lcut();
530 let time_received_at = Utc::now().timestamp_millis() as u64;
531 let values: Arc<SpecsResponseFull> = Arc::from(next_values);
532 let sync_cursor = ConfigSyncCursor {
533 lcut: values.time,
534 checksum: values.checksum.clone(),
535 };
536 let notification = SpecUpdateNotification {
537 source: specs_update.source.clone(),
538 source_api: specs_update.source_api.clone(),
539 lcut: values.time,
540 checksum: values
541 .checksum
542 .as_ref()
543 .map(|value| value.as_str().to_string()),
544 values: values.clone(),
545 };
546
547 self.publish_data(SpecStoreData {
548 source: specs_update.source.clone(),
549 source_api: specs_update.source_api.clone(),
550 time_received_at: Some(time_received_at),
551 snapshot: values,
552 id_lists: data.id_lists.clone(),
553 gcir_evaluation_plan: Arc::new(OnceLock::new()),
554 sync_cursor,
555 spec_decode_stats,
556 });
557
558 Ok(ApplyResult {
559 prev_source,
560 prev_lcut,
561 time_received_at,
562 notification,
563 spec_decode_stats,
564 })
565 }
566
567 fn specs_cursor_update_apply(&self, lcut: u64, checksum: String, specs_update: &SpecsUpdate) {
568 let data = self.load_data();
569 let mut next_data = data.as_ref().clone();
570 next_data.source = specs_update.source.clone();
571 next_data.source_api = specs_update.source_api.clone();
572 next_data.time_received_at = Some(Utc::now().timestamp_millis() as u64);
573 next_data.sync_cursor = ConfigSyncCursor {
574 lcut,
575 checksum: Some(checksum),
576 };
577 self.publish_data(next_data);
578 }
579
580 fn specs_update_notify(
581 &self,
582 response_format: SpecsFormat,
583 response_type: ConfigResponseType,
584 specs_update: SpecsUpdate,
585 apply_result: ApplyResult,
586 ) -> Result<(), StatsigErr> {
587 let SpecsUpdate { data, .. } = specs_update;
588 let ApplyResult {
589 prev_source,
590 prev_lcut,
591 time_received_at,
592 notification,
593 spec_decode_stats: _,
594 } = apply_result;
595 let SpecUpdateNotification {
596 source,
597 source_api,
598 values,
599 lcut,
600 checksum,
601 } = notification;
602
603 self.emit_specs_updated_sdk_event(&source, &source_api, values.as_ref());
604 if let Some(sdk_configs) = &values.sdk_configs {
605 self.emit_internal_sdk_configs_updated_sdk_event(sdk_configs);
606 }
607
608 self.try_update_data_store(
610 &source,
611 data,
612 time_received_at,
613 checksum,
614 matches!(response_format, SpecsFormat::Protobuf),
615 );
616
617 self.ops_stats_log_config_propagation_diff(
618 lcut,
619 prev_lcut,
620 &source,
621 &prev_source,
622 source_api,
623 response_format,
624 response_type,
625 );
626
627 Ok(())
628 }
629
630 fn deserialize_specs_data(
631 &self,
632 current_data: &SpecStoreData,
633 response_format: &SpecsFormat,
634 response_data: &mut ResponseData,
635 ) -> Result<(DeserializedSpecs, SpecDecodeStats), StatsigErr> {
636 let (result, spec_decode_stats) = track_spec_decodes(|| {
637 let mut next_values = Box::new(SpecsResponseFull::default());
638
639 match response_format {
640 SpecsFormat::Protobuf => {
641 let update = deserialize_protobuf_for_store(
642 &self.ops_stats,
643 current_data.snapshot.as_ref(),
644 current_data.spec_decode_stats,
645 next_values.as_mut(),
646 response_data,
647 )?;
648 match update {
649 ProtobufUpdate::Materialized { is_delta } => {
650 Ok(DeserializedSpecs::Materialized {
651 values: next_values,
652 is_delta,
653 })
654 }
655 ProtobufUpdate::CursorOnly { lcut, checksum } => {
656 Ok(DeserializedSpecs::CursorOnly { lcut, checksum })
657 }
658 }
659 }
660 SpecsFormat::Json => {
661 response_data.deserialize_in_place(next_values.as_mut())?;
662 Ok(DeserializedSpecs::Materialized {
663 values: next_values,
664 is_delta: false,
665 })
666 }
667 }
668 });
669
670 Ok((result?, spec_decode_stats))
671 }
672
673 fn emit_specs_updated_sdk_event(
674 &self,
675 source: &SpecsSource,
676 source_api: &Option<String>,
677 values: &SpecsResponseFull,
678 ) {
679 self.event_emitter.emit(SdkEvent::SpecsUpdated {
680 source,
681 source_api,
682 values,
683 });
684 }
685
686 fn emit_internal_sdk_configs_updated_sdk_event(
687 &self,
688 sdk_configs: &HashMap<String, crate::DynamicValue>,
689 ) {
690 self.event_emitter
691 .emit(SdkEvent::InternalSdkConfigsUpdated { sdk_configs });
692 }
693
694 fn get_spec_response_format(&self, update: &SpecsUpdate) -> SpecsFormat {
695 let content_type = update.data.get_header_ref("content-type");
696 if content_type.map(|s| s.as_str().contains("application/octet-stream")) != Some(true) {
697 return SpecsFormat::Json;
698 }
699
700 let content_encoding = update.data.get_header_ref("content-encoding");
701 if content_encoding.map(|s| s.as_str().contains("statsig-br")) != Some(true) {
702 return SpecsFormat::Json;
703 }
704
705 SpecsFormat::Protobuf
706 }
707
708 fn try_update_global_configs(&self, dcs: &SpecsResponseFull) {
709 if let Some(diagnostics) = &dcs.diagnostics {
710 self.global_configs
711 .set_diagnostics_sampling_rates(diagnostics.clone());
712 }
713
714 if let Some(sdk_configs) = &dcs.sdk_configs {
715 self.global_configs.set_sdk_configs(sdk_configs.clone());
716 }
717
718 if let Some(sdk_flags) = &dcs.sdk_flags {
719 self.global_configs.set_sdk_flags(sdk_flags.clone());
720 }
721 }
722
723 fn try_update_data_store(
724 &self,
725 source: &SpecsSource,
726 mut data: ResponseData,
727 now: u64,
728 checksum: Option<String>,
729 is_protobuf: bool,
730 ) {
731 if source != &SpecsSource::Network {
732 return;
733 }
734
735 if data.get_header_ref("x-deltas-used").is_some() {
736 log_d!(
737 TAG,
738 "Skipping data store write for delta response identified by x-deltas-used header"
739 );
740 return;
741 }
742
743 let data_store = match &self.data_store {
744 Some(data_store) => data_store.clone(),
745 None => return,
746 };
747
748 let data_store_key = if is_protobuf {
749 self.data_store_keys.statsig_br.clone()
750 } else {
751 self.data_store_keys.plain_text.clone()
752 };
753
754 let spawn_result = self.statsig_runtime.spawn(
755 "spec_store_update_data_store",
756 move |_shutdown_notif| async move {
757 let data_bytes = match data.read_to_bytes() {
758 Ok(bytes) => bytes,
759 Err(e) => {
760 log_e!(TAG, "Failed to read data as bytes: {}", e);
761 return;
762 }
763 };
764
765 write_specs_to_data_store(
766 data_store,
767 data_store_key,
768 data_bytes,
769 checksum,
770 now,
771 is_protobuf,
772 )
773 .await;
774 },
775 );
776
777 if let Err(e) = spawn_result {
778 log_e!(
779 TAG,
780 "Failed to spawn spec store update data store task: {e}"
781 );
782 }
783 }
784
785 fn are_current_values_newer(
786 &self,
787 data: &SpecStoreData,
788 next_values: &SpecsResponseFull,
789 ) -> bool {
790 let curr_checksum = data.checksum().unwrap_or_default();
791 let new_checksum = next_values.checksum.as_deref().unwrap_or_default();
792
793 let cached_time_is_newer = data.lcut() > 0 && data.lcut() > next_values.time;
794 let checksums_match = !curr_checksum.is_empty() && curr_checksum == new_checksum;
795
796 if cached_time_is_newer || checksums_match {
797 log_d!(
798 TAG,
799 "Received values for [time: {}, checksum: {}], but currently has values for [time: {}, checksum: {}]. Ignoring values.",
800 next_values.time,
801 new_checksum,
802 data.lcut(),
803 curr_checksum,
804 );
805 return true;
806 }
807
808 false
809 }
810}
811
812async fn write_specs_to_data_store(
813 data_store: Arc<dyn DataStoreTrait>,
814 data_store_key: String,
815 data_bytes: Vec<u8>,
816 checksum: Option<String>,
817 now: u64,
818 is_protobuf: bool,
819) {
820 match data_store
821 .set_bytes(&data_store_key, &data_bytes, Some(now), checksum)
822 .await
823 {
824 Ok(()) => return,
825 Err(e @ StatsigErr::BytesNotImplemented) if is_protobuf => {
826 if data_store
827 .support_polling_updates_for(RequestPath::RulesetsV2)
828 .await
829 {
830 log_w!(
831 TAG,
832 "Failed to write protobuf specs to data store as bytes. Protobuf specs cannot fall back to string writes: {}",
833 e
834 );
835 }
836 return;
837 }
838 Err(e @ StatsigErr::BytesNotImplemented) => {
839 log_w!(
840 TAG,
841 "Data store bytes write is not implemented. Falling back to string write: {}",
842 e
843 );
844 }
845 Err(e) => {
846 log_w!(TAG, "Failed to write specs to data store as bytes: {}", e);
847 return;
848 }
849 }
850
851 let data_string = match String::from_utf8(data_bytes) {
852 Ok(s) => s,
853 Err(e) => {
854 log_w!(
855 TAG,
856 "Skipping data store string write because payload is not valid UTF-8: {}",
857 e
858 );
859 return;
860 }
861 };
862
863 if let Err(e) = data_store
864 .set(&data_store_key, &data_string, Some(now))
865 .await
866 {
867 log_w!(TAG, "Failed to write specs to data store as string: {}", e);
868 }
869}
870
871impl SpecStore {
874 fn ops_stats_log_interned_mmap_spec_decode(&self, stats: SpecDecodeStats) {
875 if stats.total == 0 {
876 return;
877 }
878
879 let (source, reason) = if stats.mmap == stats.total {
880 ("mmap", "preloaded")
881 } else if stats.mmap > 0 {
882 ("mixed", "partial_match")
883 } else if InternedStore::has_preloaded_mmap_v2() {
884 ("owned", "no_match")
885 } else {
886 ("owned", "spec_preload_unavailable")
887 };
888
889 self.ops_stats.log(ObservabilityEvent::new_event(
890 MetricType::Increment,
891 INTERNED_MMAP_SPEC_DECODE_COUNT_METRIC.to_string(),
892 1.0,
893 Some(HashMap::from([
894 ("source".to_string(), source.to_string()),
895 ("reason".to_string(), reason.to_string()),
896 ("sdk_key".to_string(), self.loggable_sdk_key.clone()),
897 ])),
898 ));
899 }
900
901 fn ops_stats_log_no_update(
902 &self,
903 source: SpecsSource,
904 source_api: Option<String>,
905 response_type: ConfigResponseType,
906 ) {
907 log_d!(TAG, "No Updates");
908 self.ops_stats.log(ObservabilityEvent::new_event(
909 MetricType::Increment,
910 "config_no_update".to_string(),
911 1.0,
912 Some(HashMap::from([
913 ("source".to_string(), source.to_string()),
914 ("source_api".to_string(), source_api.unwrap_or_default()),
915 (
916 RESPONSE_TYPE_TAG.to_string(),
917 response_type.as_str().to_string(),
918 ),
919 ])),
920 ));
921 }
922
923 fn ops_stats_log_proto_update(&self, outcome: &str) {
924 self.ops_stats.log(ObservabilityEvent::new_event(
925 MetricType::Increment,
926 CONFIG_PROTO_UPDATE_COUNT_METRIC.to_string(),
927 1.0,
928 Some(HashMap::from([(
929 CONFIG_PROTO_UPDATE_OUTCOME_TAG.to_string(),
930 outcome.to_string(),
931 )])),
932 ));
933 }
934
935 fn ops_stats_log_proto_update_latency(&self, outcome: &str, duration_ms: f64) {
936 self.ops_stats.log(ObservabilityEvent::new_event(
937 MetricType::Dist,
938 CONFIG_PROTO_UPDATE_LATENCY_METRIC.to_string(),
939 duration_ms,
940 Some(HashMap::from([(
941 CONFIG_PROTO_UPDATE_OUTCOME_TAG.to_string(),
942 outcome.to_string(),
943 )])),
944 ));
945 }
946
947 #[allow(clippy::too_many_arguments)]
948 fn ops_stats_log_config_propagation_diff(
949 &self,
950 lcut: u64,
951 prev_lcut: u64,
952 source: &SpecsSource,
953 prev_source: &SpecsSource,
954 source_api: Option<String>,
955 response_format: SpecsFormat,
956 response_type: ConfigResponseType,
957 ) {
958 let delay = (Utc::now().timestamp_millis() as u64).saturating_sub(lcut);
959 log_d!(TAG, "Updated ({:?})", source);
960
961 if *prev_source == SpecsSource::Uninitialized || *prev_source == SpecsSource::Loading {
962 return;
963 }
964
965 self.ops_stats.log(ObservabilityEvent::new_event(
966 MetricType::Dist,
967 "config_propagation_diff".to_string(),
968 delay as f64,
969 Some(HashMap::from([
970 ("source".to_string(), source.to_string()),
971 ("lcut".to_string(), lcut.to_string()),
972 ("prev_lcut".to_string(), prev_lcut.to_string()),
973 ("source_api".to_string(), source_api.unwrap_or_default()),
974 ("sdk_key".to_string(), self.loggable_sdk_key.clone()),
975 (
976 "response_format".to_string(),
977 Into::<&str>::into(&response_format).to_string(),
978 ),
979 (
980 RESPONSE_TYPE_TAG.to_string(),
981 response_type.as_str().to_string(),
982 ),
983 ])),
984 ));
985 }
986}
987
988impl SpecsUpdateListener for SpecStore {
991 fn did_receive_specs_update(&self, update: SpecsUpdate) -> Result<(), StatsigErr> {
992 self.set_values(update)
993 }
994
995 fn get_current_specs_info(&self) -> SpecsInfo {
996 let data = self.load_data();
997 SpecsInfo {
998 lcut: Some(data.lcut()),
999 checksum: data.sync_cursor.checksum.clone(),
1000 source: data.source.clone(),
1001 source_api: data.source_api.clone(),
1002 }
1003 }
1004}
1005
1006impl IdListsUpdateListener for SpecStore {
1009 fn get_current_id_list_metadata(
1010 &self,
1011 ) -> HashMap<String, crate::id_lists_adapter::IdListMetadata> {
1012 let data = self.load_data();
1013 data.id_lists
1014 .iter()
1015 .map(|(key, list)| (key.clone(), list.metadata.clone()))
1016 .collect()
1017 }
1018
1019 fn did_receive_id_list_updates(
1020 &self,
1021 updates: HashMap<String, crate::id_lists_adapter::IdListUpdate>,
1022 ) {
1023 let Some(_update_guard) = self.try_lock_for_update("did_receive_id_list_updates") else {
1024 return;
1025 };
1026
1027 let data = self.load_data();
1028 let mut id_lists = data.id_lists.as_ref().clone();
1029
1030 id_lists.retain(|name, _| updates.contains_key(name));
1032
1033 for (list_name, update) in updates {
1034 if let Some(entry) = id_lists.get_mut(&list_name) {
1035 entry.apply_update(update);
1037 } else {
1038 let mut list = IdList::new(update.new_metadata.clone());
1040 list.apply_update(update);
1041 id_lists.insert(list_name, list);
1042 }
1043 }
1044
1045 let mut next_data = data.as_ref().clone();
1046 next_data.id_lists = Arc::new(id_lists);
1047 self.publish_data(next_data);
1048 }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use super::{
1054 ConfigResponseType, ConfigSyncCursor, SpecDecodeStats, SpecStoreData, DELTAS_USED_HEADER,
1055 };
1056 use crate::hashing::HashUtil;
1057 use crate::networking::ResponseData;
1058 use crate::specs_response::spec_types::SpecsResponseFull;
1059 use crate::SpecsSource;
1060 use std::collections::HashMap;
1061 use std::sync::{Arc, OnceLock};
1062
1063 #[test]
1064 fn applied_response_type_uses_delta_header() {
1065 let delta_data = ResponseData::from_bytes_with_headers(
1066 Vec::new(),
1067 Some(HashMap::from([(
1068 DELTAS_USED_HEADER.to_string(),
1069 "true".to_string(),
1070 )])),
1071 );
1072 let full_data = ResponseData::from_bytes(Vec::new());
1073
1074 assert_eq!(
1075 ConfigResponseType::from_applied_response_data(&delta_data).as_str(),
1076 "delta"
1077 );
1078 assert_eq!(
1079 ConfigResponseType::from_applied_response_data(&full_data).as_str(),
1080 "full"
1081 );
1082 }
1083
1084 #[test]
1085 fn spec_store_data_reuses_gcir_evaluation_plan_cache_across_clones() {
1086 let data = SpecStoreData {
1087 source: SpecsSource::Network,
1088 source_api: Some("/v2/download_config_specs".to_string()),
1089 time_received_at: Some(1),
1090 snapshot: Arc::new(SpecsResponseFull::default()),
1091 id_lists: Arc::new(HashMap::new()),
1092 gcir_evaluation_plan: Arc::new(OnceLock::new()),
1093 sync_cursor: ConfigSyncCursor::default(),
1094 spec_decode_stats: SpecDecodeStats::default(),
1095 };
1096 let hashing = HashUtil::new();
1097
1098 let first = data.gcir_evaluation_plan(&hashing);
1099 let cloned = data.clone();
1100 let second = cloned.gcir_evaluation_plan(&hashing);
1101
1102 assert!(std::ptr::eq(first, second));
1103 }
1104}