Skip to main content

journal_engine/
cache.rs

1//! Cache types for journal file indexes
2
3use crate::facets::Facets;
4use foyer::HybridCache;
5use journal_index::{FieldName, FileIndex};
6use journal_registry::File;
7use serde::{Deserialize, Serialize};
8
9/// Cache version number. Increment this when the FileIndex schema, FileIndexKey
10/// schema, or indexer semantics change to automatically invalidate old cache
11/// entries.
12///
13/// v3: Index semantics changed from the v2 ND_REMAPPING-specific behavior, and
14///     FileIndexKey gained an explicit consumer namespace. Old v2 cache entries
15///     are not reused across SDK versions with different index rules.
16const CACHE_VERSION: u32 = 3;
17
18/// Cache key for file indexes that includes the file, facets, source timestamp
19/// field, and cache version. Different facet configurations or timestamp fields
20/// produce different indexes, so all are needed to uniquely identify a cached
21/// index. The version ensures that schema changes automatically invalidate old
22/// cache entries.
23#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct FileIndexKey {
25    version: u32,
26    #[serde(default)]
27    namespace: String,
28    pub file: File,
29    pub(crate) facets: Facets,
30    pub(crate) source_timestamp_field: Option<FieldName>,
31}
32
33impl FileIndexKey {
34    pub fn new(file: &File, facets: &Facets, source_timestamp_field: Option<FieldName>) -> Self {
35        Self::new_with_namespace(file, facets, source_timestamp_field, "")
36    }
37
38    /// Creates a cache key inside a consumer-controlled namespace.
39    ///
40    /// Consumers can change the namespace to force a clean index rebuild for
41    /// their own semantic migrations without changing the SDK-owned cache
42    /// version or moving the cache directory.
43    pub fn new_with_namespace(
44        file: &File,
45        facets: &Facets,
46        source_timestamp_field: Option<FieldName>,
47        namespace: impl Into<String>,
48    ) -> Self {
49        Self {
50            version: CACHE_VERSION,
51            namespace: namespace.into(),
52            file: file.clone(),
53            facets: facets.clone(),
54            source_timestamp_field,
55        }
56    }
57}
58
59/// Type alias for file index cache using Foyer's HybridCache.
60pub type FileIndexCache = HybridCache<FileIndexKey, FileIndex>;
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use std::collections::HashMap;
66    use std::path::Path;
67
68    #[test]
69    fn cache_version_bump_prevents_old_key_reuse() {
70        let file = test_file();
71        let facets = Facets::new(&[]);
72        let current = FileIndexKey::new(&file, &facets, None);
73        let old = FileIndexKey {
74            version: CACHE_VERSION - 1,
75            namespace: String::new(),
76            file,
77            facets,
78            source_timestamp_field: None,
79        };
80
81        let mut cache = HashMap::new();
82        cache.insert(old, "old-index");
83
84        assert_eq!(cache.get(&current), None);
85    }
86
87    #[test]
88    fn cache_namespace_keys_do_not_collide() {
89        let file = test_file();
90        let facets = Facets::new(&[]);
91
92        let default = FileIndexKey::new(&file, &facets, None);
93        let explicit_default = FileIndexKey::new_with_namespace(&file, &facets, None, "");
94        let named = FileIndexKey::new_with_namespace(&file, &facets, None, "consumer-v2");
95
96        assert_eq!(default, explicit_default);
97        assert_ne!(default, named);
98    }
99
100    #[test]
101    fn old_serialized_cache_key_without_namespace_decodes_as_old_key() {
102        let file = test_file();
103        let facets = Facets::new(&[]);
104        let old = FileIndexKey {
105            version: CACHE_VERSION - 1,
106            namespace: "old-cache".to_string(),
107            file: file.clone(),
108            facets: facets.clone(),
109            source_timestamp_field: None,
110        };
111        let mut old_shape = serde_json::to_value(&old).expect("serialize old key");
112        old_shape
113            .as_object_mut()
114            .expect("key serializes as object")
115            .remove("namespace");
116
117        let decoded: FileIndexKey =
118            serde_json::from_value(old_shape).expect("decode old key without namespace");
119
120        assert_eq!(decoded.namespace, "");
121        assert_eq!(decoded.version, CACHE_VERSION - 1);
122        assert_ne!(decoded, FileIndexKey::new(&file, &facets, None));
123    }
124
125    fn test_file() -> File {
126        File::from_path(Path::new(
127            "/var/log/journal/00112233445566778899aabbccddeeff/system.journal",
128        ))
129        .expect("valid journal file path")
130    }
131}