1use std::collections::VecDeque;
47use std::fs::{File, OpenOptions};
48use std::io::{Read, Seek, SeekFrom, Write};
49use std::path::{Path, PathBuf};
50use std::sync::OnceLock;
51use std::time::Duration;
52
53use crate::function::AppError;
54use crate::platform::Platform;
55use crate::util::app_config_reader::AppConfigReader;
56
57pub const MEMORY_BUFFER: u64 = 20;
60
61const LENGTH_PREFIX: usize = 4;
62const SEGMENT_PREFIX: &str = "eq-";
63const SEGMENT_SUFFIX: &str = ".dat";
64const DEFAULT_SEGMENT_BYTES: u64 = 16 * 1024 * 1024;
65const MIN_SEGMENT_BYTES: u64 = 512;
66const SEGMENT_SIZE_CONFIG: &str = "elastic.queue.segment.size.bytes";
67const RUNNING: &str = "RUNNING";
68
69static BASE_DIR: OnceLock<PathBuf> = OnceLock::new();
70
71const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(20);
72const EXPIRY: Duration = Duration::from_secs(60 * 60); pub fn base_dir() -> &'static Path {
77 BASE_DIR.get_or_init(|| {
78 let config = AppConfigReader::get_instance();
79 let tmp_root =
80 PathBuf::from(config.get_property_or("transient.data.store", "/tmp/reactive"));
81 let running_in_cloud = config.get_property_or("running.in.cloud", "false") == "true";
82 let dir = if running_in_cloud {
83 tmp_root
84 } else {
85 tmp_root.join(format!("{}-{}", Platform::name(), Platform::origin()))
86 };
87 if !dir.exists() {
88 if let Err(e) = std::fs::create_dir_all(&dir) {
89 log::error!("Unable to create {} - {e}", dir.display());
90 } else {
91 log::info!("{} created", dir.display());
92 }
93 }
94 write_running_marker(&dir);
95 purge_leftover_segments(&dir, None);
96 log::info!("Elastic file store ready ({})", dir.display());
97 dir
98 })
99}
100
101fn write_running_marker(dir: &Path) {
103 let epoch_ms = std::time::SystemTime::now()
104 .duration_since(std::time::UNIX_EPOCH)
105 .map(|d| d.as_millis().to_string())
106 .unwrap_or_default();
107 let _ = std::fs::write(dir.join(RUNNING), epoch_ms);
108}
109
110pub fn start_housekeeping() {
116 static STARTED: OnceLock<()> = OnceLock::new();
117 STARTED.get_or_init(|| {
118 let dir = base_dir().to_path_buf();
119 let running_in_cloud =
120 AppConfigReader::get_instance().get_property_or("running.in.cloud", "false") == "true";
121 if !running_in_cloud {
122 scan_expired_stores();
123 }
124 tokio::spawn(async move {
125 loop {
126 tokio::time::sleep(KEEP_ALIVE_INTERVAL).await;
127 write_running_marker(&dir);
128 }
129 });
130 });
131}
132
133pub fn scan_expired_stores() {
138 let current = base_dir();
139 let Some(tmp_root) = current.parent() else {
140 return;
141 };
142 let Ok(entries) = std::fs::read_dir(tmp_root) else {
143 return;
144 };
145 for entry in entries.flatten() {
146 let folder = entry.path();
147 if !folder.is_dir() || folder == current {
148 continue;
149 }
150 let marker = folder.join(RUNNING);
151 if marker.exists() {
152 let stale = std::fs::metadata(&marker)
153 .and_then(|m| m.modified())
154 .ok()
155 .and_then(|modified| modified.elapsed().ok())
156 .is_some_and(|age| age > EXPIRY);
157 if stale && has_segment_files(&folder) && std::fs::remove_dir_all(&folder).is_ok() {
158 log::info!("Elastic file holding area {} expired", folder.display());
159 }
160 } else if has_segment_files(&folder) && std::fs::remove_dir_all(&folder).is_ok() {
161 log::warn!(
162 "Unknown elastic file holding area {} removed",
163 folder.display()
164 );
165 }
166 }
167}
168
169fn has_segment_files(folder: &Path) -> bool {
170 std::fs::read_dir(folder)
171 .map(|entries| {
172 entries.flatten().any(|e| {
173 let name = e.file_name().to_string_lossy().to_string();
174 e.path().is_file()
175 && name.starts_with(SEGMENT_PREFIX)
176 && name.ends_with(SEGMENT_SUFFIX)
177 })
178 })
179 .unwrap_or(false)
180}
181
182pub fn shutdown_cleanup() {
186 let dir = base_dir();
187 purge_leftover_segments(dir, None);
188 if let Err(e) = std::fs::remove_file(dir.join(RUNNING)) {
189 log::debug!("Unable to delete {RUNNING} marker - {e}");
190 }
191}
192
193fn purge_leftover_segments(dir: &Path, id_prefix: Option<&str>) {
196 let prefix = match id_prefix {
197 Some(id) => format!("{SEGMENT_PREFIX}{id}-"),
198 None => SEGMENT_PREFIX.to_string(),
199 };
200 if let Ok(entries) = std::fs::read_dir(dir) {
201 for entry in entries.flatten() {
202 let name = entry.file_name().to_string_lossy().to_string();
203 if name.starts_with(&prefix) && name.ends_with(SEGMENT_SUFFIX) {
204 if let Err(e) = std::fs::remove_file(entry.path()) {
205 log::debug!("Unable to delete leftover {name} - {e}");
206 }
207 }
208 }
209 }
210}
211
212fn sanitize(s: &str) -> String {
213 s.chars()
214 .map(|c| {
215 if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
216 c
217 } else {
218 '_'
219 }
220 })
221 .collect()
222}
223
224struct Segment {
227 index: u32,
228 path: PathBuf,
229 file: Option<File>,
230 write_pos: u64,
231 read_pos: u64,
232 records_written: u64,
233 records_read: u64,
234 sealed: bool,
235}
236
237impl Segment {
238 fn open(path: PathBuf, index: u32) -> Result<Self, AppError> {
239 let file = OpenOptions::new()
240 .create(true)
241 .read(true)
242 .write(true)
243 .truncate(true)
244 .open(&path)
245 .map_err(|e| {
246 AppError::new(
247 500,
248 format!("Unable to open elastic segment {}: {e}", path.display()),
249 )
250 })?;
251 Ok(Segment {
252 index,
253 path,
254 file: Some(file),
255 write_pos: 0,
256 read_pos: 0,
257 records_written: 0,
258 records_read: 0,
259 sealed: false,
260 })
261 }
262
263 fn handle(&mut self) -> Result<&mut File, AppError> {
266 if self.file.is_none() {
267 let file = OpenOptions::new()
268 .read(true)
269 .write(true)
270 .open(&self.path)
271 .map_err(|e| {
272 AppError::new(
273 500,
274 format!(
275 "Unable to reopen elastic segment {}: {e}",
276 self.path.display()
277 ),
278 )
279 })?;
280 self.file = Some(file);
281 }
282 Ok(self.file.as_mut().expect("segment file just ensured"))
283 }
284
285 fn close_quietly(&mut self) {
286 self.file = None; }
288
289 fn close_and_delete(&mut self) {
290 self.close_quietly();
291 if let Err(e) = std::fs::remove_file(&self.path) {
292 log::debug!("Unable to delete segment {} - {e}", self.path.display());
293 }
294 }
295}
296
297pub struct ElasticQueue {
301 id: String,
302 safe_id: String,
303 segment_bytes: u64,
304 memory: VecDeque<Vec<u8>>,
305 segments: VecDeque<Segment>,
306 read_counter: u64,
307 write_counter: u64,
308 empty: bool,
309 peeked: Option<Vec<u8>>,
310 generation: u32,
311}
312
313impl ElasticQueue {
314 pub fn new(id: &str) -> Self {
316 let safe_id = sanitize(id);
317 let segment_bytes = AppConfigReader::get_instance()
318 .get_property_or(SEGMENT_SIZE_CONFIG, &DEFAULT_SEGMENT_BYTES.to_string())
319 .parse::<u64>()
320 .unwrap_or(DEFAULT_SEGMENT_BYTES)
321 .max(MIN_SEGMENT_BYTES);
322 base_dir(); ElasticQueue {
324 id: id.to_string(),
325 safe_id,
326 segment_bytes,
327 memory: VecDeque::new(),
328 segments: VecDeque::new(),
329 read_counter: 0,
330 write_counter: 0,
331 empty: true,
332 peeked: None,
333 generation: 0,
334 }
335 }
336
337 pub fn id(&self) -> &str {
338 &self.id
339 }
340
341 pub fn read_counter(&self) -> u64 {
342 self.read_counter
343 }
344
345 pub fn write_counter(&self) -> u64 {
346 self.write_counter
347 }
348
349 pub fn is_closed(&self) -> bool {
351 self.write_counter == 0
352 }
353
354 pub fn write(&mut self, event: &[u8]) -> Result<(), AppError> {
357 if event.is_empty() {
358 return Ok(());
359 }
360 if self.write_counter < MEMORY_BUFFER {
361 self.memory.push_back(event.to_vec());
363 } else {
364 self.append_to_disk(event)?;
365 }
366 self.write_counter += 1;
367 self.empty = false;
368 Ok(())
369 }
370
371 pub fn peek(&mut self) -> Result<Vec<u8>, AppError> {
373 if let Some(held) = &self.peeked {
374 return Ok(held.clone());
375 }
376 let next = self.read()?;
377 if !next.is_empty() {
378 self.peeked = Some(next.clone());
379 }
380 Ok(next)
381 }
382
383 pub fn read(&mut self) -> Result<Vec<u8>, AppError> {
386 if let Some(held) = self.peeked.take() {
387 return Ok(held);
388 }
389 if self.read_counter >= self.write_counter {
390 self.close();
392 return Ok(Vec::new());
393 }
394 if self.read_counter < MEMORY_BUFFER {
395 if let Some(event) = self.memory.pop_front() {
396 self.read_counter += 1;
397 return Ok(event);
398 }
399 return Ok(Vec::new());
400 }
401 let event = self.read_from_disk()?;
402 if !event.is_empty() {
403 self.read_counter += 1;
404 }
405 Ok(event)
406 }
407
408 pub fn close(&mut self) {
410 self.peeked = None;
411 if !self.empty {
412 self.empty = true;
413 self.read_counter = 0;
414 self.write_counter = 0;
415 self.memory.clear();
416 for segment in &mut self.segments {
417 segment.close_and_delete();
418 }
419 self.segments.clear();
420 self.generation += 1;
421 }
422 }
423
424 pub fn destroy(&mut self) {
427 self.close();
428 purge_leftover_segments(base_dir(), Some(&self.safe_id));
429 }
430
431 fn append_to_disk(&mut self, event: &[u8]) -> Result<(), AppError> {
432 let needs_new_tail = match self.segments.back() {
433 None => true,
434 Some(tail) => tail.sealed,
435 };
436 if needs_new_tail {
437 let index = self.segments.back().map_or(0, |t| t.index + 1);
438 let path = base_dir().join(format!(
439 "{SEGMENT_PREFIX}{}-{}-{}{SEGMENT_SUFFIX}",
440 self.safe_id, self.generation, index
441 ));
442 self.segments.push_back(Segment::open(path, index)?);
443 }
444 let single_segment = self.segments.len() == 1;
445 let segment_bytes = self.segment_bytes;
446 let tail = self.segments.back_mut().expect("tail segment just ensured");
447 let mut record = Vec::with_capacity(LENGTH_PREFIX + event.len());
450 record.extend_from_slice(&(event.len() as u32).to_be_bytes());
451 record.extend_from_slice(event);
452 let write_pos = tail.write_pos;
453 let file = tail.handle()?;
454 file.seek(SeekFrom::Start(write_pos))
455 .and_then(|_| file.write_all(&record))
456 .map_err(|e| AppError::new(500, format!("Elastic spill write failed - {e}")))?;
457 tail.write_pos += record.len() as u64;
458 tail.records_written += 1;
459 if tail.write_pos >= segment_bytes {
460 tail.sealed = true;
461 if !single_segment {
464 tail.close_quietly();
465 }
466 }
467 Ok(())
468 }
469
470 fn read_from_disk(&mut self) -> Result<Vec<u8>, AppError> {
471 let Some(head) = self.segments.front_mut() else {
472 log::error!(
473 "Missing segment for {} at read position {}",
474 self.id,
475 self.read_counter
476 );
477 return Ok(Vec::new());
478 };
479 let read_pos = head.read_pos;
480 let file = head.handle()?;
481 let mut len_buf = [0u8; LENGTH_PREFIX];
482 file.seek(SeekFrom::Start(read_pos))
483 .and_then(|_| file.read_exact(&mut len_buf))
484 .map_err(|e| AppError::new(500, format!("Elastic spill read failed - {e}")))?;
485 let len = u32::from_be_bytes(len_buf) as usize;
486 let mut payload = vec![0u8; len];
487 file.read_exact(&mut payload)
488 .map_err(|e| AppError::new(500, format!("Elastic spill read failed - {e}")))?;
489 head.read_pos += (LENGTH_PREFIX + len) as u64;
490 head.records_read += 1;
491 if head.sealed && head.records_read >= head.records_written {
494 head.close_and_delete();
495 self.segments.pop_front();
496 }
497 Ok(payload)
498 }
499}
500
501impl Drop for ElasticQueue {
502 fn drop(&mut self) {
503 self.destroy();
504 }
505}