1use std::cmp::Ordering;
6use std::collections::hash_map::Entry;
7use std::collections::HashMap;
8use std::fs::{create_dir_all, File, OpenOptions};
9use std::io::BufReader;
10use std::io::Write;
11use std::io::{self, BufRead};
12use std::path::{Path, PathBuf};
13use std::sync::{atomic, Arc, Mutex, RwLock};
14use std::{fs, mem};
15
16use chrono::{DateTime, FixedOffset, Utc};
17
18use malloc_size_of::MallocSizeOf;
19use malloc_size_of_derive::MallocSizeOf;
20use serde::{Deserialize, Serialize};
21use serde_json::{json, Value as JsonValue};
22
23use crate::common_metric_data::CommonMetricDataInternal;
24use crate::error_recording::{record_error, ErrorType};
25use crate::metrics::{DatetimeMetric, TimeUnit};
26use crate::session::{EventSessionContext, SessionMetadata};
27use crate::storage::INTERNAL_STORAGE;
28use crate::util::get_iso_time_string;
29use crate::Glean;
30use crate::Result;
31use crate::{CommonMetricData, CounterMetric, Lifetime};
32
33#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, MallocSizeOf)]
35#[cfg_attr(test, derive(Default))]
36pub struct RecordedEvent {
37 pub timestamp: u64,
41
42 pub category: String,
46
47 pub name: String,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
56 pub extra: Option<HashMap<String, String>>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
63 #[serde(default)]
64 pub session: Option<SessionMetadata>,
65}
66
67#[derive(
69 Debug, Clone, Deserialize, Serialize, PartialEq, Eq, malloc_size_of_derive::MallocSizeOf,
70)]
71struct StoredEvent {
72 #[serde(flatten)]
73 event: RecordedEvent,
74
75 #[serde(default)]
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub execution_counter: Option<i32>,
82}
83
84#[derive(Debug)]
106pub struct EventDatabase {
107 pub path: PathBuf,
109 event_stores: RwLock<HashMap<String, Vec<StoredEvent>>>,
111 event_store_files: RwLock<HashMap<String, Arc<File>>>,
112 file_lock: Mutex<()>,
114 events_pings_submitted: atomic::AtomicUsize,
117}
118
119impl MallocSizeOf for EventDatabase {
120 fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
121 let mut n = 0;
122 n += self.event_stores.read().unwrap().size_of(ops);
123
124 let map = self.event_store_files.read().unwrap();
125 for store_name in map.keys() {
126 n += store_name.size_of(ops);
127 n += mem::size_of::<File>();
129 }
130 n
131 }
132}
133
134impl EventDatabase {
135 pub fn new(data_path: &Path) -> Result<Self> {
142 let path = data_path.join("events");
143 create_dir_all(&path)?;
144
145 Ok(Self {
146 path,
147 event_stores: RwLock::new(HashMap::new()),
148 event_store_files: RwLock::new(HashMap::new()),
149 file_lock: Mutex::new(()),
150 events_pings_submitted: atomic::AtomicUsize::new(0),
151 })
152 }
153
154 pub fn flush_pending_events_on_startup(
180 &self,
181 glean: &Glean,
182 trim_data_to_registered_pings: bool,
183 ) -> bool {
184 match self.load_events_from_disk(glean, trim_data_to_registered_pings) {
185 Ok(_) => {
186 let stores_with_events: Vec<String> = {
187 self.event_stores
188 .read()
189 .unwrap()
190 .keys()
191 .map(|x| x.to_owned())
192 .collect() };
194 let has_events_events = stores_with_events.contains(&"events".to_owned());
197 let glean_restarted_stores = if has_events_events {
198 stores_with_events
199 .into_iter()
200 .filter(|store| store != "events")
201 .collect()
202 } else {
203 stores_with_events
204 };
205 if !glean_restarted_stores.is_empty() {
206 for store_name in glean_restarted_stores.iter() {
207 CounterMetric::new(CommonMetricData {
208 name: "execution_counter".into(),
209 category: store_name.into(),
210 send_in_pings: vec![INTERNAL_STORAGE.into()],
211 lifetime: Lifetime::Ping,
212 ..Default::default()
213 })
214 .add_sync(glean, 1);
215 }
216 let glean_restarted = CommonMetricData {
217 name: "restarted".into(),
218 category: "glean".into(),
219 send_in_pings: glean_restarted_stores,
220 lifetime: Lifetime::Ping,
221 ..Default::default()
222 };
223 let startup = get_iso_time_string(glean.start_time(), TimeUnit::Minute);
224 let mut extra: HashMap<String, String> =
225 [("glean.startup.date".into(), startup)].into();
226 if glean.with_timestamps() {
227 let now = Utc::now();
228 let precise_timestamp = now.timestamp_millis() as u64;
229 extra.insert("glean_timestamp".to_string(), precise_timestamp.to_string());
230 }
231 self.record(
232 glean,
233 &glean_restarted.into(),
234 crate::get_timestamp_ms(),
235 Some(extra),
236 EventSessionContext::OutOfSession,
237 );
238 }
239 if has_events_events && glean.submit_ping_by_name("events", Some("startup")) {
240 self.events_pings_submitted
241 .fetch_sub(1, atomic::Ordering::Relaxed);
242 true
243 } else {
244 false
245 }
246 }
247 Err(err) => {
248 log::warn!("Error loading events from disk: {}", err);
249 false
250 }
251 }
252 }
253
254 fn load_events_from_disk(
255 &self,
256 glean: &Glean,
257 trim_data_to_registered_pings: bool,
258 ) -> Result<()> {
259 let mut db = self.event_stores.write().unwrap(); let _lock = self.file_lock.lock().unwrap(); for entry in fs::read_dir(&self.path)? {
267 let entry = entry?;
268 if entry.file_type()?.is_file() {
269 let store_name = entry.file_name().into_string()?;
270 log::info!("Loading events for {}", store_name);
271 if trim_data_to_registered_pings && glean.get_ping_by_name(&store_name).is_none() {
272 log::warn!("Trimming {}'s events", store_name);
273 if let Err(err) = fs::remove_file(entry.path()) {
274 match err.kind() {
275 std::io::ErrorKind::NotFound => {
276 }
278 _ => log::warn!("Error trimming events file '{}': {}", store_name, err),
279 }
280 }
281 continue;
282 }
283 let file = BufReader::new(File::open(entry.path())?);
284 db.insert(
285 store_name,
286 file.lines()
287 .map_while(Result::ok)
288 .filter_map(|line| serde_json::from_str::<StoredEvent>(&line).ok())
289 .collect(),
290 );
291 }
292 }
293 Ok(())
294 }
295
296 pub fn record(
315 &self,
316 glean: &Glean,
317 meta: &CommonMetricDataInternal,
318 timestamp: u64,
319 extra: Option<HashMap<String, String>>,
320 ctx: EventSessionContext,
321 ) -> bool {
322 if !glean.is_upload_enabled() {
324 return false;
325 }
326
327 let session = match ctx {
329 EventSessionContext::OutOfSession => None,
330 EventSessionContext::InSession(session_meta) => Some(session_meta),
331 };
332
333 let mut submit_max_capacity_event_ping = false;
334 {
335 let mut db = self.event_stores.write().unwrap(); for store_name in meta.inner.send_in_pings.iter() {
337 if !glean.is_ping_enabled(store_name) {
338 continue;
339 }
340
341 let store = db.entry(store_name.to_string()).or_default();
342 let execution_counter = CounterMetric::new(CommonMetricData {
343 name: "execution_counter".into(),
344 category: store_name.into(),
345 send_in_pings: vec![INTERNAL_STORAGE.into()],
346 lifetime: Lifetime::Ping,
347 ..Default::default()
348 })
349 .get_value(glean, INTERNAL_STORAGE);
350 let event = StoredEvent {
352 event: RecordedEvent {
353 timestamp,
354 category: meta.inner.category.to_string(),
355 name: meta.inner.name.to_string(),
356 extra: extra.clone(),
357 session: session.clone(),
358 },
359 execution_counter,
360 };
361 let event_json = serde_json::to_string(&event).unwrap(); store.push(event);
363 self.write_event_to_disk(store_name, &event_json);
364 if store_name == "events" {
365 if store.len() == glean.get_max_events() {
366 submit_max_capacity_event_ping = true;
367 }
368 let factor = glean.get_events_ping_acceleration_factor();
369 let events_pings_submitted =
370 self.events_pings_submitted.load(atomic::Ordering::Relaxed);
371 if factor > events_pings_submitted {
372 let a = glean.get_max_events() / factor.saturating_mul(factor);
374 let x = events_pings_submitted + 1; let y = a.saturating_mul(x.saturating_mul(x));
376 if store.len() >= y {
379 submit_max_capacity_event_ping = true;
380 }
381 }
382 }
383 }
384 }
385
386 if submit_max_capacity_event_ping {
387 glean.submit_ping_by_name("events", Some("max_capacity"));
388 true
389 } else {
390 false
391 }
392 }
393
394 fn get_event_store(&self, store_name: &str) -> Result<Arc<File>, io::Error> {
395 let mut map = self.event_store_files.write().unwrap();
397 let entry = map.entry(store_name.to_string());
398
399 match entry {
400 Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
401 Entry::Vacant(entry) => {
402 let file = OpenOptions::new()
403 .create(true)
404 .append(true)
405 .open(self.path.join(store_name))?;
406 let file = Arc::new(file);
407 let entry = entry.insert(file);
408 Ok(Arc::clone(entry))
409 }
410 }
411 }
412
413 fn write_event_to_disk(&self, store_name: &str, event_json: &str) {
420 let _lock = self.file_lock.lock().unwrap(); let write_res = (|| {
423 let mut file = self.get_event_store(store_name)?;
424 file.write_all(event_json.as_bytes())?;
425 file.write_all(b"\n")?;
426 file.flush()?;
427 Ok::<(), std::io::Error>(())
428 })();
429
430 if let Err(err) = write_res {
431 log::warn!("IO error writing event to store '{}': {}", store_name, err);
432 }
433 }
434
435 fn normalize_store(
464 &self,
465 glean: &Glean,
466 store_name: &str,
467 store: &mut Vec<StoredEvent>,
468 glean_start_time: DateTime<FixedOffset>,
469 ) {
470 let is_glean_restarted =
471 |event: &RecordedEvent| event.category == "glean" && event.name == "restarted";
472 let glean_restarted_meta = |store_name: &str| CommonMetricData {
473 name: "restarted".into(),
474 category: "glean".into(),
475 send_in_pings: vec![store_name.into()],
476 lifetime: Lifetime::Ping,
477 ..Default::default()
478 };
479 store.sort_by(|a, b| {
481 a.execution_counter
482 .cmp(&b.execution_counter)
483 .then_with(|| a.event.timestamp.cmp(&b.event.timestamp))
484 .then_with(|| {
485 if is_glean_restarted(&a.event) {
486 Ordering::Less
487 } else {
488 Ordering::Greater
489 }
490 })
491 });
492 let final_event = match store
496 .iter()
497 .rposition(|event| !is_glean_restarted(&event.event))
498 {
499 Some(idx) => idx + 1,
500 _ => 0,
501 };
502 store.drain(final_event..);
503 let first_event = store
504 .iter()
505 .position(|event| !is_glean_restarted(&event.event))
506 .unwrap_or(store.len());
507 store.drain(..first_event);
508 if store.is_empty() {
509 return;
511 }
512 let mut cur_ec = 0;
518 let mut intra_group_offset = store[0].event.timestamp;
520 let mut inter_group_offset = 0;
522 let mut highest_ts = 0;
523 for event in store.iter_mut() {
524 let execution_counter = event.execution_counter.take().unwrap_or(0);
525 if is_glean_restarted(&event.event) {
526 cur_ec = execution_counter;
529 let glean_startup_date = event
530 .event
531 .extra
532 .as_mut()
533 .and_then(|extra| {
534 extra.remove("glean.startup.date").and_then(|date_str| {
535 DateTime::parse_from_str(&date_str, TimeUnit::Minute.format_pattern())
536 .map_err(|_| {
537 record_error(
538 glean,
539 &glean_restarted_meta(store_name).into(),
540 ErrorType::InvalidState,
541 format!("Unparseable glean.startup.date '{}'", date_str),
542 None,
543 );
544 })
545 .ok()
546 })
547 })
548 .unwrap_or(glean_start_time);
549 if event
550 .event
551 .extra
552 .as_ref()
553 .is_some_and(|extra| extra.is_empty())
554 {
555 event.event.extra = None;
557 }
558 let ping_start = DatetimeMetric::new(
559 CommonMetricData {
560 name: format!("{}#start", store_name),
561 category: "".into(),
562 send_in_pings: vec![INTERNAL_STORAGE.into()],
563 lifetime: Lifetime::User,
564 ..Default::default()
565 },
566 TimeUnit::Minute,
567 );
568 let ping_start = ping_start
569 .get_value(glean, INTERNAL_STORAGE)
570 .unwrap_or(glean_start_time);
571 let time_from_ping_start_to_glean_restarted =
572 (glean_startup_date - ping_start).num_milliseconds();
573 intra_group_offset = event.event.timestamp;
574 inter_group_offset =
575 u64::try_from(time_from_ping_start_to_glean_restarted).unwrap_or(0);
576 if inter_group_offset < highest_ts {
577 record_error(
578 glean,
579 &glean_restarted_meta(store_name).into(),
580 ErrorType::InvalidValue,
581 format!("Time between restart and ping start {} indicates client clock weirdness.", time_from_ping_start_to_glean_restarted),
582 None,
583 );
584 inter_group_offset = highest_ts + 1;
589 }
590 } else if cur_ec == 0 {
591 cur_ec = execution_counter;
593 }
594 event.event.timestamp = event.event.timestamp - intra_group_offset + inter_group_offset;
595 if execution_counter != cur_ec {
596 record_error(
597 glean,
598 &glean_restarted_meta(store_name).into(),
599 ErrorType::InvalidState,
600 format!(
601 "Inconsistent execution counter {} (expected {})",
602 execution_counter, cur_ec
603 ),
604 None,
605 );
606 cur_ec = execution_counter;
608 }
609
610 if event.event.timestamp > i64::MAX as u64 {
613 glean
614 .additional_metrics
615 .event_timestamp_clamped
616 .add_sync(glean, 1);
617 log::warn!(
618 "Calculated event timestamp was too high. Got: {}, max: {}",
619 event.event.timestamp,
620 i64::MAX,
621 );
622 event.event.timestamp = event.event.timestamp.clamp(0, i64::MAX as u64);
623 }
624
625 if highest_ts > event.event.timestamp {
626 record_error(
629 glean,
630 &glean_restarted_meta(store_name).into(),
631 ErrorType::InvalidState,
632 format!(
633 "Inconsistent previous highest timestamp {} (expected <= {})",
634 highest_ts, event.event.timestamp
635 ),
636 None,
637 );
638 }
640 highest_ts = event.event.timestamp
641 }
642 }
643
644 pub fn snapshot_as_json(
656 &self,
657 glean: &Glean,
658 store_name: &str,
659 clear_store: bool,
660 ) -> Option<JsonValue> {
661 let result = {
662 let mut db = self.event_stores.write().unwrap(); db.get_mut(&store_name.to_string()).and_then(|store| {
664 if !store.is_empty() {
665 let mut clone;
668 let store = if clear_store {
669 store
670 } else {
671 clone = store.clone();
672 &mut clone
673 };
674 self.normalize_store(glean, store_name, store, glean.start_time());
676 Some(json!(store))
677 } else {
678 log::warn!("Unexpectly got empty event store for '{}'", store_name);
679 None
680 }
681 })
682 };
683
684 if clear_store {
685 self.event_stores
686 .write()
687 .unwrap() .remove(&store_name.to_string());
689 self.event_store_files
690 .write()
691 .unwrap() .remove(&store_name.to_string());
693
694 let _lock = self.file_lock.lock().unwrap(); if let Err(err) = fs::remove_file(self.path.join(store_name)) {
696 match err.kind() {
697 std::io::ErrorKind::NotFound => {
698 }
700 _ => log::warn!("Error removing events queue file '{}': {}", store_name, err),
701 }
702 }
703 }
704
705 if clear_store && store_name == "events" {
706 self.events_pings_submitted
707 .fetch_add(1, atomic::Ordering::Relaxed);
708 }
709
710 result
711 }
712
713 pub fn clear_all(&self) -> Result<()> {
715 self.event_stores.write().unwrap().clear();
717 self.event_store_files.write().unwrap().clear();
718
719 let _lock = self.file_lock.lock().unwrap();
721 std::fs::remove_dir_all(&self.path)?;
722 create_dir_all(&self.path)?;
723
724 Ok(())
725 }
726
727 pub fn clear_for_ping(&self, ping: &str) -> Result<()> {
729 log::debug!("Clearing event storage for ping {ping}");
730
731 let _lock = self.file_lock.lock().unwrap();
733
734 self.event_stores.write().unwrap().remove(ping);
736 if let Some(ping_file) = self.event_store_files.write().unwrap().remove(ping) {
737 debug_assert_eq!(1, Arc::strong_count(&ping_file));
739 drop(ping_file);
740 let file_path = self.path.join(ping);
741 std::fs::remove_file(file_path)?;
742 }
743
744 Ok(())
745 }
746
747 pub fn test_get_value<'a>(
754 &'a self,
755 meta: &'a CommonMetricDataInternal,
756 store_name: &str,
757 ) -> Option<Vec<RecordedEvent>> {
758 let value: Vec<RecordedEvent> = self
759 .event_stores
760 .read()
761 .unwrap() .get(&store_name.to_string())
763 .into_iter()
764 .flatten()
765 .map(|stored_event| stored_event.event.clone())
766 .filter(|event| event.name == meta.inner.name && event.category == meta.inner.category)
767 .collect();
768 if !value.is_empty() {
769 Some(value)
770 } else {
771 None
772 }
773 }
774}
775
776#[cfg(test)]
777mod test {
778 use super::*;
779 use crate::metrics::RemoteSettingsConfig;
780 use crate::test_get_num_recorded_errors;
781 use crate::tests::new_glean;
782 use chrono::{TimeZone, Timelike};
783
784 #[test]
785 fn handle_truncated_events_on_disk() {
786 let (glean, t) = new_glean(None);
787
788 {
789 let db = EventDatabase::new(t.path()).unwrap();
790 db.write_event_to_disk("events", "{\"timestamp\": 500");
791 db.write_event_to_disk("events", "{\"timestamp\"");
792 db.write_event_to_disk(
793 "events",
794 "{\"timestamp\": 501, \"category\": \"ui\", \"name\": \"click\"}",
795 );
796 }
797
798 {
799 let db = EventDatabase::new(t.path()).unwrap();
800 db.load_events_from_disk(&glean, false).unwrap();
801 let events = &db.event_stores.read().unwrap()["events"];
802 assert_eq!(1, events.len());
803 }
804 }
805
806 #[test]
807 fn stable_serialization() {
808 let event_empty = RecordedEvent {
809 timestamp: 2,
810 category: "cat".to_string(),
811 name: "name".to_string(),
812 extra: None,
813 session: None,
814 };
815
816 let mut data = HashMap::new();
817 data.insert("a key".to_string(), "a value".to_string());
818 let event_data = RecordedEvent {
819 timestamp: 2,
820 category: "cat".to_string(),
821 name: "name".to_string(),
822 extra: Some(data),
823 session: None,
824 };
825
826 let event_empty_json = ::serde_json::to_string_pretty(&event_empty).unwrap();
827 let event_data_json = ::serde_json::to_string_pretty(&event_data).unwrap();
828
829 assert_eq!(
830 StoredEvent {
831 event: event_empty,
832 execution_counter: None
833 },
834 serde_json::from_str(&event_empty_json).unwrap()
835 );
836 assert_eq!(
837 StoredEvent {
838 event: event_data,
839 execution_counter: None
840 },
841 serde_json::from_str(&event_data_json).unwrap()
842 );
843 }
844
845 #[test]
846 fn deserialize_existing_data() {
847 let event_empty_json = r#"
848{
849 "timestamp": 2,
850 "category": "cat",
851 "name": "name"
852}
853 "#;
854
855 let event_data_json = r#"
856{
857 "timestamp": 2,
858 "category": "cat",
859 "name": "name",
860 "extra": {
861 "a key": "a value"
862 }
863}
864 "#;
865
866 let event_empty = RecordedEvent {
867 timestamp: 2,
868 category: "cat".to_string(),
869 name: "name".to_string(),
870 extra: None,
871 session: None,
872 };
873
874 let mut data = HashMap::new();
875 data.insert("a key".to_string(), "a value".to_string());
876 let event_data = RecordedEvent {
877 timestamp: 2,
878 category: "cat".to_string(),
879 name: "name".to_string(),
880 extra: Some(data),
881 session: None,
882 };
883
884 assert_eq!(
885 StoredEvent {
886 event: event_empty,
887 execution_counter: None
888 },
889 serde_json::from_str(event_empty_json).unwrap()
890 );
891 assert_eq!(
892 StoredEvent {
893 event: event_data,
894 execution_counter: None
895 },
896 serde_json::from_str(event_data_json).unwrap()
897 );
898 }
899
900 #[test]
901 fn doesnt_record_when_upload_is_disabled() {
902 let (mut glean, dir) = new_glean(None);
903 let db = EventDatabase::new(dir.path()).unwrap();
904
905 let test_storage = "store1";
906 let test_category = "category";
907 let test_name = "name";
908 let test_timestamp = 2;
909 let test_meta = CommonMetricDataInternal::new(test_category, test_name, test_storage);
910 let event_data = RecordedEvent {
911 timestamp: test_timestamp,
912 category: test_category.to_string(),
913 name: test_name.to_string(),
914 extra: None,
915 session: None,
916 };
917
918 db.record(
921 &glean,
922 &test_meta,
923 2,
924 None,
925 EventSessionContext::OutOfSession,
926 );
927 {
928 let event_stores = db.event_stores.read().unwrap();
929 assert_eq!(
930 &StoredEvent {
931 event: event_data,
932 execution_counter: None
933 },
934 &event_stores.get(test_storage).unwrap()[0]
935 );
936 assert_eq!(event_stores.get(test_storage).unwrap().len(), 1);
937 }
938
939 glean.set_upload_enabled(false);
940
941 db.record(
943 &glean,
944 &test_meta,
945 2,
946 None,
947 EventSessionContext::OutOfSession,
948 );
949 {
950 let event_stores = db.event_stores.read().unwrap();
951 assert_eq!(event_stores.get(test_storage).unwrap().len(), 1);
952 }
953 }
954
955 #[test]
956 fn normalize_store_of_glean_restarted() {
957 let (glean, _dir) = new_glean(None);
959
960 let store_name = "store-name";
961 let glean_restarted = StoredEvent {
962 event: RecordedEvent {
963 timestamp: 2,
964 category: "glean".into(),
965 name: "restarted".into(),
966 extra: None,
967 session: None,
968 },
969 execution_counter: None,
970 };
971 let mut store = vec![glean_restarted.clone()];
972 let glean_start_time = glean.start_time();
973
974 glean
975 .event_storage()
976 .normalize_store(&glean, store_name, &mut store, glean_start_time);
977 assert!(store.is_empty());
978
979 let mut store = vec![glean_restarted.clone(), glean_restarted.clone()];
980 glean
981 .event_storage()
982 .normalize_store(&glean, store_name, &mut store, glean_start_time);
983 assert!(store.is_empty());
984
985 let mut store = vec![
986 glean_restarted.clone(),
987 glean_restarted.clone(),
988 glean_restarted,
989 ];
990 glean
991 .event_storage()
992 .normalize_store(&glean, store_name, &mut store, glean_start_time);
993 assert!(store.is_empty());
994 }
995
996 #[test]
997 fn normalize_store_of_glean_restarted_on_both_ends() {
998 let (glean, _dir) = new_glean(None);
1000
1001 let store_name = "store-name";
1002 let glean_restarted = StoredEvent {
1003 event: RecordedEvent {
1004 timestamp: 2,
1005 category: "glean".into(),
1006 name: "restarted".into(),
1007 extra: None,
1008 session: None,
1009 },
1010 execution_counter: None,
1011 };
1012 let not_glean_restarted = StoredEvent {
1013 event: RecordedEvent {
1014 timestamp: 20,
1015 category: "category".into(),
1016 name: "name".into(),
1017 extra: None,
1018 session: None,
1019 },
1020 execution_counter: None,
1021 };
1022 let mut store = vec![
1023 glean_restarted.clone(),
1024 not_glean_restarted.clone(),
1025 glean_restarted,
1026 ];
1027 let glean_start_time = glean.start_time();
1028
1029 glean
1030 .event_storage()
1031 .normalize_store(&glean, store_name, &mut store, glean_start_time);
1032 assert_eq!(1, store.len());
1033 assert_eq!(
1034 StoredEvent {
1035 event: RecordedEvent {
1036 timestamp: 0,
1037 ..not_glean_restarted.event
1038 },
1039 execution_counter: None
1040 },
1041 store[0]
1042 );
1043 }
1044
1045 #[test]
1046 fn normalize_store_single_run_timestamp_math() {
1047 let (glean, _dir) = new_glean(None);
1051
1052 let store_name = "store-name";
1053 let glean_restarted = StoredEvent {
1054 event: RecordedEvent {
1055 timestamp: 2,
1056 category: "glean".into(),
1057 name: "restarted".into(),
1058 extra: None,
1059 session: None,
1060 },
1061 execution_counter: None,
1062 };
1063 let timestamps = [20, 40, 200];
1064 let not_glean_restarted = StoredEvent {
1065 event: RecordedEvent {
1066 timestamp: timestamps[0],
1067 category: "category".into(),
1068 name: "name".into(),
1069 extra: None,
1070 session: None,
1071 },
1072 execution_counter: None,
1073 };
1074 let mut store = vec![
1075 glean_restarted.clone(),
1076 not_glean_restarted.clone(),
1077 StoredEvent {
1078 event: RecordedEvent {
1079 timestamp: timestamps[1],
1080 ..not_glean_restarted.event.clone()
1081 },
1082 execution_counter: None,
1083 },
1084 StoredEvent {
1085 event: RecordedEvent {
1086 timestamp: timestamps[2],
1087 ..not_glean_restarted.event.clone()
1088 },
1089 execution_counter: None,
1090 },
1091 glean_restarted,
1092 ];
1093
1094 glean
1095 .event_storage()
1096 .normalize_store(&glean, store_name, &mut store, glean.start_time());
1097 assert_eq!(3, store.len());
1098 for (timestamp, event) in timestamps.iter().zip(store.iter()) {
1099 assert_eq!(
1100 &StoredEvent {
1101 event: RecordedEvent {
1102 timestamp: timestamp - timestamps[0],
1103 ..not_glean_restarted.clone().event
1104 },
1105 execution_counter: None
1106 },
1107 event
1108 );
1109 }
1110 }
1111
1112 #[test]
1113 fn normalize_store_multi_run_timestamp_math() {
1114 let (glean, _dir) = new_glean(None);
1119
1120 let store_name = "store-name";
1121 let glean_restarted = StoredEvent {
1122 event: RecordedEvent {
1123 category: "glean".into(),
1124 name: "restarted".into(),
1125 ..Default::default()
1126 },
1127 execution_counter: None,
1128 };
1129 let not_glean_restarted = StoredEvent {
1130 event: RecordedEvent {
1131 category: "category".into(),
1132 name: "name".into(),
1133 ..Default::default()
1134 },
1135 execution_counter: None,
1136 };
1137
1138 let timestamps = [20, 40, 200, 12];
1141 let ecs = [0, 1];
1142 let some_hour = 16;
1143 let startup_date = FixedOffset::east_opt(0)
1144 .unwrap()
1145 .with_ymd_and_hms(2022, 11, 24, some_hour, 29, 0) .unwrap();
1147 let glean_start_time = startup_date.with_hour(some_hour - 1);
1148 let restarted_ts = 2;
1149 let mut store = vec![
1150 StoredEvent {
1151 event: RecordedEvent {
1152 timestamp: timestamps[0],
1153 ..not_glean_restarted.event.clone()
1154 },
1155 execution_counter: Some(ecs[0]),
1156 },
1157 StoredEvent {
1158 event: RecordedEvent {
1159 timestamp: timestamps[1],
1160 ..not_glean_restarted.event.clone()
1161 },
1162 execution_counter: Some(ecs[0]),
1163 },
1164 StoredEvent {
1165 event: RecordedEvent {
1166 timestamp: timestamps[2],
1167 ..not_glean_restarted.event.clone()
1168 },
1169 execution_counter: Some(ecs[0]),
1170 },
1171 StoredEvent {
1172 event: RecordedEvent {
1173 extra: Some(
1174 [(
1175 "glean.startup.date".into(),
1176 get_iso_time_string(startup_date, TimeUnit::Minute),
1177 )]
1178 .into(),
1179 ),
1180 timestamp: restarted_ts,
1181 ..glean_restarted.event.clone()
1182 },
1183 execution_counter: Some(ecs[1]),
1184 },
1185 StoredEvent {
1186 event: RecordedEvent {
1187 timestamp: timestamps[3],
1188 ..not_glean_restarted.event.clone()
1189 },
1190 execution_counter: Some(ecs[1]),
1191 },
1192 ];
1193
1194 glean.event_storage().normalize_store(
1195 &glean,
1196 store_name,
1197 &mut store,
1198 glean_start_time.unwrap(),
1199 );
1200 assert_eq!(5, store.len()); for (timestamp, event) in timestamps[..timestamps.len() - 1].iter().zip(store.clone()) {
1204 assert_eq!(
1205 StoredEvent {
1206 event: RecordedEvent {
1207 timestamp: timestamp - timestamps[0],
1208 ..not_glean_restarted.event.clone()
1209 },
1210 execution_counter: None,
1211 },
1212 event
1213 );
1214 }
1215 let hour_in_millis = 3600000;
1217 assert_eq!(
1218 store[3],
1219 StoredEvent {
1220 event: RecordedEvent {
1221 timestamp: hour_in_millis,
1222 ..glean_restarted.event
1223 },
1224 execution_counter: None,
1225 }
1226 );
1227 assert_eq!(
1229 store[4],
1230 StoredEvent {
1231 event: RecordedEvent {
1232 timestamp: hour_in_millis + timestamps[3] - restarted_ts,
1233 ..not_glean_restarted.event
1234 },
1235 execution_counter: None,
1236 }
1237 );
1238 }
1239
1240 #[test]
1241 fn normalize_store_multi_run_client_clocks() {
1242 let (glean, _dir) = new_glean(None);
1245
1246 let store_name = "store-name";
1247 let glean_restarted = StoredEvent {
1248 event: RecordedEvent {
1249 category: "glean".into(),
1250 name: "restarted".into(),
1251 ..Default::default()
1252 },
1253 execution_counter: None,
1254 };
1255 let not_glean_restarted = StoredEvent {
1256 event: RecordedEvent {
1257 category: "category".into(),
1258 name: "name".into(),
1259 ..Default::default()
1260 },
1261 execution_counter: None,
1262 };
1263
1264 let timestamps = [20, 40, 12, 200];
1267 let ecs = [0, 1];
1268 let some_hour = 10;
1269 let startup_date = FixedOffset::east_opt(0)
1270 .unwrap()
1271 .with_ymd_and_hms(2022, 11, 25, some_hour, 37, 0) .unwrap();
1273 let glean_start_time = startup_date.with_hour(some_hour + 1);
1274 let restarted_ts = 2;
1275 let mut store = vec![
1276 StoredEvent {
1277 event: RecordedEvent {
1278 timestamp: timestamps[0],
1279 ..not_glean_restarted.event.clone()
1280 },
1281 execution_counter: Some(ecs[0]),
1282 },
1283 StoredEvent {
1284 event: RecordedEvent {
1285 timestamp: timestamps[1],
1286 ..not_glean_restarted.event.clone()
1287 },
1288 execution_counter: Some(ecs[0]),
1289 },
1290 StoredEvent {
1291 event: RecordedEvent {
1292 extra: Some(
1293 [(
1294 "glean.startup.date".into(),
1295 get_iso_time_string(startup_date, TimeUnit::Minute),
1296 )]
1297 .into(),
1298 ),
1299 timestamp: restarted_ts,
1300 ..glean_restarted.event.clone()
1301 },
1302 execution_counter: Some(ecs[1]),
1303 },
1304 StoredEvent {
1305 event: RecordedEvent {
1306 timestamp: timestamps[2],
1307 ..not_glean_restarted.event.clone()
1308 },
1309 execution_counter: Some(ecs[1]),
1310 },
1311 StoredEvent {
1312 event: RecordedEvent {
1313 timestamp: timestamps[3],
1314 ..not_glean_restarted.event.clone()
1315 },
1316 execution_counter: Some(ecs[1]),
1317 },
1318 ];
1319
1320 glean.event_storage().normalize_store(
1321 &glean,
1322 store_name,
1323 &mut store,
1324 glean_start_time.unwrap(),
1325 );
1326 assert_eq!(5, store.len()); for (timestamp, event) in timestamps[..timestamps.len() - 2].iter().zip(store.clone()) {
1330 assert_eq!(
1331 StoredEvent {
1332 event: RecordedEvent {
1333 timestamp: timestamp - timestamps[0],
1334 ..not_glean_restarted.event.clone()
1335 },
1336 execution_counter: None,
1337 },
1338 event
1339 );
1340 }
1341 assert_eq!(
1345 store[2],
1346 StoredEvent {
1347 event: RecordedEvent {
1348 timestamp: store[1].event.timestamp + 1,
1349 ..glean_restarted.event
1350 },
1351 execution_counter: None,
1352 }
1353 );
1354 assert_eq!(
1356 store[3],
1357 StoredEvent {
1358 event: RecordedEvent {
1359 timestamp: timestamps[2] - restarted_ts + store[2].event.timestamp,
1360 ..not_glean_restarted.event
1361 },
1362 execution_counter: None,
1363 }
1364 );
1365 assert_eq!(
1367 Ok(1),
1368 test_get_num_recorded_errors(
1369 &glean,
1370 &CommonMetricData {
1371 name: "restarted".into(),
1372 category: "glean".into(),
1373 send_in_pings: vec![store_name.into()],
1374 lifetime: Lifetime::Ping,
1375 ..Default::default()
1376 }
1377 .into(),
1378 ErrorType::InvalidValue
1379 )
1380 );
1381 }
1382
1383 #[test]
1384 fn normalize_store_non_zero_ec() {
1385 let (glean, _dir) = new_glean(None);
1388
1389 let store_name = "store-name";
1390 let glean_restarted = StoredEvent {
1391 event: RecordedEvent {
1392 timestamp: 2,
1393 category: "glean".into(),
1394 name: "restarted".into(),
1395 extra: None,
1396 session: None,
1397 },
1398 execution_counter: Some(2),
1399 };
1400 let not_glean_restarted = StoredEvent {
1401 event: RecordedEvent {
1402 timestamp: 20,
1403 category: "category".into(),
1404 name: "name".into(),
1405 extra: None,
1406 session: None,
1407 },
1408 execution_counter: Some(2),
1409 };
1410 let glean_restarted_2 = StoredEvent {
1411 event: RecordedEvent {
1412 timestamp: 2,
1413 category: "glean".into(),
1414 name: "restarted".into(),
1415 extra: None,
1416 session: None,
1417 },
1418 execution_counter: Some(3),
1419 };
1420 let mut store = vec![
1421 glean_restarted,
1422 not_glean_restarted.clone(),
1423 glean_restarted_2,
1424 ];
1425 let glean_start_time = glean.start_time();
1426
1427 glean
1428 .event_storage()
1429 .normalize_store(&glean, store_name, &mut store, glean_start_time);
1430
1431 assert_eq!(1, store.len());
1432 assert_eq!(
1433 StoredEvent {
1434 event: RecordedEvent {
1435 timestamp: 0,
1436 ..not_glean_restarted.event
1437 },
1438 execution_counter: None
1439 },
1440 store[0]
1441 );
1442 assert!(test_get_num_recorded_errors(
1444 &glean,
1445 &CommonMetricData {
1446 name: "restarted".into(),
1447 category: "glean".into(),
1448 send_in_pings: vec![store_name.into()],
1449 lifetime: Lifetime::Ping,
1450 ..Default::default()
1451 }
1452 .into(),
1453 ErrorType::InvalidState
1454 )
1455 .is_err());
1456 assert!(test_get_num_recorded_errors(
1458 &glean,
1459 &CommonMetricData {
1460 name: "restarted".into(),
1461 category: "glean".into(),
1462 send_in_pings: vec![store_name.into()],
1463 lifetime: Lifetime::Ping,
1464 ..Default::default()
1465 }
1466 .into(),
1467 ErrorType::InvalidValue
1468 )
1469 .is_err());
1470 }
1471
1472 #[test]
1473 fn normalize_store_clamps_timestamp() {
1474 let (glean, _dir) = new_glean(None);
1475
1476 let store_name = "store-name";
1477 let event = RecordedEvent {
1478 category: "category".into(),
1479 name: "name".into(),
1480 ..Default::default()
1481 };
1482
1483 let timestamps = [
1484 0,
1485 (i64::MAX / 2) as u64,
1486 i64::MAX as _,
1487 (i64::MAX as u64) + 1,
1488 ];
1489 let mut store = timestamps
1490 .into_iter()
1491 .map(|timestamp| StoredEvent {
1492 event: RecordedEvent {
1493 timestamp,
1494 ..event.clone()
1495 },
1496 execution_counter: None,
1497 })
1498 .collect();
1499
1500 let glean_start_time = glean.start_time();
1501 glean
1502 .event_storage()
1503 .normalize_store(&glean, store_name, &mut store, glean_start_time);
1504 assert_eq!(4, store.len());
1505
1506 assert_eq!(0, store[0].event.timestamp);
1507 assert_eq!((i64::MAX / 2) as u64, store[1].event.timestamp);
1508 assert_eq!((i64::MAX as u64), store[2].event.timestamp);
1509 assert_eq!((i64::MAX as u64), store[3].event.timestamp);
1510 }
1511
1512 #[test]
1513 fn normalize_store_clamps_timestamp_metric_enabled() {
1514 let (glean, _dir) = new_glean(None);
1515
1516 let mut cfg = RemoteSettingsConfig::default();
1517 cfg.metrics_enabled
1518 .insert("glean.error.event_timestamp_clamped".to_string(), true);
1519 glean.apply_server_knobs_config(cfg);
1520
1521 let store_name = "store-name";
1522 let event = RecordedEvent {
1523 category: "category".into(),
1524 name: "name".into(),
1525 ..Default::default()
1526 };
1527
1528 let timestamps = [0, (i64::MAX as u64) + 1];
1529 let mut store = timestamps
1530 .into_iter()
1531 .map(|timestamp| StoredEvent {
1532 event: RecordedEvent {
1533 timestamp,
1534 ..event.clone()
1535 },
1536 execution_counter: None,
1537 })
1538 .collect();
1539
1540 let glean_start_time = glean.start_time();
1541 glean
1542 .event_storage()
1543 .normalize_store(&glean, store_name, &mut store, glean_start_time);
1544 assert_eq!(2, store.len());
1545
1546 assert_eq!(0, store[0].event.timestamp);
1547 assert_eq!((i64::MAX as u64), store[1].event.timestamp);
1548
1549 let error_count = glean
1550 .additional_metrics
1551 .event_timestamp_clamped
1552 .get_value(&glean, "health");
1553 assert_eq!(Some(1), error_count);
1554 }
1555}