1use crate::canonical_message::{deserialize_u128, tracing_support::LazyMessageIds};
6use crate::event_store::{EventStore, EventStoreConsumer, RetentionPolicy};
7use crate::models::{FileConfig, FileConsumerMode, FileFormat};
8use crate::traits::{
9 ConsumerError, MessageConsumer, MessagePublisher, PublisherError, ReceivedBatch, SentBatch,
10};
11use crate::CanonicalMessage;
12use anyhow::Context;
13use async_trait::async_trait;
14use once_cell::sync::Lazy;
15use std::any::Any;
16use std::collections::HashMap;
17use std::io::Seek;
18use std::path::Path;
19use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
20use std::sync::{Arc, Mutex as StdMutex, Weak};
21use tokio::fs::{self, File, OpenOptions};
22use tokio::io::{self, AsyncBufReadExt, AsyncSeekExt, BufReader};
23use tokio::io::{AsyncWriteExt, BufWriter};
24use tokio::sync::Mutex;
25use tracing::{info, instrument, trace, warn};
26
27static FILE_LOCKS: Lazy<StdMutex<HashMap<String, Arc<Mutex<()>>>>> =
29 Lazy::new(|| StdMutex::new(HashMap::new()));
30
31fn get_file_lock(path: &str) -> Arc<Mutex<()>> {
32 let mut locks = FILE_LOCKS.lock().unwrap();
33 locks.retain(|_, v| Arc::strong_count(v) > 1);
34 locks
35 .entry(path.to_string())
36 .or_insert_with(|| Arc::new(Mutex::new(())))
37 .clone()
38}
39
40fn parse_delimiter(delimiter: Option<&str>) -> anyhow::Result<Vec<u8>> {
41 let bytes = match delimiter {
42 Some(s) if s.starts_with("0x") => {
43 let hex = s.trim_start_matches("0x");
44 if hex.len() != 2 {
45 return Err(anyhow::anyhow!(
46 "Hex delimiter must be 1 byte (2 hex chars)"
47 ));
48 }
49 (0..hex.len())
50 .step_by(2)
51 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16))
52 .collect::<Result<Vec<u8>, _>>()
53 .map_err(|e| anyhow::anyhow!("Invalid hex delimiter: {}", e))?
54 }
55 Some(s) => s.as_bytes().to_vec(),
56 None => vec![b'\n'],
57 };
58
59 if bytes.is_empty() {
60 return Err(anyhow::anyhow!("Delimiter cannot be empty"));
61 }
62
63 Ok(bytes)
64}
65
66async fn read_until_bytes<R: AsyncBufReadExt + Unpin>(
67 reader: &mut R,
68 delimiter: &[u8],
69 buf: &mut Vec<u8>,
70) -> std::io::Result<usize> {
71 if delimiter.len() == 1 {
72 return reader.read_until(delimiter[0], buf).await;
73 }
74 let last_byte = delimiter[delimiter.len() - 1];
75 let mut total_read = 0;
76 loop {
77 let n = reader.read_until(last_byte, buf).await?;
78 if n == 0 {
79 return Ok(total_read);
80 }
81 total_read += n;
82 if buf.len() >= delimiter.len() && &buf[buf.len() - delimiter.len()..] == delimiter {
83 return Ok(total_read);
84 }
85 }
86}
87
88#[derive(Clone)]
89pub struct FilePublisher {
90 path: String,
91 file_lock: Arc<Mutex<()>>,
92 delimiter: Vec<u8>,
93 format: FileFormat,
94}
95
96impl FilePublisher {
97 pub async fn new(config: &FileConfig) -> anyhow::Result<Self> {
98 let path_str = &config.path;
99 let path = Path::new(path_str);
100 if let Some(parent) = path.parent() {
101 tokio::fs::create_dir_all(parent).await.with_context(|| {
102 format!("Failed to create parent directory for file: {:?}", parent)
103 })?;
104 }
105
106 let _ = OpenOptions::new()
107 .create(true)
108 .append(true)
109 .open(&path)
110 .await
111 .with_context(|| format!("Failed to open or create file for writing: {}", path_str))?;
112
113 let file_lock = get_file_lock(path_str);
114 let delimiter = parse_delimiter(config.delimiter.as_deref())?;
115 let format = config.format.clone();
116
117 info!(path = %path_str, format = ?format, "File sink opened for appending");
118 Ok(Self {
119 path: path_str.to_string(),
120 file_lock,
121 delimiter,
122 format,
123 })
124 }
125}
126
127#[async_trait]
128impl MessagePublisher for FilePublisher {
129 #[instrument(skip_all, fields(batch_size = messages.len()), level = "debug")]
130 async fn send_batch(
131 &self,
132 messages: Vec<CanonicalMessage>,
133 ) -> Result<SentBatch, PublisherError> {
134 if messages.is_empty() {
135 return Ok(SentBatch::Ack);
136 }
137
138 trace!(count = messages.len(), path = %self.path, message_ids = ?LazyMessageIds(&messages), "Writing batch to file");
139 let _file_guard = self.file_lock.lock().await;
140
141 let file = OpenOptions::new()
146 .create(true)
147 .append(true)
148 .open(&self.path)
149 .await
150 .context("Failed to open file for writing batch")?;
151
152 let mut writer = BufWriter::new(file);
153 let mut failed_messages = Vec::new();
154
155 for msg in messages {
157 let serialized_msg = match self.format {
158 FileFormat::Raw => Ok(msg.payload.to_vec()),
159 FileFormat::Normal => {
160 if msg
161 .metadata
162 .get("mq_bridge.original_format")
163 .map(|s| s.as_str())
164 == Some("raw")
165 {
166 Ok(msg.payload.to_vec())
169 } else {
170 serde_json::to_vec(&msg)
171 }
172 }
173 FileFormat::Json => {
174 if let Ok(json_val) = serde_json::from_slice::<serde_json::Value>(&msg.payload)
175 {
176 #[derive(serde::Serialize)]
177 struct JsonWrapper<'a> {
178 #[serde(serialize_with = "crate::canonical_message::print_uuidv7")]
179 message_id: u128,
180 payload: serde_json::Value,
181 metadata: &'a HashMap<String, String>,
182 }
183 serde_json::to_vec(&JsonWrapper {
184 message_id: msg.message_id,
185 payload: json_val,
186 metadata: &msg.metadata,
187 })
188 } else {
189 serde_json::to_vec(&msg)
190 }
191 }
192 FileFormat::Text => {
193 if let Ok(text) = std::str::from_utf8(&msg.payload) {
194 #[derive(serde::Serialize)]
195 struct TextWrapper<'a> {
196 #[serde(serialize_with = "crate::canonical_message::print_uuidv7")]
197 message_id: u128,
198 payload: &'a str,
199 metadata: &'a HashMap<String, String>,
200 }
201 serde_json::to_vec(&TextWrapper {
202 message_id: msg.message_id,
203 payload: text,
204 metadata: &msg.metadata,
205 })
206 } else {
207 serde_json::to_vec(&msg)
208 }
209 }
210 };
211 let serialized_msg = match serialized_msg {
212 Ok(s) => s,
213 Err(e) => {
214 tracing::error!("Failed to serialize message for file sink: {}", e);
215 failed_messages.push((msg, PublisherError::NonRetryable(anyhow::anyhow!(e))));
216 continue;
217 }
218 };
219
220 if let Err(e) = writer.write_all(&serialized_msg).await {
221 tracing::error!("Failed to write message to file: {}", e);
222 failed_messages.push((msg, PublisherError::NonRetryable(anyhow::anyhow!(e))));
223 } else if let Err(e) = writer.write_all(&self.delimiter).await {
224 tracing::error!("Failed to write delimiter to file: {}", e);
225 return Err(PublisherError::NonRetryable(anyhow::anyhow!(e)));
226 }
227 }
228
229 writer
230 .flush()
231 .await
232 .context("Failed to flush file writer")?;
233 if failed_messages.is_empty() {
234 Ok(SentBatch::Ack)
235 } else {
236 Ok(SentBatch::Partial {
237 responses: None,
238 failed: failed_messages,
239 })
240 }
241 }
242
243 async fn flush(&self) -> anyhow::Result<()> {
244 Ok(())
245 }
246
247 fn as_any(&self) -> &dyn Any {
248 self
249 }
250}
251
252static FILE_EVENT_STORES: Lazy<Mutex<HashMap<String, Weak<EventStore>>>> =
253 Lazy::new(|| Mutex::new(HashMap::new()));
254
255struct FileFeedState {
256 lines_in_memory: usize,
258}
259
260async fn create_file_event_store(
263 path: &str,
264 delimiter: Vec<u8>,
265 format: FileFormat,
266) -> anyhow::Result<Arc<EventStore>> {
267 let path = path.to_string();
268 let feed_state = Arc::new(Mutex::new(FileFeedState { lines_in_memory: 0 }));
270
271 let file_op_lock = get_file_lock(&path);
273
274 let feed_state_clone = feed_state.clone();
275 let path_clone = path.clone();
276 let file_op_lock_clone = file_op_lock.clone();
277 let delimiter_clone = delimiter.clone();
278
279 let retention = RetentionPolicy {
280 gc_interval: std::time::Duration::ZERO,
281 ..Default::default()
282 };
283 let store = Arc::new(
287 EventStore::new(retention).with_drop_callback(move |events| {
288 let count = events.len();
290 if count == 0 {
291 return;
292 }
293 let state = feed_state_clone.clone();
294 let path = path_clone.clone();
295 let file_op_lock = file_op_lock_clone.clone();
296 let delimiter = delimiter_clone.clone();
297
298 tokio::spawn(async move {
299 let _guard = file_op_lock.lock().await;
301
302 {
303 let mut s = state.lock().await;
304 s.lines_in_memory = s.lines_in_memory.saturating_sub(count);
305 }
306
307 if let Err(e) = remove_lines_from_file(&path, count, &delimiter).await {
308 tracing::error!("Failed to remove lines from file {}: {}", path, e);
309 } else {
312 trace!("Removed {} lines from {}", count, path);
313 }
314 });
315 }),
316 );
317
318 let store_weak = Arc::downgrade(&store);
320 let path_clone = path.clone();
321 let feed_state_clone = feed_state.clone();
322 let file_op_lock_clone = file_op_lock.clone();
323 let format_clone = format;
324
325 tokio::spawn(async move {
326 let mut current_sleep = std::time::Duration::from_millis(1);
327 const MAX_SLEEP: std::time::Duration = std::time::Duration::from_millis(100);
328
329 loop {
330 let store_clone = match store_weak.upgrade() {
332 Some(s) => s,
333 None => break, };
335
336 let file_guard = Some(file_op_lock_clone.lock().await);
338
339 let mut state = feed_state_clone.lock().await;
340
341 let file_res = OpenOptions::new().read(true).open(&path_clone).await;
343 let mut file = match file_res {
344 Ok(f) => f,
345 Err(e) => {
346 tracing::error!("Failed to open file {}: {}", path_clone, e);
347 drop(state);
348 drop(file_guard);
349 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
350 continue;
351 }
352 };
353
354 let mut reader = BufReader::new(file);
358 let mut lines_skipped = 0;
359 let mut error = false;
360 let lines_to_skip = state.lines_in_memory;
361 while lines_skipped < lines_to_skip {
362 let mut buf = Vec::new();
363 match read_until_bytes(&mut reader, &delimiter, &mut buf).await {
364 Ok(0) => break, Ok(_) => lines_skipped += 1,
366 Err(e) => {
367 tracing::error!("Error skipping lines in {}: {}", path_clone, e);
368 error = true;
369 break;
370 }
371 }
372 }
373 if error {
374 drop(state);
375 drop(file_guard);
376 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
377 continue;
378 }
379 file = reader.into_inner();
380
381 drop(file_guard);
383
384 let mut reader = BufReader::new(file);
386 let mut lines_read = 0;
387 let mut batch = Vec::with_capacity(128);
388
389 loop {
390 let mut buffer = Vec::new();
391 match read_until_bytes(&mut reader, &delimiter, &mut buffer).await {
392 Ok(0) => break,
393 Ok(_) => {
394 if buffer.ends_with(&delimiter) {
395 buffer.truncate(buffer.len() - delimiter.len());
396 }
397 if delimiter.len() == 1 && delimiter[0] == b'\n' && buffer.ends_with(b"\r")
398 {
399 buffer.pop();
400 }
401 let msg = parse_message(&buffer, &format_clone);
402 batch.push(msg);
403 lines_read += 1;
404
405 state.lines_in_memory += 1;
406
407 if batch.len() >= 128 {
408 store_clone.append_batch(std::mem::take(&mut batch)).await;
409 batch.reserve(128);
410 }
411 }
412 Err(e) => {
413 tracing::error!("Error reading from {}: {}", path_clone, e);
414 break;
415 }
416 }
417 }
418
419 if !batch.is_empty() {
420 store_clone.append_batch(batch).await;
421 }
422
423 drop(state); if lines_read == 0 {
427 tokio::time::sleep(current_sleep).await;
428 current_sleep = std::cmp::min(current_sleep * 2, MAX_SLEEP);
429 } else {
430 current_sleep = std::time::Duration::from_millis(1);
431 }
432 }
433 });
434
435 Ok(store)
436}
437
438async fn remove_lines_from_file(path: &str, count: usize, delimiter: &[u8]) -> anyhow::Result<()> {
439 let unique_id = fast_uuid_v7::gen_id_str();
440 let temp_path = format!("{}.{}.tmp", path, unique_id);
441
442 let file = File::open(path).await?;
443 let mut reader = BufReader::new(file);
444 let temp_file = File::create(&temp_path).await?;
445 let mut writer = BufWriter::new(temp_file);
446
447 let mut lines_skipped = 0;
448 while lines_skipped < count {
449 let mut buf = Vec::new();
450 if read_until_bytes(&mut reader, delimiter, &mut buf).await? == 0 {
451 break;
452 }
453 lines_skipped += 1;
454 }
455
456 if let Err(e) = io::copy(&mut reader, &mut writer).await {
457 let _ = fs::remove_file(&temp_path).await;
458 return Err(e.into());
459 }
460
461 writer.flush().await?;
462 let temp_file = writer.into_inner();
463 temp_file.sync_all().await?;
464 drop(temp_file); drop(reader); fs::rename(&temp_path, path).await?;
468
469 if let Some(parent) = Path::new(path).parent() {
471 if let Ok(parent_dir) = File::open(parent).await {
472 let _ = parent_dir.sync_all().await;
473 }
474 }
475
476 Ok(())
477}
478
479struct FileTailConsumer {
480 msg_rx: async_channel::Receiver<Vec<CanonicalMessage>>,
481 buffer: Vec<CanonicalMessage>,
482 offset_file: Option<Arc<Mutex<tokio::fs::File>>>,
483 ready: Arc<AtomicBool>,
484}
485
486fn read_until_bytes_sync<R: std::io::BufRead>(
487 reader: &mut R,
488 delimiter: &[u8],
489 buf: &mut Vec<u8>,
490) -> std::io::Result<usize> {
491 if delimiter.len() == 1 {
492 return reader.read_until(delimiter[0], buf);
493 }
494 let last_byte = delimiter[delimiter.len() - 1];
495 let mut total_read = 0;
496 loop {
497 let n = reader.read_until(last_byte, buf)?;
498 if n == 0 {
499 return Ok(total_read);
500 }
501 total_read += n;
502 if buf.len() >= delimiter.len() && &buf[buf.len() - delimiter.len()..] == delimiter {
503 return Ok(total_read);
504 }
505 }
506}
507
508fn run_file_tail_task_sync(
509 path: String,
510 msg_tx: async_channel::Sender<Vec<CanonicalMessage>>,
511 initial_offset: u64,
512 group_id: Option<String>,
513 delimiter: Vec<u8>,
514 format: FileFormat,
515 ready: Arc<AtomicBool>,
516) {
517 let mut last_position: u64 = initial_offset;
518 let mut reader: Option<std::io::BufReader<std::fs::File>> = None;
519 let mut current_sleep = std::time::Duration::from_millis(1);
520 const MAX_SLEEP: std::time::Duration = std::time::Duration::from_millis(50);
521 let mut initialized = false;
522 const BATCH_SIZE: usize = 1024;
523 let mut buf = Vec::with_capacity(1024);
524
525 loop {
526 if reader.is_none() {
527 let mut file = match std::fs::OpenOptions::new().read(true).open(&path) {
528 Ok(f) => f,
529 Err(e) => {
530 tracing::error!("Failed to open {}: {}", path, e);
531 std::thread::sleep(std::time::Duration::from_secs(1));
532 continue;
533 }
534 };
535
536 if let Ok(metadata) = file.metadata() {
537 if metadata.len() < last_position {
538 tracing::warn!("File {} was truncated. Resetting position to 0.", path);
539 last_position = 0;
540 }
541 }
542
543 if let Err(e) = file.seek(std::io::SeekFrom::Start(last_position)) {
544 tracing::error!("Failed to seek in {}: {}", path, e);
545 last_position = 0; if let Err(e) = file.seek(std::io::SeekFrom::Start(0)) {
547 tracing::error!("Failed to reset seek to 0 in {}: {}", path, e);
548 std::thread::sleep(std::time::Duration::from_secs(1));
549 continue;
550 }
551 }
552
553 reader = Some(std::io::BufReader::with_capacity(128 * BATCH_SIZE, file));
554 if !initialized {
555 ready.store(true, Ordering::SeqCst);
556 initialized = true;
557 }
558 }
559
560 let mut batch = Vec::with_capacity(BATCH_SIZE);
561 let mut lines_read_in_batch = 0;
562
563 if let Some(r) = reader.as_mut() {
564 for _ in 0..BATCH_SIZE {
565 buf.clear();
566 match read_until_bytes_sync(r, &delimiter, &mut buf) {
567 Ok(0) => break, Ok(n) => {
569 last_position += n as u64;
570 if buf.ends_with(&delimiter) {
571 buf.truncate(buf.len() - delimiter.len());
572 }
573 if delimiter.len() == 1 && delimiter[0] == b'\n' && buf.ends_with(b"\r") {
574 buf.pop();
575 }
576 let mut msg = parse_message(&buf, &format);
577 if group_id.is_some() {
578 msg.metadata
579 .insert("file_offset".to_string(), last_position.to_string());
580 }
581 batch.push(msg);
582 lines_read_in_batch += 1;
583 }
584 Err(e) => {
585 tracing::error!("Error reading {}: {}", path, e);
586 reader = None; break;
588 }
589 }
590 }
591 }
592
593 if !batch.is_empty() {
594 if msg_tx.send_blocking(batch).is_err() {
595 break; }
597 current_sleep = std::time::Duration::from_millis(1);
598 }
599
600 if lines_read_in_batch == 0 {
601 std::thread::sleep(current_sleep);
603 current_sleep = std::cmp::min(current_sleep * 2, MAX_SLEEP);
604 reader = None;
606 }
607 }
608}
609
610struct FileQueueConsumer {
611 msg_rx: async_channel::Receiver<Vec<CanonicalMessage>>,
612 lines_in_memory: Arc<AtomicUsize>,
613 path: String,
614 file_lock: Arc<Mutex<()>>,
615 buffer: Arc<Mutex<Vec<CanonicalMessage>>>,
616 delimiter: Vec<u8>,
617 ready: Arc<AtomicBool>,
618}
619
620#[allow(clippy::too_many_arguments)]
621fn run_file_queue_task(
622 path: String,
623 msg_tx: async_channel::Sender<Vec<CanonicalMessage>>,
624 lines_in_memory: Arc<AtomicUsize>,
625 file_lock: Arc<Mutex<()>>,
626 runtime_handle: tokio::runtime::Handle,
627 delimiter: Vec<u8>,
628 format: FileFormat,
629 ready: Arc<AtomicBool>,
630) {
631 let mut current_sleep = std::time::Duration::from_millis(1);
632 const MAX_SLEEP: std::time::Duration = std::time::Duration::from_millis(100);
633 let mut initialized = false;
634 let mut buf = Vec::new();
635
636 loop {
637 buf.clear();
638 let mut batch = Vec::with_capacity(128);
639 let mut lines_read = 0;
640
641 {
642 let _guard = runtime_handle.block_on(file_lock.lock());
643 let skip_count = lines_in_memory.load(Ordering::SeqCst);
644
645 let file = match std::fs::OpenOptions::new().read(true).open(&path) {
646 Ok(f) => f,
647 Err(e) => {
648 tracing::error!("Failed to open {}: {}", path, e);
649 drop(_guard);
650 std::thread::sleep(std::time::Duration::from_secs(1));
651 continue;
652 }
653 };
654
655 let mut reader = std::io::BufReader::new(file);
656 let mut skipped = 0;
657 let mut error = false;
658
659 while skipped < skip_count {
660 buf.clear();
661 match read_until_bytes_sync(&mut reader, &delimiter, &mut buf) {
662 Ok(0) => break,
663 Ok(_) => skipped += 1,
664 Err(e) => {
665 tracing::error!("Error skipping lines in {}: {}", path, e);
666 error = true;
667 break;
668 }
669 }
670 }
671
672 if !error {
673 for _ in 0..128 {
674 buf.clear();
675 match read_until_bytes_sync(&mut reader, &delimiter, &mut buf) {
676 Ok(0) => break,
677 Ok(_) => {
678 if buf.ends_with(&delimiter) {
679 buf.truncate(buf.len() - delimiter.len());
680 }
681 if delimiter.len() == 1 && delimiter[0] == b'\n' && buf.ends_with(b"\r")
682 {
683 buf.pop();
684 }
685 batch.push(parse_message(&buf, &format));
686 lines_read += 1;
687 }
688 Err(_) => break,
689 }
690 }
691
692 if !initialized {
693 ready.store(true, Ordering::SeqCst);
694 initialized = true;
695 }
696 }
697 }
698
699 if lines_read > 0 {
700 lines_in_memory.fetch_add(lines_read, Ordering::SeqCst);
701 if msg_tx.send_blocking(batch).is_err() {
702 break;
703 }
704 current_sleep = std::time::Duration::from_millis(1);
705 } else {
706 std::thread::sleep(current_sleep);
707 current_sleep = std::cmp::min(current_sleep * 2, MAX_SLEEP);
708 }
709 }
710}
711
712enum ConsumerBackend {
713 EventStore(EventStoreConsumer),
714 Tail(FileTailConsumer),
715 Queue(FileQueueConsumer),
716}
717
718pub struct FileConsumer {
720 backend: ConsumerBackend,
721}
722
723impl FileConsumer {
724 pub async fn new(config: &FileConfig) -> anyhow::Result<Self> {
725 let delimiter = parse_delimiter(config.delimiter.as_deref())?;
726 let format = config.format.clone();
727 match &config.mode {
728 None | Some(FileConsumerMode::Consume { delete: false }) => {
729 Self::new_tail(&config.path, false, None, delimiter.clone(), format).await
730 }
731 Some(FileConsumerMode::Subscribe { delete: false }) => {
732 Self::new_tail(&config.path, true, None, delimiter.clone(), format).await
733 }
734 Some(FileConsumerMode::GroupSubscribe {
735 group_id,
736 read_from_tail,
737 }) => {
738 let start_at_end = *read_from_tail;
739 Self::new_tail(
740 &config.path,
741 start_at_end,
742 Some(group_id.clone()),
743 delimiter.clone(),
744 format,
745 )
746 .await
747 }
748 Some(FileConsumerMode::Consume { delete: true }) => {
749 let (msg_tx, msg_rx) = async_channel::bounded(100);
750 let file_lock = get_file_lock(&config.path);
751 let lines_in_memory = Arc::new(AtomicUsize::new(0));
752 let ready = Arc::new(AtomicBool::new(false));
753 let ready_clone = ready.clone();
754 let lines_clone = lines_in_memory.clone();
755 let lock_clone = file_lock.clone();
756 let runtime = tokio::runtime::Handle::current();
757 let path_clone = config.path.clone();
758
759 let delimiter_clone = delimiter.clone();
760 let format_clone = format.clone();
761 std::thread::spawn(move || {
762 run_file_queue_task(
763 path_clone,
764 msg_tx,
765 lines_clone,
766 lock_clone,
767 runtime,
768 delimiter_clone,
769 format_clone,
770 ready_clone,
771 );
772 });
773
774 info!(path = %config.path, mode = "queue (delete, optimized)", "File consumer connected");
775 Ok(Self {
776 backend: ConsumerBackend::Queue(FileQueueConsumer {
777 msg_rx,
778 lines_in_memory,
779 path: config.path.clone(),
780 file_lock,
781 buffer: Arc::new(Mutex::new(Vec::new())),
782 delimiter,
783 ready,
784 }),
785 })
786 }
787 Some(FileConsumerMode::Subscribe { delete: true }) => {
788 let key = format!(
789 "{}|subscribe|delete|{:?}|{:?}",
790 config.path, format, delimiter
791 );
792
793 let store = if let Some(store) = {
794 let mut stores = FILE_EVENT_STORES.lock().await;
795 stores.retain(|_, v| v.strong_count() > 0);
796 stores.get(&key).and_then(|w| w.upgrade())
797 } {
798 store
799 } else {
800 let created =
801 create_file_event_store(&config.path, delimiter.clone(), format).await?;
802 let mut stores = FILE_EVENT_STORES.lock().await;
803 let store = stores
804 .get(&key)
805 .and_then(|w| w.upgrade())
806 .unwrap_or_else(|| {
807 stores.insert(key.clone(), Arc::downgrade(&created));
808 created
809 });
810 store
811 };
812
813 let subscriber_id = format!("file-sub-{}", fast_uuid_v7::gen_id_str());
814 info!(path = %config.path, mode = "subscribe (delete)", subscriber_id = %subscriber_id, "File consumer connected");
815
816 Ok(Self {
817 backend: ConsumerBackend::EventStore(store.consumer(subscriber_id)),
818 })
819 }
820 }
821 }
822
823 async fn new_tail(
824 path: &str,
825 start_at_end: bool,
826 group_id: Option<String>,
827 delimiter: Vec<u8>,
828 format: FileFormat,
829 ) -> anyhow::Result<Self> {
830 let (msg_tx, msg_rx) = async_channel::bounded(100);
831 let mut initial_offset = 0;
832 let ready = Arc::new(AtomicBool::new(false));
833 let ready_clone = ready.clone();
834 let mut offset_file = None;
835
836 if let Some(gid) = &group_id {
837 let offset_path = format!("{}.{}.offset", path, gid);
838 if let Ok(content) = tokio::fs::read_to_string(&offset_path).await {
839 if let Ok(pos) = content.trim().parse::<u64>() {
840 initial_offset = pos;
841 info!(
842 "Restored offset {} for group {} from {}",
843 pos, gid, offset_path
844 );
845 }
846 }
847 let file = OpenOptions::new()
848 .write(true)
849 .create(true)
850 .truncate(false)
851 .open(&offset_path)
852 .await?;
853 offset_file = Some(Arc::new(Mutex::new(file)));
854 }
855
856 if initial_offset == 0 && start_at_end {
857 if let Ok(metadata) = tokio::fs::metadata(path).await {
858 initial_offset = metadata.len();
859 }
860 }
861
862 let path_clone = path.to_string();
863 let format_clone = format;
864 std::thread::spawn(move || {
865 run_file_tail_task_sync(
866 path_clone,
867 msg_tx,
868 initial_offset,
869 group_id,
870 delimiter,
871 format_clone,
872 ready_clone,
873 );
874 });
875
876 info!(path = %path, mode = "tail (no-delete, optimized)", "File consumer connected");
877
878 Ok(Self {
879 backend: ConsumerBackend::Tail(FileTailConsumer {
880 msg_rx,
881 buffer: Vec::new(),
882 offset_file,
883 ready,
884 }),
885 })
886 }
887
888 pub fn is_ready(&self) -> bool {
890 match &self.backend {
891 ConsumerBackend::EventStore(_) => true,
892 ConsumerBackend::Tail(c) => c.ready.load(Ordering::SeqCst),
893 ConsumerBackend::Queue(c) => c.ready.load(Ordering::SeqCst),
894 }
895 }
896}
897
898#[async_trait]
899impl MessageConsumer for FileConsumer {
900 async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
901 match &mut self.backend {
902 ConsumerBackend::EventStore(c) => c.receive_batch(max_messages).await,
903 ConsumerBackend::Tail(c) => {
904 if c.buffer.is_empty() {
905 match c.msg_rx.recv().await {
906 Ok(batch) => c.buffer = batch,
907 Err(_) => return Err(ConsumerError::EndOfStream),
908 }
909 }
910
911 while c.buffer.len() < max_messages {
913 match c.msg_rx.try_recv() {
914 Ok(mut next_batch) => c.buffer.append(&mut next_batch),
915 Err(_) => break, }
917 }
918
919 let count = std::cmp::min(c.buffer.len(), max_messages);
920 let messages: Vec<_> = c.buffer.drain(0..count).collect();
921
922 let commit: crate::traits::BatchCommitFunc = if let Some(offset_file) =
923 &c.offset_file
924 {
925 let offset_file = offset_file.clone();
926 let captured_messages = messages.clone();
927
928 Box::new(
929 move |dispositions: Vec<crate::traits::MessageDisposition>| {
930 Box::pin(async move {
931 let max_offset = dispositions
932 .iter()
933 .zip(captured_messages.iter())
934 .filter_map(|(d, m)| match d {
935 crate::traits::MessageDisposition::Ack
936 | crate::traits::MessageDisposition::Reply(_) => m
937 .metadata
938 .get("file_offset")
939 .and_then(|s| s.parse::<u64>().ok()),
940 _ => None,
941 })
942 .max();
943
944 if let Some(offset) = max_offset {
945 let mut file = offset_file.lock().await;
946 if let Err(e) = file.rewind().await {
947 tracing::error!("Failed to rewind offset file: {}", e);
948 } else if let Err(e) = file.set_len(0).await {
949 tracing::error!("Failed to truncate offset file: {}", e);
950 } else if let Err(e) =
951 file.write_all(offset.to_string().as_bytes()).await
952 {
953 tracing::error!("Failed to write offset file: {}", e);
954 } else if let Err(e) = file.flush().await {
955 tracing::error!("Failed to flush offset file: {}", e);
956 }
957 }
958 Ok(())
959 })
960 as crate::traits::BoxFuture<'static, anyhow::Result<()>>
961 },
962 )
963 } else {
964 Box::new(|_dispositions: Vec<crate::traits::MessageDisposition>| {
966 Box::pin(async move { Ok(()) })
967 as crate::traits::BoxFuture<'static, anyhow::Result<()>>
968 })
969 };
970
971 Ok(ReceivedBatch { messages, commit })
972 }
973 ConsumerBackend::Queue(c) => {
974 {
975 let buffer = c.buffer.lock().await;
976 if buffer.is_empty() {
977 drop(buffer);
978 match c.msg_rx.recv().await {
979 Ok(b) => c.buffer.lock().await.extend(b),
980 Err(_) => return Err(ConsumerError::EndOfStream),
981 }
982 }
983 }
984 let mut buffer = c.buffer.lock().await;
985
986 while buffer.len() < max_messages {
987 match c.msg_rx.try_recv() {
988 Ok(mut b) => buffer.append(&mut b),
989 Err(_) => break,
990 }
991 }
992
993 let count = std::cmp::min(buffer.len(), max_messages);
994 let batch: Vec<_> = buffer.drain(0..count).collect();
995 drop(buffer);
996
997 let path = c.path.clone();
998 let lock = c.file_lock.clone();
999 let buffer_clone = c.buffer.clone();
1000 let lines_mem = c.lines_in_memory.clone();
1001 let batch_for_commit = batch.clone();
1002 let delimiter = c.delimiter.clone();
1003
1004 let commit = Box::new(
1005 move |dispositions: Vec<crate::traits::MessageDisposition>| {
1006 Box::pin(async move {
1007 let mut leading_acks = 0;
1008 let mut nacked_msgs = Vec::new();
1009 let mut encountered_nack = false;
1010
1011 for (i, d) in dispositions.iter().enumerate() {
1012 if encountered_nack {
1013 if let Some(msg) = batch_for_commit.get(i) {
1014 nacked_msgs.push(msg.clone());
1015 }
1016 continue;
1017 }
1018 match d {
1019 crate::traits::MessageDisposition::Ack
1020 | crate::traits::MessageDisposition::Reply(_) => {
1021 leading_acks += 1;
1022 }
1023 crate::traits::MessageDisposition::Nack => {
1024 encountered_nack = true;
1025 if let Some(msg) = batch_for_commit.get(i) {
1026 nacked_msgs.push(msg.clone());
1027 }
1028 }
1029 }
1030 }
1031
1032 if !nacked_msgs.is_empty() {
1033 let mut buf = buffer_clone.lock().await;
1034 let old_buf = std::mem::take(&mut *buf);
1035 let mut new_buf = nacked_msgs;
1036 new_buf.extend(old_buf);
1037 *buf = new_buf;
1038 }
1039
1040 if leading_acks > 0 {
1041 let _guard = lock.lock().await;
1042 if let Err(e) =
1043 remove_lines_from_file(&path, leading_acks, &delimiter).await
1044 {
1045 tracing::error!("Failed to remove lines from {}: {}", path, e);
1046 }
1047 lines_mem.fetch_sub(leading_acks, Ordering::SeqCst);
1048 }
1049 Ok(())
1050 })
1051 as crate::traits::BoxFuture<'static, anyhow::Result<()>>
1052 },
1053 );
1054
1055 Ok(ReceivedBatch {
1056 messages: batch,
1057 commit,
1058 })
1059 }
1060 }
1061 }
1062
1063 fn as_any(&self) -> &dyn Any {
1064 self
1065 }
1066}
1067
1068fn parse_message(buffer: &[u8], format: &FileFormat) -> CanonicalMessage {
1069 match format {
1070 FileFormat::Raw => {
1071 let mut msg = CanonicalMessage::new(buffer.to_vec(), None);
1072 msg.metadata
1073 .insert("mq_bridge.original_format".to_string(), "raw".to_string());
1074 msg
1075 }
1076 FileFormat::Normal | FileFormat::Json | FileFormat::Text => {
1077 #[derive(serde::Deserialize)]
1078 struct AnyPayloadMessage {
1079 #[serde(deserialize_with = "deserialize_u128")]
1080 message_id: u128,
1081 payload: serde_json::Value,
1082 #[serde(default)]
1083 metadata: HashMap<String, String>,
1084 }
1085
1086 match serde_json::from_slice::<AnyPayloadMessage>(buffer) {
1087 Ok(wrapper) => {
1088 let payload_bytes = if matches!(format, FileFormat::Json) {
1089 serde_json::to_vec(&wrapper.payload).unwrap_or_default()
1090 } else {
1091 match wrapper.payload {
1092 serde_json::Value::String(s) => s.into_bytes(),
1093 serde_json::Value::Array(arr) => {
1094 if let Ok(bytes) = serde_json::from_value::<Vec<u8>>(
1095 serde_json::Value::Array(arr.clone()),
1096 ) {
1097 bytes
1098 } else {
1099 serde_json::to_vec(&serde_json::Value::Array(arr))
1100 .unwrap_or_default()
1101 }
1102 }
1103 other => serde_json::to_vec(&other).unwrap_or_default(),
1104 }
1105 };
1106 CanonicalMessage {
1107 message_id: wrapper.message_id,
1108 payload: payload_bytes.into(),
1109 metadata: wrapper.metadata,
1110 }
1111 }
1112 Err(e) => {
1113 warn!(error = %e, content_length = buffer.len(), "Failed to parse file line as JSON, treating as raw.");
1114 let mut msg = CanonicalMessage::new(buffer.to_vec(), None);
1115 msg.metadata
1116 .insert("mq_bridge.original_format".to_string(), "raw".to_string());
1117 msg
1118 }
1119 }
1120 }
1121 }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126 use crate::endpoints::file::{FileConsumer, FilePublisher};
1127 use crate::models::{FileConfig, FileConsumerMode, FileFormat};
1128 use crate::msg;
1129 use crate::traits::MessageConsumer;
1130 use crate::traits::MessagePublisher;
1131 use serde_json::json;
1132 use tempfile::tempdir;
1133 use tokio::fs::OpenOptions;
1134 use tokio::io::AsyncWriteExt;
1135
1136 #[tokio::test]
1137 async fn test_file_sink_and_source_integration() {
1138 let dir = tempdir().unwrap();
1140 let file_path = dir.path().join("test.log");
1141 let file_path_str = file_path.to_str().unwrap().to_string();
1142
1143 let config = FileConfig {
1145 path: file_path_str.clone(),
1146 ..Default::default()
1147 };
1148 let sink = FilePublisher::new(&config).await.unwrap();
1149
1150 let msg1 = msg!(json!({"hello": "world"}));
1151 let msg2 = msg!(json!({"foo": "bar"}));
1152
1153 sink.send_batch(vec![msg1.clone(), msg2.clone()])
1154 .await
1155 .unwrap();
1156 sink.flush().await.unwrap();
1158 drop(sink);
1160
1161 let mut source = FileConsumer::new(&config).await.unwrap();
1163
1164 let received1 = source.receive().await.unwrap();
1166 let _ = (received1.commit)(crate::traits::MessageDisposition::Ack).await; assert_eq!(received1.message.message_id, msg1.message_id);
1169 assert_eq!(received1.message.payload, msg1.payload);
1170
1171 let batch = source.receive_batch(1).await.unwrap();
1172 let (received_msgs, commit2) = (batch.messages, batch.commit);
1173 let len = received_msgs.len();
1174 let received_msg2 = received_msgs.into_iter().next().unwrap();
1175 let _ = commit2(vec![crate::traits::MessageDisposition::Ack; len]).await;
1176 assert_eq!(received_msg2.message_id, msg2.message_id);
1177 assert_eq!(received_msg2.payload, msg2.payload);
1178
1179 let result = tokio::time::timeout(
1181 std::time::Duration::from_millis(200),
1182 source.receive_batch(1),
1183 )
1184 .await;
1185 assert!(result.is_err(), "Expected timeout waiting for new data");
1186 }
1187
1188 #[tokio::test]
1189 async fn test_file_sink_creates_directory() {
1190 let dir = tempdir().unwrap();
1191 let nested_dir_path = dir.path().join("nested");
1192 let file_path = nested_dir_path.join("test.log");
1193
1194 let config = FileConfig {
1195 path: file_path.to_str().unwrap().to_string(),
1196 ..Default::default()
1197 };
1198 let sink_result = FilePublisher::new(&config).await;
1199
1200 assert!(sink_result.is_ok());
1201 assert!(nested_dir_path.exists());
1202 assert!(file_path.exists());
1203 }
1204
1205 #[tokio::test]
1206 async fn test_file_consumer_consume_mode() {
1207 let dir = tempdir().unwrap();
1208 let file_path = dir.path().join("consume.log");
1209 let file_path_str = file_path.to_str().unwrap().to_string();
1210
1211 tokio::fs::write(&file_path, b"line1\nline2\nline3\n")
1213 .await
1214 .unwrap();
1215
1216 let config = FileConfig {
1217 path: file_path_str,
1218 mode: Some(FileConsumerMode::Consume { delete: true }),
1219 ..Default::default()
1220 };
1221 let mut consumer = FileConsumer::new(&config).await.unwrap();
1222
1223 let received1 = consumer.receive().await.unwrap();
1225 assert_eq!(received1.message.payload.as_ref(), b"line1");
1226
1227 (received1.commit)(crate::traits::MessageDisposition::Ack)
1229 .await
1230 .unwrap();
1231
1232 let mut content = String::new();
1234 for _ in 0..20 {
1235 content = tokio::fs::read_to_string(&file_path).await.unwrap();
1236 if content == "line2\nline3\n" {
1237 break;
1238 }
1239 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1240 }
1241 assert_eq!(content, "line2\nline3\n");
1242
1243 let received2 = consumer.receive().await.unwrap();
1245 assert_eq!(received2.message.payload.as_ref(), b"line2");
1246 (received2.commit)(crate::traits::MessageDisposition::Ack)
1247 .await
1248 .unwrap();
1249
1250 let received3 = consumer.receive().await.unwrap();
1252 assert_eq!(received3.message.payload.as_ref(), b"line3");
1253 (received3.commit)(crate::traits::MessageDisposition::Ack)
1254 .await
1255 .unwrap();
1256
1257 for _ in 0..20 {
1259 content = tokio::fs::read_to_string(&file_path).await.unwrap();
1260 if content.is_empty() {
1261 break;
1262 }
1263 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1264 }
1265 assert_eq!(content, "");
1266 }
1267
1268 #[tokio::test]
1269 async fn test_file_consumer_nack_behavior() {
1270 let dir = tempdir().unwrap();
1271 let file_path = dir.path().join("nack.log");
1272 let file_path_str = file_path.to_str().unwrap().to_string();
1273
1274 tokio::fs::write(&file_path, b"msg1\nmsg2\n").await.unwrap();
1276
1277 let config = FileConfig {
1278 path: file_path_str.clone(),
1279 mode: Some(FileConsumerMode::Consume { delete: true }),
1280 ..Default::default()
1281 };
1282 let mut consumer = FileConsumer::new(&config).await.unwrap();
1283
1284 let batch1 = consumer.receive_batch(1).await.unwrap();
1286 assert_eq!(batch1.messages.len(), 1);
1287 assert_eq!(batch1.messages[0].payload.as_ref(), b"msg1");
1288
1289 (batch1.commit)(vec![crate::traits::MessageDisposition::Nack])
1291 .await
1292 .unwrap();
1293
1294 let batch2 = consumer.receive_batch(1).await.unwrap();
1296 assert_eq!(batch2.messages.len(), 1);
1297 assert_eq!(batch2.messages[0].payload.as_ref(), b"msg1");
1298
1299 (batch2.commit)(vec![crate::traits::MessageDisposition::Ack])
1301 .await
1302 .unwrap();
1303
1304 let batch3 = consumer.receive_batch(1).await.unwrap();
1306 assert_eq!(batch3.messages.len(), 1);
1307 assert_eq!(batch3.messages[0].payload.as_ref(), b"msg2");
1308 }
1309
1310 #[tokio::test]
1311 async fn test_file_consumer_consume_no_delete() {
1312 let dir = tempdir().unwrap();
1313 let file_path = dir.path().join("consume_no_delete.log");
1314 let file_path_str = file_path.to_str().unwrap().to_string();
1315
1316 tokio::fs::write(&file_path, b"line1\nline2\nline3\n")
1318 .await
1319 .unwrap();
1320
1321 let config = FileConfig {
1322 path: file_path_str.clone(),
1323 ..Default::default()
1324 };
1325 let mut consumer = FileConsumer::new(&config).await.unwrap();
1326
1327 let received1 = consumer.receive().await.unwrap();
1329 assert_eq!(received1.message.payload.as_ref(), b"line1");
1330
1331 (received1.commit)(crate::traits::MessageDisposition::Ack)
1333 .await
1334 .unwrap();
1335
1336 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1338
1339 let content = tokio::fs::read_to_string(&file_path).await.unwrap();
1341 assert_eq!(content, "line1\nline2\nline3\n");
1342
1343 let received2 = consumer.receive().await.unwrap();
1345 assert_eq!(received2.message.payload.as_ref(), b"line2");
1346 }
1347
1348 #[tokio::test]
1349 async fn test_file_consumer_subscribe_mode() {
1350 let dir = tempdir().unwrap();
1351 let file_path = dir.path().join("subscribe.log");
1352 let file_path_str = file_path.to_str().unwrap().to_string();
1353
1354 tokio::fs::write(&file_path, b"line1\n").await.unwrap();
1356
1357 let config = FileConfig {
1358 path: file_path_str.clone(),
1359 mode: Some(FileConsumerMode::Subscribe { delete: false }),
1360 ..Default::default()
1361 };
1362
1363 let mut consumer = FileConsumer::new(&config).await.unwrap();
1364
1365 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1367
1368 {
1370 let mut file = OpenOptions::new()
1371 .append(true)
1372 .open(&file_path)
1373 .await
1374 .unwrap();
1375 file.write_all(b"line2\n").await.unwrap();
1376 }
1377
1378 let received2 = consumer.receive_batch(2).await.unwrap();
1380 assert_eq!(received2.messages.len(), 1);
1381 assert_eq!(received2.messages[0].payload.as_ref(), b"line2");
1382 (received2.commit)(vec![crate::traits::MessageDisposition::Ack])
1383 .await
1384 .unwrap();
1385
1386 let content = tokio::fs::read_to_string(&file_path).await.unwrap();
1388 assert_eq!(content, "line1\nline2\n");
1389 }
1390
1391 #[tokio::test]
1392 async fn test_file_consumer_consume_explicit_delete() {
1393 let dir = tempdir().unwrap();
1394 let file_path = dir.path().join("consume_explicit_delete.log");
1395 let file_path_str = file_path.to_str().unwrap().to_string();
1396
1397 tokio::fs::write(&file_path, b"line1\n").await.unwrap();
1398
1399 let config = FileConfig {
1400 path: file_path_str.clone(),
1401 mode: Some(FileConsumerMode::Consume { delete: true }),
1402 ..Default::default()
1403 };
1404 let mut consumer = FileConsumer::new(&config).await.unwrap();
1405
1406 let received = consumer.receive().await.unwrap();
1407 assert_eq!(received.message.payload.as_ref(), b"line1");
1408
1409 (received.commit)(crate::traits::MessageDisposition::Ack)
1410 .await
1411 .unwrap();
1412
1413 let mut content = String::new();
1415 for _ in 0..20 {
1416 content = tokio::fs::read_to_string(&file_path).await.unwrap();
1417 if content.is_empty() {
1418 break;
1419 }
1420 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1421 }
1422 assert_eq!(content, "");
1423 }
1424
1425 #[tokio::test]
1426 async fn test_file_consumer_subscribe_with_delete() {
1427 let dir = tempdir().unwrap();
1428 let file_path = dir.path().join("subscribe_delete.log");
1429 let file_path_str = file_path.to_str().unwrap().to_string();
1430
1431 tokio::fs::write(&file_path, b"line1\n").await.unwrap();
1432
1433 let config = FileConfig {
1434 path: file_path_str.clone(),
1435 mode: Some(FileConsumerMode::Subscribe { delete: true }),
1436 ..Default::default()
1437 };
1438
1439 let mut sub1 = FileConsumer::new(&config).await.unwrap();
1440 let mut sub2 = FileConsumer::new(&config).await.unwrap();
1441
1442 let msg1 = sub1.receive().await.unwrap();
1443 assert_eq!(msg1.message.payload.as_ref(), b"line1");
1444
1445 let msg2 = sub2.receive().await.unwrap();
1446 assert_eq!(msg2.message.payload.as_ref(), b"line1");
1447
1448 (msg1.commit)(crate::traits::MessageDisposition::Ack)
1450 .await
1451 .unwrap();
1452
1453 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1454 let content = tokio::fs::read_to_string(&file_path).await.unwrap();
1455 assert_eq!(content, "line1\n");
1456
1457 (msg2.commit)(crate::traits::MessageDisposition::Ack)
1459 .await
1460 .unwrap();
1461
1462 let mut content = String::new();
1463 for _ in 0..20 {
1464 content = tokio::fs::read_to_string(&file_path).await.unwrap();
1465 if content.is_empty() {
1466 break;
1467 }
1468 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1469 }
1470 assert_eq!(content, "");
1471 }
1472
1473 #[tokio::test]
1474 async fn test_file_consumer_subscribe_explicit_no_delete() {
1475 let dir = tempdir().unwrap();
1476 let file_path = dir.path().join("subscribe_no_delete.log");
1477 let file_path_str = file_path.to_str().unwrap().to_string();
1478
1479 tokio::fs::write(&file_path, b"line1\n").await.unwrap();
1480
1481 let config = FileConfig {
1482 path: file_path_str.clone(),
1483 mode: Some(FileConsumerMode::Subscribe { delete: false }),
1484 ..Default::default()
1485 };
1486
1487 let mut consumer = FileConsumer::new(&config).await.unwrap();
1488 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1490 {
1491 let mut file = OpenOptions::new()
1492 .append(true)
1493 .open(&file_path)
1494 .await
1495 .unwrap();
1496 file.write_all(b"line2\n").await.unwrap();
1497 }
1498
1499 let received = consumer.receive().await.unwrap();
1500 assert_eq!(received.message.payload.as_ref(), b"line2");
1501
1502 (received.commit)(crate::traits::MessageDisposition::Ack)
1503 .await
1504 .unwrap();
1505
1506 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1507 let content = tokio::fs::read_to_string(&file_path).await.unwrap();
1508 assert_eq!(content, "line1\nline2\n");
1509 }
1510
1511 use crate::models::{Endpoint, EndpointType, Route};
1512
1513 #[tokio::test]
1514 async fn test_route_file_consume_explicit_delete() {
1515 let dir = tempdir().unwrap();
1516 let file_path = dir.path().join("route_consume_explicit_delete.log");
1517 let file_path_str = file_path.to_str().unwrap().to_string();
1518 tokio::fs::write(&file_path, b"msg1\n").await.unwrap();
1519
1520 let input = Endpoint::new(EndpointType::File(FileConfig {
1521 path: file_path_str.clone(),
1522 mode: Some(FileConsumerMode::Consume { delete: true }),
1523 ..Default::default()
1524 }));
1525 let output = Endpoint::new_memory("out_consume_explicit_delete", 10);
1526 let route = Route::new(input, output.clone());
1527
1528 let handle = route
1529 .run("test_route_consume_explicit_delete")
1530 .await
1531 .unwrap();
1532
1533 let channel = output.channel().unwrap();
1534 let mut received = Vec::new();
1536 for _ in 0..20 {
1537 if !channel.is_empty() {
1538 received = channel.drain_messages();
1539 break;
1540 }
1541 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1542 }
1543 assert_eq!(received.len(), 1);
1544 assert_eq!(&received[0].payload.to_vec(), b"msg1");
1545
1546 let mut content = String::new();
1548 for _ in 0..20 {
1549 content = tokio::fs::read_to_string(&file_path).await.unwrap();
1550 if content.is_empty() {
1551 break;
1552 }
1553 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1554 }
1555 assert_eq!(content, "");
1556
1557 handle.stop().await;
1558 }
1559
1560 #[tokio::test]
1561 async fn test_route_file_subscribe_with_delete() {
1562 let dir = tempdir().unwrap();
1563 let file_path = dir.path().join("route_subscribe_delete.log");
1564 let file_path_str = file_path.to_str().unwrap().to_string();
1565 tokio::fs::write(&file_path, b"msg1\n").await.unwrap();
1566
1567 let input = Endpoint::new(EndpointType::File(FileConfig {
1568 path: file_path_str.clone(),
1569 mode: Some(FileConsumerMode::Subscribe { delete: true }),
1570 ..Default::default()
1571 }));
1572 let output = Endpoint::new_memory("out_subscribe_delete", 10);
1573 let route = Route::new(input, output.clone());
1574
1575 let handle = route.run("test_route_subscribe_delete").await.unwrap();
1576
1577 let channel = output.channel().unwrap();
1578 let mut received = Vec::new();
1579 for _ in 0..20 {
1580 if !channel.is_empty() {
1581 received = channel.drain_messages();
1582 break;
1583 }
1584 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1585 }
1586 assert_eq!(received.len(), 1);
1587
1588 let mut content = String::new();
1590 for _ in 0..20 {
1591 content = tokio::fs::read_to_string(&file_path).await.unwrap();
1592 if content.is_empty() {
1593 break;
1594 }
1595 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1596 }
1597 assert_eq!(content, "");
1598
1599 handle.stop().await;
1600 }
1601
1602 #[tokio::test]
1603 async fn test_route_file_subscribe_explicit_no_delete() {
1604 let dir = tempdir().unwrap();
1605 let file_path = dir.path().join("route_subscribe_no_delete.log");
1606 let file_path_str = file_path.to_str().unwrap().to_string();
1607 tokio::fs::write(&file_path, b"msg1\n").await.unwrap();
1608
1609 let input = Endpoint::new(EndpointType::File(FileConfig {
1610 path: file_path_str.clone(),
1611 mode: Some(FileConsumerMode::Subscribe { delete: false }),
1612 ..Default::default()
1613 }));
1614 let output = Endpoint::new_memory("out_subscribe_no_delete", 10);
1615 let route = Route::new(input, output.clone());
1616
1617 let handle = route.run("test_route_subscribe_no_delete").await.unwrap();
1618
1619 let channel = output.channel().unwrap();
1620 let mut received = Vec::new();
1621 for _ in 0..20 {
1622 if !channel.is_empty() {
1623 received = channel.drain_messages();
1624 break;
1625 }
1626 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1627 }
1628 assert_eq!(received.len(), 0);
1629
1630 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1631 let content = tokio::fs::read_to_string(&file_path).await.unwrap();
1632 assert_eq!(content, "msg1\n");
1633
1634 handle.stop().await;
1635 }
1636
1637 #[tokio::test]
1638 async fn test_route_file_consume_all_lines() {
1639 let dir = tempdir().unwrap();
1640 let file_path = dir.path().join("consume_all.log");
1641 let file_path_str = file_path.to_str().unwrap().to_string();
1642
1643 let mut content = String::new();
1645 for i in 0..10 {
1646 content.push_str(&format!("msg{}\n", i));
1647 }
1648 tokio::fs::write(&file_path, content).await.unwrap();
1649
1650 let input = Endpoint::new(EndpointType::File(FileConfig {
1651 path: file_path_str.clone(),
1652 mode: Some(FileConsumerMode::Consume { delete: true }),
1653 ..Default::default()
1654 }));
1655 let output = Endpoint::new_memory("out_consume_all", 100);
1656 let route = Route::new(input, output.clone());
1657
1658 let handle = route.run("test_route_consume_all").await.unwrap();
1659
1660 let channel = output.channel().unwrap();
1661 let mut received_count = 0;
1663 for _ in 0..100 {
1664 received_count += channel.drain_messages().len();
1665 if received_count >= 10 {
1666 break;
1667 }
1668 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1669 }
1670 assert_eq!(received_count, 10);
1671
1672 let mut content = String::new();
1674 for _ in 0..40 {
1675 content = tokio::fs::read_to_string(&file_path).await.unwrap();
1676 if content.is_empty() {
1677 break;
1678 }
1679 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1680 }
1681 assert_eq!(content, "");
1682
1683 handle.stop().await;
1684 }
1685
1686 #[tokio::test]
1687 async fn test_file_consumer_group_id_persistence() {
1688 let dir = tempdir().unwrap();
1689 let file_path = dir.path().join("group_id.log");
1690 let file_path_str = file_path.to_str().unwrap().to_string();
1691 let offset_path = dir.path().join("group_id.log.my_group.offset");
1692
1693 tokio::fs::write(&file_path, b"msg1\nmsg2\n").await.unwrap();
1695
1696 let config = FileConfig {
1697 path: file_path_str.clone(),
1698 mode: Some(FileConsumerMode::GroupSubscribe {
1699 group_id: "my_group".to_string(),
1700 read_from_tail: false,
1701 }),
1702 ..Default::default()
1703 };
1704
1705 let mut consumer1 = FileConsumer::new(&config).await.unwrap();
1707 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1709
1710 let batch1 = consumer1.receive_batch(1).await.unwrap();
1711 assert_eq!(batch1.messages[0].payload.as_ref(), b"msg1");
1712
1713 (batch1.commit)(vec![crate::traits::MessageDisposition::Ack])
1715 .await
1716 .unwrap();
1717
1718 let offset_content = tokio::fs::read_to_string(&offset_path).await.unwrap();
1720 assert_eq!(offset_content, "5");
1721
1722 drop(consumer1);
1723
1724 let mut consumer2 = FileConsumer::new(&config).await.unwrap();
1726 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1727
1728 let batch2 = consumer2.receive_batch(1).await.unwrap();
1729 assert_eq!(batch2.messages[0].payload.as_ref(), b"msg2");
1730
1731 (batch2.commit)(vec![crate::traits::MessageDisposition::Ack])
1732 .await
1733 .unwrap();
1734
1735 let offset_content = tokio::fs::read_to_string(&offset_path).await.unwrap();
1737 assert_eq!(offset_content, "10");
1738 }
1739
1740 #[tokio::test]
1741 async fn test_file_consumer_group_id_init_from_start() {
1742 let dir = tempdir().unwrap();
1743 let file_path = dir.path().join("group_id_start.log");
1744 let file_path_str = file_path.to_str().unwrap().to_string();
1745
1746 tokio::fs::write(&file_path, b"msg1\nmsg2\n").await.unwrap();
1748
1749 let config = FileConfig {
1750 path: file_path_str.clone(),
1751 mode: Some(FileConsumerMode::GroupSubscribe {
1752 group_id: "my_group_start".to_string(),
1753 read_from_tail: false,
1754 }),
1755 ..Default::default()
1756 };
1757
1758 let mut consumer = FileConsumer::new(&config).await.unwrap();
1760 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1762
1763 let batch = consumer.receive_batch(2).await.unwrap();
1764 assert_eq!(batch.messages.len(), 2);
1765 assert_eq!(batch.messages[0].payload.as_ref(), b"msg1");
1766 assert_eq!(batch.messages[1].payload.as_ref(), b"msg2");
1767 }
1768
1769 #[tokio::test]
1770 async fn test_file_tail_concurrent_publish_and_consume() {
1771 let dir = tempdir().unwrap();
1779 let file_path = dir.path().join("concurrent.log");
1780 let file_path_str = file_path.to_str().unwrap().to_string();
1781
1782 tokio::fs::write(&file_path, b"msg0\n").await.unwrap();
1784
1785 let config = FileConfig {
1786 path: file_path_str.clone(),
1787 mode: Some(FileConsumerMode::Subscribe { delete: false }),
1788 ..Default::default()
1789 };
1790
1791 let mut consumer = FileConsumer::new(&config).await.unwrap();
1793 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1794
1795 let publisher_path = file_path_str.clone();
1797 let publish_handle = tokio::spawn(async move {
1798 let pub_config = FileConfig {
1799 path: publisher_path,
1800 mode: Some(FileConsumerMode::Subscribe { delete: false }),
1801 ..Default::default()
1802 };
1803 let publisher = FilePublisher::new(&pub_config).await.unwrap();
1804
1805 for i in 1..=100 {
1807 let msg = msg!(json!({"id": i, "data": format!("message_{}", i)}));
1808 publisher.send_batch(vec![msg]).await.unwrap();
1809 if i % 10 == 0 {
1811 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1812 }
1813 }
1814 });
1815
1816 let mut received_count = 0;
1818 let mut message_ids = Vec::new();
1819
1820 let expected_count = 100;
1822 let start = std::time::Instant::now();
1823
1824 while received_count < expected_count {
1825 if start.elapsed() > std::time::Duration::from_secs(10) {
1826 break;
1827 }
1828 match tokio::time::timeout(
1829 std::time::Duration::from_millis(200),
1830 consumer.receive_batch(10),
1831 )
1832 .await
1833 {
1834 Ok(Ok(batch)) => {
1835 for msg in &batch.messages {
1836 received_count += 1;
1837 if let Ok(json_msg) =
1838 serde_json::from_slice::<serde_json::Value>(&msg.payload)
1839 {
1840 if let Some(id) = json_msg.get("id").and_then(|v| v.as_i64()) {
1841 message_ids.push(id);
1842 }
1843 }
1844 }
1845 (batch.commit)(vec![
1846 crate::traits::MessageDisposition::Ack;
1847 batch.messages.len()
1848 ])
1849 .await
1850 .unwrap();
1851 }
1852 Ok(Err(_)) => break, Err(_) => continue, }
1855 }
1856
1857 publish_handle.await.unwrap();
1858
1859 assert_eq!(
1862 received_count, expected_count,
1863 "Expected {} messages, got {}. This may indicate file locking issues on this platform.",
1864 expected_count, received_count
1865 );
1866
1867 let final_content = tokio::fs::read_to_string(&file_path)
1869 .await
1870 .expect("File should still be readable after concurrent access");
1871 assert!(
1872 !final_content.is_empty(),
1873 "File should contain messages after concurrent access"
1874 );
1875 }
1876
1877 #[tokio::test]
1883 async fn test_file_subscribe_concurrent_external_write() {
1884 let dir = tempdir().unwrap();
1885 let file_path = dir.path().join("external_write.log");
1886 let file_path_str = file_path.to_str().unwrap().to_string();
1887
1888 tokio::fs::write(&file_path, b"").await.unwrap();
1890
1891 let config = FileConfig {
1892 path: file_path_str.clone(),
1893 mode: Some(FileConsumerMode::Subscribe { delete: false }),
1894 ..Default::default()
1895 };
1896
1897 let mut consumer = FileConsumer::new(&config).await.unwrap();
1898
1899 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1901
1902 let file_path_clone = file_path.clone();
1903 let write_task = tokio::spawn(async move {
1904 let mut file = OpenOptions::new()
1905 .append(true)
1906 .open(&file_path_clone)
1907 .await
1908 .unwrap();
1909
1910 for i in 0..5 {
1911 let line = format!("message {}\n", i);
1912 file.write_all(line.as_bytes()).await.unwrap();
1913 file.flush().await.unwrap();
1914 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1915 }
1916 });
1917
1918 for i in 0..5 {
1919 let received =
1920 tokio::time::timeout(std::time::Duration::from_secs(5), consumer.receive())
1921 .await
1922 .expect("Timed out waiting for message")
1923 .unwrap();
1924
1925 let expected_payload = format!("message {}", i);
1926 assert_eq!(received.message.get_payload_str().trim(), expected_payload);
1927 (received.commit)(crate::traits::MessageDisposition::Ack)
1928 .await
1929 .unwrap();
1930 }
1931
1932 write_task.await.unwrap();
1933 }
1934
1935 #[tokio::test]
1936 async fn test_file_custom_delimiter() {
1937 let dir = tempdir().unwrap();
1938 let file_path = dir.path().join("custom_delim.log");
1939 let file_path_str = file_path.to_str().unwrap().to_string();
1940
1941 let config = FileConfig {
1942 path: file_path_str.clone(),
1943 delimiter: Some("|".to_string()),
1944 mode: Some(FileConsumerMode::Consume { delete: false }),
1945 ..Default::default()
1946 };
1947
1948 let publisher = FilePublisher::new(&config).await.unwrap();
1949 let mut consumer = FileConsumer::new(&config).await.unwrap();
1950
1951 let msg1 = crate::CanonicalMessage::from("msg1").with_raw_format();
1952 let msg2 = crate::CanonicalMessage::from("msg2").with_raw_format();
1953
1954 publisher.send_batch(vec![msg1, msg2]).await.unwrap();
1955 publisher.flush().await.unwrap();
1956 drop(publisher); let content = tokio::fs::read_to_string(&file_path).await.unwrap();
1960 assert_eq!(content, "msg1|msg2|");
1961
1962 let received1 = consumer.receive().await.unwrap();
1963 assert_eq!(received1.message.get_payload_str(), "msg1");
1964
1965 let received2 = consumer.receive().await.unwrap();
1966 assert_eq!(received2.message.get_payload_str(), "msg2");
1967 }
1968
1969 #[tokio::test]
1970 async fn test_file_xml_delimiter() {
1971 let dir = tempdir().unwrap();
1972 let file_path = dir.path().join("xml_delim.log");
1973 let file_path_str = file_path.to_str().unwrap().to_string();
1974
1975 let config = FileConfig {
1976 path: file_path_str.clone(),
1977 delimiter: Some("</message>".to_string()),
1978 mode: Some(FileConsumerMode::Consume { delete: false }),
1979 ..Default::default()
1980 };
1981
1982 let publisher = FilePublisher::new(&config).await.unwrap();
1983 let mut consumer = FileConsumer::new(&config).await.unwrap();
1984
1985 let msg1 = crate::CanonicalMessage::from("<xml>content1").with_raw_format();
1986 let msg2 = crate::CanonicalMessage::from("<xml>content2").with_raw_format();
1987
1988 publisher.send_batch(vec![msg1, msg2]).await.unwrap();
1989 publisher.flush().await.unwrap();
1990 drop(publisher); let content = tokio::fs::read_to_string(&file_path).await.unwrap();
1994 assert_eq!(content, "<xml>content1</message><xml>content2</message>");
1995
1996 let received1 = consumer.receive().await.unwrap();
1997 assert_eq!(received1.message.get_payload_str(), "<xml>content1");
1998
1999 let received2 = consumer.receive().await.unwrap();
2000 assert_eq!(received2.message.get_payload_str(), "<xml>content2");
2001 }
2002
2003 #[tokio::test]
2004 async fn test_file_formats_and_fallbacks() {
2005 let dir = tempdir().unwrap();
2006
2007 let json_path = dir.path().join("json.log");
2009 let json_config = FileConfig {
2010 path: json_path.to_str().unwrap().to_string(),
2011 format: FileFormat::Json,
2012 ..Default::default()
2013 };
2014
2015 let json_publisher = FilePublisher::new(&json_config).await.unwrap();
2016 let mut json_consumer = FileConsumer::new(&json_config).await.unwrap();
2017
2018 let json_payload = json!({"key": "value", "num": 123});
2019 let msg = msg!(json_payload.clone());
2020
2021 json_publisher.send_batch(vec![msg.clone()]).await.unwrap();
2022 json_publisher.flush().await.unwrap();
2023 drop(json_publisher); let received = json_consumer.receive().await.unwrap();
2026 let received_json: serde_json::Value =
2027 serde_json::from_slice(&received.message.payload).unwrap();
2028 assert_eq!(received_json, json_payload);
2029 (received.commit)(crate::traits::MessageDisposition::Ack)
2030 .await
2031 .unwrap();
2032
2033 let text_path = dir.path().join("text.log");
2035 let text_config = FileConfig {
2036 path: text_path.to_str().unwrap().to_string(),
2037 format: FileFormat::Text,
2038 ..Default::default()
2039 };
2040
2041 let text_publisher = FilePublisher::new(&text_config).await.unwrap();
2042 let mut text_consumer = FileConsumer::new(&text_config).await.unwrap();
2043
2044 let text_payload = "Hello World";
2045 let msg = crate::CanonicalMessage::from(text_payload);
2046
2047 text_publisher.send_batch(vec![msg.clone()]).await.unwrap();
2048 text_publisher.flush().await.unwrap();
2049 drop(text_publisher);
2050
2051 let received = text_consumer.receive().await.unwrap();
2052 assert_eq!(received.message.get_payload_str(), text_payload);
2053 (received.commit)(crate::traits::MessageDisposition::Ack)
2054 .await
2055 .unwrap();
2056
2057 {
2060 let mut file = OpenOptions::new()
2061 .append(true)
2062 .open(&json_path)
2063 .await
2064 .unwrap();
2065 file.write_all(b"Not a JSON wrapper\n").await.unwrap();
2066 }
2067
2068 let received_fallback = json_consumer.receive().await.unwrap();
2069 assert_eq!(
2071 received_fallback.message.get_payload_str(),
2072 "Not a JSON wrapper"
2073 );
2074 assert_eq!(
2075 received_fallback
2076 .message
2077 .metadata
2078 .get("mq_bridge.original_format")
2079 .map(|s| s.as_str()),
2080 Some("raw")
2081 );
2082 }
2083}