1use std::future::Future;
16use std::sync::Arc;
17use std::time::Duration;
18
19use datatypes::timestamp::TimestampNanosecond;
20use moka::future::Cache;
21
22use crate::error::{CacheLoadSnafu, MultiPipelineWithDiffSchemaSnafu, Result};
23use crate::etl::Pipeline;
24use crate::manager::PipelineVersion;
25use crate::table::EMPTY_SCHEMA_NAME;
26use crate::util::{generate_pipeline_cache_key, generate_pipeline_cache_key_suffix};
27
28const PIPELINES_CACHE_SIZE: u64 = 10000;
30
31pub(crate) struct PipelineCache {
39 pipelines: Cache<String, Arc<Pipeline>>,
40 original_pipelines: Cache<String, PipelineContent>,
41 failover_cache: Cache<String, PipelineContent>,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct PipelineContent {
48 pub name: String,
49 pub content: String,
50 pub version: TimestampNanosecond,
51 pub schema: String,
52}
53
54impl PipelineCache {
55 pub(crate) fn new(ttl: Duration) -> Self {
56 Self {
57 pipelines: Cache::builder()
58 .max_capacity(PIPELINES_CACHE_SIZE)
59 .time_to_live(ttl)
60 .name("pipelines")
61 .build(),
62 original_pipelines: Cache::builder()
63 .max_capacity(PIPELINES_CACHE_SIZE)
64 .time_to_live(ttl)
65 .name("original_pipelines")
66 .build(),
67 failover_cache: Cache::builder()
68 .max_capacity(PIPELINES_CACHE_SIZE)
69 .name("failover_cache")
70 .build(),
71 }
72 }
73
74 pub(crate) async fn get_pipeline_with(
76 &self,
77 schema: &str,
78 name: &str,
79 version: PipelineVersion,
80 init: impl Future<Output = Result<Arc<Pipeline>>>,
81 ) -> Result<Arc<Pipeline>> {
82 let key = generate_pipeline_cache_key(schema, name, version);
83 self.pipelines
84 .try_get_with(key, init)
85 .await
86 .map_err(|error| CacheLoadSnafu { error }.build())
87 }
88
89 pub(crate) async fn get_pipeline_str_with(
91 &self,
92 schema: &str,
93 name: &str,
94 version: PipelineVersion,
95 init: impl Future<Output = Result<PipelineContent>>,
96 ) -> Result<PipelineContent> {
97 let key = generate_pipeline_cache_key(schema, name, version);
98 self.original_pipelines
99 .try_get_with(key, init)
100 .await
101 .map_err(|error| CacheLoadSnafu { error }.build())
102 }
103
104 pub(crate) async fn get_failover_cache(
107 &self,
108 schema: &str,
109 name: &str,
110 version: PipelineVersion,
111 ) -> Result<Option<PipelineContent>> {
112 for key in [
113 generate_pipeline_cache_key(EMPTY_SCHEMA_NAME, name, version),
114 generate_pipeline_cache_key(schema, name, version),
115 ] {
116 if let Some(content) = self.failover_cache.get(&key).await {
117 return Ok(Some(content));
118 }
119 }
120
121 let suffix = generate_pipeline_cache_key_suffix(name, version);
123 let mut found = self
124 .failover_cache
125 .iter()
126 .filter(|(k, _)| k.ends_with(&suffix))
127 .collect::<Vec<_>>();
128
129 match found.len() {
130 0 => Ok(None),
131 1 => Ok(Some(found.remove(0).1)),
132 _ => MultiPipelineWithDiffSchemaSnafu {
133 name: name.to_string(),
134 current_schema: schema.to_string(),
135 schemas: found
136 .iter()
137 .filter_map(|(k, _)| k.split_once('/').map(|k| k.0))
138 .collect::<Vec<_>>()
139 .join(","),
140 }
141 .fail(),
142 }
143 }
144
145 pub(crate) async fn insert_failover_cache(&self, content: PipelineContent, with_latest: bool) {
146 let versioned =
147 generate_pipeline_cache_key(&content.schema, &content.name, Some(content.version));
148 let latest = generate_pipeline_cache_key(&content.schema, &content.name, None);
149
150 self.failover_cache.insert(versioned, content.clone()).await;
151 if with_latest {
152 self.failover_cache.insert(latest, content).await;
153 }
154 }
155
156 pub(crate) async fn on_pipeline_created(&self, content: PipelineContent) {
160 self.invalidate(&content.name, None).await;
161 self.insert_failover_cache(content, true).await;
162 }
163
164 pub(crate) async fn invalidate(&self, name: &str, version: PipelineVersion) {
167 let mut suffixes = vec![generate_pipeline_cache_key_suffix(name, None)];
168 if version.is_some() {
169 suffixes.push(generate_pipeline_cache_key_suffix(name, version));
170 }
171
172 let ks = self
173 .pipelines
174 .iter()
175 .map(|(k, _)| k)
176 .chain(self.original_pipelines.iter().map(|(k, _)| k))
177 .chain(self.failover_cache.iter().map(|(k, _)| k))
178 .filter(|k| suffixes.iter().any(|suffix| k.ends_with(suffix)))
179 .collect::<Vec<_>>();
180
181 for k in ks {
182 let k = k.as_str();
183 self.pipelines.invalidate(k).await;
184 self.original_pipelines.invalidate(k).await;
185 self.failover_cache.invalidate(k).await;
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use std::sync::atomic::{AtomicUsize, Ordering};
193
194 use tokio::sync::Barrier;
195
196 use super::*;
197
198 fn content_at(version: i64) -> PipelineContent {
200 PipelineContent {
201 name: "p".to_string(),
202 content: "transform:".to_string(),
203 version: TimestampNanosecond::new(version),
204 schema: EMPTY_SCHEMA_NAME.to_string(),
205 }
206 }
207
208 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
209 async fn test_concurrent_misses_run_one_loader() {
210 const CONCURRENCY: usize = 8;
211
212 let cache = Arc::new(PipelineCache::new(Duration::from_secs(60)));
213 let loads = Arc::new(AtomicUsize::new(0));
214 let barrier = Arc::new(Barrier::new(CONCURRENCY));
215
216 let handles = (0..CONCURRENCY)
217 .map(|_| {
218 let (cache, loads, barrier) = (cache.clone(), loads.clone(), barrier.clone());
219 tokio::spawn(async move {
220 barrier.wait().await;
221 cache
222 .get_pipeline_str_with("db", "p", None, async {
223 loads.fetch_add(1, Ordering::SeqCst);
224 tokio::time::sleep(Duration::from_millis(100)).await;
226 Ok(content_at(1))
227 })
228 .await
229 .unwrap()
230 })
231 })
232 .collect::<Vec<_>>();
233
234 for handle in handles {
235 assert_eq!(handle.await.unwrap(), content_at(1));
236 }
237 assert_eq!(loads.load(Ordering::SeqCst), 1);
238 }
239
240 #[tokio::test]
241 async fn test_delete_drops_version_pinned_entry() {
242 let cache = PipelineCache::new(Duration::from_secs(60));
243 let content = content_at(1);
244 let version = Some(content.version);
245
246 cache
247 .get_pipeline_str_with("db", "p", version, async { Ok(content.clone()) })
248 .await
249 .unwrap();
250
251 cache.invalidate("p", version).await;
252
253 let loads = AtomicUsize::new(0);
254 cache
255 .get_pipeline_str_with("db", "p", version, async {
256 loads.fetch_add(1, Ordering::SeqCst);
257 Ok(content.clone())
258 })
259 .await
260 .unwrap();
261 assert_eq!(loads.load(Ordering::SeqCst), 1);
262 }
263
264 #[tokio::test]
265 async fn test_create_drops_stale_latest_and_primes_failover() {
266 let cache = PipelineCache::new(Duration::from_secs(60));
267 let v2 = content_at(2);
268
269 cache
270 .get_pipeline_str_with("a", "p", None, async { Ok(content_at(1)) })
271 .await
272 .unwrap();
273 cache.insert_failover_cache(content_at(1), true).await;
274
275 cache.on_pipeline_created(v2.clone()).await;
276
277 let loads = AtomicUsize::new(0);
278 let cached = cache
279 .get_pipeline_str_with("a", "p", None, async {
280 loads.fetch_add(1, Ordering::SeqCst);
281 Ok(v2.clone())
282 })
283 .await
284 .unwrap();
285 assert_eq!(loads.load(Ordering::SeqCst), 1);
286 assert_eq!(cached.version, v2.version);
287
288 let failover = cache.get_failover_cache("b", "p", None).await.unwrap();
289 assert_eq!(failover.map(|c| c.version), Some(v2.version));
290 }
291
292 #[tokio::test]
293 async fn test_failover_serves_global_pipeline_to_unwarmed_schema() {
294 let cache = PipelineCache::new(Duration::from_secs(60));
295 let content = content_at(1);
296
297 cache.insert_failover_cache(content.clone(), true).await;
298
299 let found = cache.get_failover_cache("b", "p", None).await.unwrap();
300 assert_eq!(found, Some(content.clone()));
301
302 let schema_local = PipelineContent {
304 schema: "x".to_string(),
305 ..content_at(2)
306 };
307 cache.insert_failover_cache(schema_local, true).await;
308
309 let found = cache.get_failover_cache("b", "p", None).await.unwrap();
310 assert_eq!(found, Some(content));
311 }
312}