saluki_env/workload/stores/
external_data.rs1use std::{num::NonZeroUsize, sync::Arc};
2
3use arc_swap::ArcSwap;
4use saluki_common::collections::{FastHashSet, FastIndexMap};
5use saluki_context::origin::{ExternalData, RawExternalData};
6use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
7use saluki_metrics::{static_metrics, Counter, Gauge};
8use tracing::{debug, trace};
9
10use crate::workload::{
11 aggregator::MetadataStore, origin::ResolvedExternalData, EntityId, MetadataAction, MetadataOperation,
12};
13
14#[static_metrics(prefix = external_data_store)]
15#[derive(Clone)]
16struct Telemetry {
17 entity_limit: Gauge,
18 active_entities: Gauge,
19 ops_delete_total: Counter,
20 ops_attach_external_data_total: Counter,
21}
22
23pub struct ExternalDataStore {
41 snapshot: Arc<ArcSwap<ExternalDataSnapshot>>,
42 entity_limit: NonZeroUsize,
43 active_entities: FastHashSet<EntityId>,
44 forward_mappings: FastIndexMap<ExternalData, ResolvedExternalData>,
45 reverse_mappings: FastIndexMap<EntityId, ExternalData>,
46 telemetry: Telemetry,
47}
48
49impl ExternalDataStore {
50 pub fn with_entity_limit(entity_limit: NonZeroUsize) -> Self {
55 let telemetry = Telemetry::new();
56 telemetry.entity_limit().set(entity_limit.get() as f64);
57
58 Self {
59 snapshot: Arc::new(ArcSwap::new(Arc::new(ExternalDataSnapshot::default()))),
60 entity_limit,
61 active_entities: FastHashSet::default(),
62 forward_mappings: FastIndexMap::default(),
63 reverse_mappings: FastIndexMap::default(),
64 telemetry,
65 }
66 }
67
68 pub fn entity_limit(&self) -> usize {
70 self.entity_limit.get()
71 }
72
73 fn track_entity(&mut self, entity_id: &EntityId) -> bool {
74 if self.active_entities.contains(entity_id) {
75 return true;
76 }
77
78 if self.active_entities.len() >= self.entity_limit() {
79 return false;
80 }
81
82 self.telemetry.active_entities().increment(1);
83 let _ = self.active_entities.insert(entity_id.clone());
84 true
85 }
86
87 fn add_mapping(&mut self, external_data: ExternalData, entity_id: EntityId) {
88 if !self.track_entity(&entity_id) {
89 trace!(
90 entity_limit = self.entity_limit(),
91 %entity_id,
92 "Entity limit reached, not adding mapping."
93 );
94 return;
95 }
96
97 let resolved = ResolvedExternalData::new(EntityId::PodUid(external_data.pod_uid().clone()), entity_id.clone());
100
101 let _ = self.forward_mappings.insert(external_data.clone(), resolved);
102 let _ = self.reverse_mappings.insert(entity_id, external_data);
103 }
104
105 fn remove_mapping(&mut self, entity_id: EntityId) {
106 if !self.active_entities.remove(&entity_id) {
107 return;
108 }
109
110 self.telemetry.active_entities().decrement(1);
111
112 if let Some(external_data) = self.reverse_mappings.swap_remove(&entity_id) {
113 let _ = self.forward_mappings.swap_remove(&external_data);
114 }
115 }
116
117 pub fn resolver(&self) -> ExternalDataStoreResolver {
119 ExternalDataStoreResolver {
120 snapshot: Arc::clone(&self.snapshot),
121 }
122 }
123}
124
125impl MetadataStore for ExternalDataStore {
126 fn name(&self) -> &'static str {
127 "external_data"
128 }
129
130 fn process_operation(&mut self, operation: MetadataOperation) {
131 debug!(?operation, "Processing metadata operation.");
132
133 let entity_id = operation.entity_id;
136 for action in operation.actions {
137 match action {
138 MetadataAction::AttachExternalData { external_data } => {
139 self.telemetry.ops_attach_external_data_total().increment(1);
140 self.add_mapping(external_data, entity_id.clone());
141 }
142 MetadataAction::Delete => {
143 self.telemetry.ops_delete_total().increment(1);
144 self.remove_mapping(entity_id.clone());
145 }
146
147 _ => {}
149 }
150 }
151
152 let snapshot = Arc::new(ExternalDataSnapshot {
154 forward_mappings: self.forward_mappings.clone(),
155 });
156
157 self.snapshot.store(snapshot);
158 }
159}
160
161impl MemoryBounds for ExternalDataStore {
162 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
163 builder
164 .firm()
165 .with_array::<EntityId>("entity ids", self.entity_limit())
167 .with_map::<ExternalData, EntityId>("ext data entity map", self.entity_limit())
169 .with_map::<EntityId, ExternalData>("entity ext data map", self.entity_limit());
170 }
171}
172
173#[derive(Default)]
174struct ExternalDataSnapshot {
175 forward_mappings: FastIndexMap<ExternalData, ResolvedExternalData>,
176}
177
178#[derive(Clone)]
180pub struct ExternalDataStoreResolver {
181 snapshot: Arc<ArcSwap<ExternalDataSnapshot>>,
182}
183
184impl ExternalDataStoreResolver {
185 pub fn resolve(&self, external_data: &RawExternalData<'_>) -> Option<ResolvedExternalData> {
192 let snapshot = self.snapshot.load();
193 snapshot.forward_mappings.get(external_data).cloned()
194 }
195
196 pub fn with_latest_snapshot<F>(&self, mut f: F)
198 where
199 F: FnMut(&ExternalData, &EntityId),
200 {
201 let snapshot = self.snapshot.load();
202 for (external_data, resolved_ed) in snapshot.forward_mappings.iter() {
203 f(external_data, resolved_ed.container_entity_id());
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use std::num::NonZeroUsize;
211
212 use saluki_context::origin::{ExternalData, RawExternalData};
213
214 use super::ExternalDataStore;
215 use crate::workload::{aggregator::MetadataStore as _, origin::ResolvedExternalData, EntityId, MetadataOperation};
216
217 const DEFAULT_ENTITY_LIMIT: NonZeroUsize = NonZeroUsize::new(10).unwrap();
218
219 fn entity_id_container(id: &str) -> EntityId {
220 EntityId::Container(id.into())
221 }
222
223 fn build_external_data(
224 pod_uid: &str, container_name: &str, container_id: &EntityId, init_container: bool,
225 ) -> (String, ExternalData, ResolvedExternalData) {
226 let raw_external_data = format!("pu-{},cn-{},it-{}", pod_uid, container_name, init_container);
227 let pod_entity_id = EntityId::from_pod_uid(pod_uid).unwrap();
228
229 let external_data = ExternalData::new(pod_uid.into(), container_name.into(), init_container);
230 let resolved_external_data = ResolvedExternalData::new(pod_entity_id.clone(), container_id.clone());
231 (raw_external_data, external_data, resolved_external_data)
232 }
233
234 #[test]
235 fn basic() {
236 let mut store = ExternalDataStore::with_entity_limit(DEFAULT_ENTITY_LIMIT);
237 let resolver = store.resolver();
238
239 let container_eid = entity_id_container("abcdef");
240 let (raw_ed, ed, resolved_ed) = build_external_data("1234", "redis", &container_eid, false);
241 let ed_ref = RawExternalData::try_from_str(&raw_ed).unwrap();
242
243 assert_eq!(resolver.resolve(&ed_ref), None);
245
246 store.process_operation(MetadataOperation::attach_external_data(container_eid.clone(), ed));
248
249 assert_eq!(resolver.resolve(&ed_ref), Some(resolved_ed));
251
252 store.process_operation(MetadataOperation::delete(container_eid));
254
255 assert_eq!(resolver.resolve(&ed_ref), None);
256 }
257
258 #[test]
259 fn obeys_entity_limit() {
260 let mut store = ExternalDataStore::with_entity_limit(NonZeroUsize::new(2).unwrap());
262 let resolver = store.resolver();
263
264 let container_eid1 = entity_id_container("abcdef");
266 let container_eid2 = entity_id_container("bcdefg");
267 let container_eid3 = entity_id_container("cdefgh");
268 let (raw_ed1, ed1, resolved_ed1) = build_external_data("1234", "redis", &container_eid1, false);
269 let (raw_ed2, ed2, resolved_ed2) = build_external_data("1234", "init-volume", &container_eid2, true);
270 let (raw_ed3, ed3, resolved_ed3) = build_external_data("1234", "chmod-dir", &container_eid3, true);
271 let ed_ref1 = RawExternalData::try_from_str(&raw_ed1).unwrap();
272 let ed_ref2 = RawExternalData::try_from_str(&raw_ed2).unwrap();
273 let ed_ref3 = RawExternalData::try_from_str(&raw_ed3).unwrap();
274
275 assert_eq!(resolver.resolve(&ed_ref1), None);
276 assert_eq!(resolver.resolve(&ed_ref2), None);
277 assert_eq!(resolver.resolve(&ed_ref3), None);
278
279 store.process_operation(MetadataOperation::attach_external_data(container_eid1.clone(), ed1));
281 store.process_operation(MetadataOperation::attach_external_data(container_eid2, ed2));
282 store.process_operation(MetadataOperation::attach_external_data(
283 container_eid3.clone(),
284 ed3.clone(),
285 ));
286
287 assert_eq!(resolver.resolve(&ed_ref1), Some(resolved_ed1));
290 assert_eq!(resolver.resolve(&ed_ref2), Some(resolved_ed2));
291 assert_eq!(resolver.resolve(&ed_ref3), None);
292
293 store.process_operation(MetadataOperation::delete(container_eid1));
295 assert_eq!(resolver.resolve(&ed_ref1), None);
296
297 store.process_operation(MetadataOperation::attach_external_data(container_eid3, ed3));
299 assert_eq!(resolver.resolve(&ed_ref3), Some(resolved_ed3));
300 }
301}