1use crate::bus::{partition_for, BusError, EventBus, PublishAck, StoredEvent};
18use crate::manifest::BridgeManifest;
19use parking_lot::Mutex;
20use std::collections::HashMap;
21use std::fs;
22use std::io::{BufRead, BufReader, Write};
23use std::path::{Path, PathBuf};
24
25struct Partition {
26 path: PathBuf,
27 watermark_path: PathBuf,
28 idempotency_path: PathBuf,
29 seen_event_uids: HashMap<String, u64>,
30 next_offset: u64,
31 writer: fs::File,
32}
33
34#[derive(serde::Serialize, serde::Deserialize)]
35struct EventUidOffset {
36 event_uid: String,
37 offset: u64,
38}
39
40pub struct EmbeddedBroker {
42 data_dir: PathBuf,
43 manifest: BridgeManifest,
44 #[cfg(feature = "cold-store")]
45 cold_archive: Option<crate::cold_store::ColdArchive>,
46 partitions: HashMap<String, Vec<Mutex<Partition>>>,
48 validators: HashMap<String, jsonschema::Validator>,
49 pub recovered_torn_lines: u64,
51}
52
53impl EmbeddedBroker {
54 pub fn provision(data_dir: &Path, manifest: &BridgeManifest) -> Result<Self, BusError> {
57 manifest
58 .validate()
59 .map_err(|e| BusError::Backend(e.to_string()))?;
60 let manifest_path = data_dir.join("manifest.yaml");
61 fs::create_dir_all(data_dir)?;
62 copy_referenced_schemas(data_dir, manifest)?;
63 let yaml = manifest.to_yaml().map_err(|e| BusError::Backend(e.to_string()))?;
64 use std::io::Write as _;
77 let tmp = data_dir.join(format!("manifest.yaml.{}.tmp", av_core::new_event_uid()));
78 struct TmpGuard {
79 path: Option<std::path::PathBuf>,
80 }
81 impl TmpGuard {
82 fn disarm(&mut self) {
83 self.path = None;
84 }
85 }
86 impl Drop for TmpGuard {
87 fn drop(&mut self) {
88 if let Some(path) = self.path.take() {
89 let _ = fs::remove_file(&path);
90 }
91 }
92 }
93 let mut guard = TmpGuard {
94 path: Some(tmp.clone()),
95 };
96 {
97 let mut file = fs::File::create(&tmp)?;
98 file.write_all(yaml.as_bytes())?;
99 file.sync_all()?;
100 }
101 match fs::hard_link(&tmp, &manifest_path) {
102 Ok(_) => {
103 let _ = fs::remove_file(&tmp);
108 guard.disarm();
109 }
110 Err(error) => {
111 if error.kind() == std::io::ErrorKind::AlreadyExists {
112 return Err(BusError::Backend(format!(
118 "bridge already provisioned at {}",
119 av_core::fsutil::basename(data_dir)
120 )));
121 }
122 return Err(BusError::Io(error));
123 }
124 }
125 av_core::fsutil::sync_directory(data_dir).map_err(BusError::Io)?;
126 for t in &manifest.topics {
127 fs::create_dir_all(data_dir.join("topics").join(&t.name))?;
128 }
129 Self::open(data_dir)
130 }
131
132 pub fn open(data_dir: &Path) -> Result<Self, BusError> {
135 let manifest_yaml = av_core::fsutil::read_capped_string(
139 &data_dir.join("manifest.yaml"),
140 av_core::fsutil::MAX_CONTROL_BYTES,
141 )?;
142 let manifest =
143 BridgeManifest::from_yaml(&manifest_yaml).map_err(|e| BusError::Backend(e.to_string()))?;
144 let mut partitions = HashMap::new();
145 let mut torn_total = 0u64;
146 for t in &manifest.topics {
147 let dir = data_dir.join("topics").join(&t.name);
148 fs::create_dir_all(&dir)?;
149 let mut parts = Vec::with_capacity(t.partitions as usize);
150 for p in 0..t.partitions {
151 let path = dir.join(format!("p{p}.jsonl"));
152 let watermark_path = dir.join(format!("p{p}.next-offset"));
153 let idempotency_path = dir.join(format!("p{p}.event-uids.jsonl"));
154 let (segment_offset, torn) = recover_segment(&path)?;
155 let persisted_offset = read_high_water(&watermark_path)?;
156 let mut seen_event_uids = recover_event_uids(&idempotency_path)?;
157 recover_segment_event_uids(&path, &mut seen_event_uids)?;
158 if !seen_event_uids.is_empty() {
178 let mut live_uids =
179 std::collections::HashSet::<String>::with_capacity(seen_event_uids.len());
180 let mut min_offset = u64::MAX;
181 let mut max_offset = 0u64;
182 let mut have_parseable_line = false;
183 if path.exists() {
184 for line in BufReader::new(fs::File::open(&path)?).lines() {
185 let Ok(line) = line else {
186 continue;
187 };
188 if line.is_empty() {
189 continue;
190 }
191 if let Ok(event) = serde_json::from_str::<StoredEvent>(&line) {
192 if let Some(uid) = event_uid_from_value(&event.value) {
193 live_uids.insert(uid.to_owned());
194 }
195 min_offset = min_offset.min(event.offset);
196 max_offset = max_offset.max(event.offset);
197 have_parseable_line = true;
198 }
199 }
200 }
201 let offset_range = if have_parseable_line {
202 Some((min_offset, max_offset))
203 } else {
204 None
205 };
206 let before = seen_event_uids.len();
207 seen_event_uids.retain(|uid, offset| {
208 if live_uids.contains(uid) {
209 return true;
210 }
211 match offset_range {
212 Some((lo, hi)) => *offset >= lo && *offset <= hi,
213 None => false,
214 }
215 });
216 if seen_event_uids.len() != before {
217 tracing::warn!(
218 topic = %t.name,
219 partition = p,
220 dropped = before - seen_event_uids.len(),
221 "sidecar UID→offset entries dropped: corresponding segment records \
222 absent (likely retention crash between segment rewrite and sidecar rewrite)"
223 );
224 }
225 }
226 torn_total += torn;
227 let segment_created = !path.exists();
238 let writer = fs::OpenOptions::new().create(true).append(true).open(&path)?;
239 if segment_created {
240 av_core::fsutil::sync_directory(&dir).map_err(BusError::Io)?;
241 }
242 parts.push(Mutex::new(Partition {
243 path,
244 watermark_path,
245 idempotency_path,
246 seen_event_uids,
247 next_offset: segment_offset.max(persisted_offset),
248 writer,
249 }));
250 }
251 partitions.insert(t.name.clone(), parts);
252 }
253 let validators = load_validators(data_dir, &manifest)?;
254 #[cfg(feature = "cold-store")]
255 let cold_archive = crate::cold_store::ColdArchive::from_manifest_with_pending_default(
256 &manifest,
257 Some(data_dir.join("cold-outbox")),
258 )?;
259 Ok(Self {
260 data_dir: data_dir.to_owned(),
261 manifest,
262 #[cfg(feature = "cold-store")]
263 cold_archive,
264 partitions,
265 validators,
266 recovered_torn_lines: torn_total,
267 })
268 }
269
270 pub fn manifest(&self) -> &BridgeManifest {
272 &self.manifest
273 }
274
275 pub fn data_dir(&self) -> &Path {
277 &self.data_dir
278 }
279
280 pub fn enforce_retention(&self, now_ms: u64) -> Result<u64, BusError> {
290 let mut expired_total = 0u64;
291 for t in &self.manifest.topics {
292 let cutoff =
293 now_ms.saturating_sub(u64::from(t.retention.hot_hours) * av_core::units::MS_PER_HOUR);
294 let Some(parts) = self.partitions.get(&t.name) else {
295 continue;
296 };
297 for p in parts {
298 let mut part = p.lock();
299 let (kept, expired) = split_by_time(&part.path, cutoff)?;
300 if expired.is_empty() {
301 continue;
302 }
303 expired_total += expired.len() as u64;
304 if let Some(cold) = &t.retention.cold_uri {
306 if cold.contains("://") {
307 #[cfg(feature = "cold-store")]
308 {
309 let archive = self.cold_archive.as_ref().ok_or_else(|| {
310 BusError::Backend(format!("cold archive for {:?} is unavailable", t.name))
311 })?;
312 for line in &expired {
313 let event: StoredEvent = serde_json::from_str(line)?;
314 archive.put(&t.name, &event)?;
315 }
316 }
317 #[cfg(not(feature = "cold-store"))]
318 return Err(BusError::Backend(format!(
319 "cold_uri {cold:?} requires feature cold-store"
320 )));
321 } else {
322 let partition = part
323 .path
324 .file_stem()
325 .and_then(std::ffi::OsStr::to_str)
326 .unwrap_or("partition");
327 let cold_dir = Path::new(cold).join(&t.name).join(partition);
328 fs::create_dir_all(&cold_dir)?;
329 for line in &expired {
330 let event: StoredEvent = serde_json::from_str(line)?;
331 write_cold_event_once(&cold_dir, &event)?;
332 }
333 av_core::fsutil::sync_directory(&cold_dir)?;
334 }
335 }
336 persist_high_water(&part.watermark_path, part.next_offset)?;
337 let tmp = part
342 .path
343 .with_extension(format!("jsonl.{}.tmp", av_core::new_event_uid()));
344 let mut guard = av_core::fsutil::TempPathGuard::new(tmp.clone());
349 {
350 let mut f = fs::File::create(&tmp)?;
351 for line in &kept {
352 f.write_all(line.as_bytes())?;
353 f.write_all(b"\n")?;
354 }
355 f.sync_all()?;
356 }
357 fs::rename(&tmp, &part.path)?;
358 guard.disarm();
359 if let Some(parent) = part.path.parent() {
360 av_core::fsutil::sync_directory(parent)?;
361 }
362 let survivors: std::collections::HashSet<u64> = kept
375 .iter()
376 .filter_map(|line| serde_json::from_str::<StoredEvent>(line).ok())
377 .map(|event| event.offset)
378 .collect();
379 let range = if survivors.is_empty() {
380 None
381 } else {
382 let (mut lo, mut hi) = (u64::MAX, 0u64);
385 for offset in &survivors {
386 lo = lo.min(*offset);
387 hi = hi.max(*offset);
388 }
389 Some((lo, hi))
390 };
391 let before = part.seen_event_uids.len();
392 part.seen_event_uids.retain(|_, offset| {
393 if survivors.contains(offset) {
394 return true;
395 }
396 match range {
397 Some((lo, hi)) => *offset >= lo && *offset <= hi,
398 None => false,
399 }
400 });
401 if part.seen_event_uids.len() != before {
402 let mut lines: Vec<(String, u64)> = part
406 .seen_event_uids
407 .iter()
408 .map(|(uid, offset)| (uid.clone(), *offset))
409 .collect();
410 lines.sort_by_key(|(_, offset)| *offset);
411 let mut sidecar = Vec::new();
412 for (uid, offset) in lines {
413 let mapping = serde_json::to_string(&EventUidOffset {
414 event_uid: uid,
415 offset,
416 })?;
417 sidecar.extend_from_slice(mapping.as_bytes());
418 sidecar.push(b'\n');
419 }
420 rewrite_atomic(&part.idempotency_path, &sidecar)?;
421 }
422 part.writer = fs::OpenOptions::new().append(true).open(&part.path)?;
423 }
424 }
425 Ok(expired_total)
426 }
427}
428
429fn write_cold_event_once(directory: &Path, event: &StoredEvent) -> Result<(), BusError> {
430 let path = directory.join(format!("{:020}.json", event.offset));
431 let bytes = serde_json::to_vec(event)?;
432 match fs::OpenOptions::new().write(true).create_new(true).open(&path) {
433 Ok(mut file) => {
434 file.write_all(&bytes)?;
435 file.sync_all()?;
436 Ok(())
437 }
438 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
439 if fs::read(&path)? == bytes {
440 Ok(())
441 } else {
442 Err(BusError::Backend(format!(
450 "cold object {} already exists with different content",
451 av_core::fsutil::basename(&path)
452 )))
453 }
454 }
455 Err(error) => Err(BusError::Io(error)),
456 }
457}
458
459fn copy_referenced_schemas(data_dir: &Path, manifest: &BridgeManifest) -> Result<(), BusError> {
460 for topic in &manifest.topics {
461 let Some(reference) = &topic.schema_ref else {
462 continue;
463 };
464 let value = crate::manifest::schema_document(reference)?;
465 jsonschema::validator_for(&value)
466 .map_err(|error| BusError::Backend(format!("invalid schema {reference:?}: {error}")))?;
467 let destination = data_dir.join(reference);
468 if let Some(parent) = destination.parent() {
469 fs::create_dir_all(parent)?;
470 }
471 fs::write(destination, serde_json::to_vec_pretty(&value)?)?;
472 }
473 Ok(())
474}
475
476fn load_validators(
477 data_dir: &Path,
478 manifest: &BridgeManifest,
479) -> Result<HashMap<String, jsonschema::Validator>, BusError> {
480 let mut validators = HashMap::new();
481 for topic in &manifest.topics {
482 let Some(reference) = &topic.schema_ref else {
483 continue;
484 };
485 let schema = crate::manifest::schema_document(reference).or_else(|_| {
486 let bytes =
491 av_core::fsutil::read_capped(&data_dir.join(reference), av_core::fsutil::MAX_CONTROL_BYTES)?;
492 serde_json::from_slice(&bytes).map_err(BusError::from)
493 })?;
494 let validator = jsonschema::validator_for(&schema)
495 .map_err(|error| BusError::Backend(format!("invalid schema {reference:?}: {error}")))?;
496 validators.insert(topic.name.clone(), validator);
497 }
498 Ok(validators)
499}
500
501fn recover_segment(path: &Path) -> Result<(u64, u64), BusError> {
503 if !path.exists() {
504 return Ok((0, 0));
505 }
506 let bytes = fs::read(path)?;
507 if bytes.is_empty() {
508 return Ok((0, 0));
509 }
510 let complete_len = match bytes.iter().rposition(|b| *b == b'\n') {
511 Some(pos) => pos + 1,
512 None => 0, };
514 let torn = usize::from(complete_len < bytes.len());
515 if torn == 1 {
516 rewrite_atomic(path, bytes.get(..complete_len).unwrap_or_default())?;
521 }
522 let complete = bytes.get(..complete_len).unwrap_or_default();
523 let mut next_offset = 0u64;
524 for line in complete
525 .split(|byte| *byte == b'\n')
526 .filter(|line| !line.is_empty())
527 {
528 match serde_json::from_slice::<StoredEvent>(line) {
529 Ok(event) => next_offset = next_offset.max(event.offset.saturating_add(1)),
530 Err(error) => {
534 tracing::warn!(
535 %error,
536 path = %av_core::fsutil::basename(path),
537 "skipping unparseable segment record during recovery",
538 );
539 }
540 }
541 }
542 Ok((next_offset, torn as u64))
543}
544
545fn recover_event_uids(path: &Path) -> Result<HashMap<String, u64>, BusError> {
546 if !path.exists() {
547 return Ok(HashMap::new());
548 }
549 let bytes = av_core::fsutil::read_capped(path, av_core::fsutil::MAX_ATIF_BYTES)?;
554 let complete_len = bytes
555 .iter()
556 .rposition(|byte| *byte == b'\n')
557 .map_or(0, |position| position + 1);
558 if complete_len < bytes.len() {
559 rewrite_atomic(path, bytes.get(..complete_len).unwrap_or_default())?;
560 }
561 let mut seen = HashMap::new();
562 for line in bytes
563 .get(..complete_len)
564 .unwrap_or_default()
565 .split(|byte| *byte == b'\n')
566 .filter(|line| !line.is_empty())
567 {
568 let mapping: EventUidOffset = match serde_json::from_slice(line) {
569 Ok(mapping) => mapping,
570 Err(error) => {
575 tracing::warn!(
576 %error,
577 path = %av_core::fsutil::basename(path),
578 "skipping unparseable event-uid sidecar record during recovery",
579 );
580 continue;
581 }
582 };
583 if let Some(existing) = seen.insert(mapping.event_uid.clone(), mapping.offset) {
584 if existing != mapping.offset {
585 tracing::warn!(
593 event_uid = %mapping.event_uid,
594 prior_offset = existing,
595 current_offset = mapping.offset,
596 "sidecar has duplicate UID entry; segment offset will win after full recovery"
597 );
598 }
599 }
600 }
601 Ok(seen)
602}
603
604fn recover_segment_event_uids(path: &Path, seen: &mut HashMap<String, u64>) -> Result<(), BusError> {
605 if !path.exists() {
606 return Ok(());
607 }
608 for line in BufReader::new(fs::File::open(path)?).lines() {
609 let line = line?;
610 if line.is_empty() {
611 continue;
612 }
613 let event: StoredEvent = match serde_json::from_str(&line) {
614 Ok(event) => event,
615 Err(error) => {
619 tracing::warn!(
620 %error,
621 path = %av_core::fsutil::basename(path),
622 "skipping unparseable segment record while rebuilding UID index",
623 );
624 continue;
625 }
626 };
627 let Some(uid) = event_uid_from_value(&event.value) else {
628 continue;
629 };
630 if let Some(existing) = seen.insert(uid.to_owned(), event.offset) {
631 if existing != event.offset {
632 tracing::warn!(
638 event_uid = uid,
639 prior_offset = existing,
640 current_offset = event.offset,
641 "duplicate UID between sidecar and segment; segment offset wins"
642 );
643 }
644 }
645 }
646 Ok(())
647}
648
649fn rewrite_atomic(path: &Path, bytes: &[u8]) -> Result<(), BusError> {
653 let parent = path
654 .parent()
655 .ok_or_else(|| BusError::Backend("atomic rewrite has no parent".to_owned()))?;
656 let tmp = path.with_extension(format!("jsonl.{}.tmp", av_core::new_event_uid()));
657 let mut guard = av_core::fsutil::TempPathGuard::new(tmp.clone());
661 {
662 let mut file = fs::File::create(&tmp)?;
663 file.write_all(bytes)?;
664 file.sync_all()?;
665 }
666 fs::rename(&tmp, path)?;
667 guard.disarm();
668 av_core::fsutil::sync_directory(parent)?;
669 Ok(())
670}
671
672fn event_uid_from_value(value: &serde_json::Value) -> Option<&str> {
673 value
674 .get("metadata")
675 .and_then(|metadata| metadata.get("uid"))
676 .and_then(serde_json::Value::as_str)
677}
678
679fn read_high_water(path: &Path) -> Result<u64, BusError> {
680 match av_core::fsutil::read_capped_string(path, av_core::fsutil::MAX_CONTROL_BYTES) {
686 Ok(value) => match value.trim().parse::<u64>() {
687 Ok(offset) => Ok(offset),
688 Err(error) => {
694 tracing::warn!(
695 %error,
696 path = %av_core::fsutil::basename(path),
697 "high-watermark file is corrupt; falling back to segment-derived next_offset",
698 );
699 Ok(0)
700 }
701 },
702 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(0),
703 Err(error) => Err(BusError::Io(error)),
704 }
705}
706
707fn persist_high_water(path: &Path, next_offset: u64) -> Result<(), BusError> {
708 let parent = path
709 .parent()
710 .ok_or_else(|| BusError::Backend("high-watermark has no parent".to_owned()))?;
711 let temporary = path.with_extension(format!("{}.tmp", av_core::new_event_uid()));
712 let mut guard = av_core::fsutil::TempPathGuard::new(temporary.clone());
716 {
717 let mut file = fs::File::create(&temporary)?;
718 file.write_all(next_offset.to_string().as_bytes())?;
719 file.sync_all()?;
720 }
721 fs::rename(&temporary, path)?;
722 guard.disarm();
723 av_core::fsutil::sync_directory(parent)?;
724 Ok(())
725}
726
727fn split_by_time(path: &Path, cutoff_ms: u64) -> Result<(Vec<String>, Vec<String>), BusError> {
729 let mut kept = Vec::new();
730 let mut expired = Vec::new();
731 if !path.exists() {
732 return Ok((kept, expired));
733 }
734 let reader = BufReader::new(fs::File::open(path)?);
735 for line in reader.lines() {
736 let line = line?;
737 if line.is_empty() {
738 continue;
739 }
740 let is_expired = serde_json::from_str::<StoredEvent>(&line)
741 .map(|e| e.stored_at < cutoff_ms)
742 .unwrap_or(false);
743 if is_expired {
744 expired.push(line);
745 } else {
746 kept.push(line);
747 }
748 }
749 Ok((kept, expired))
750}
751
752impl EmbeddedBroker {
753 fn publish_with_uid(
754 &self,
755 topic: &str,
756 key: &str,
757 value: &serde_json::Value,
758 event_uid: Option<&str>,
759 ) -> Result<PublishAck, BusError> {
760 let parts = self
761 .partitions
762 .get(topic)
763 .ok_or_else(|| BusError::UnknownTopic(topic.to_owned()))?;
764 if let Some(validator) = self.validators.get(topic) {
765 let errors: Vec<String> = validator
766 .iter_errors(value)
767 .take(3)
768 .map(|error| error.to_string())
769 .collect();
770 if !errors.is_empty() {
771 return Err(BusError::Backend(format!(
772 "event rejected by schema for topic {topic:?}: {}",
773 errors.join("; ")
774 )));
775 }
776 }
777 if let Some(expected) = event_uid {
778 match event_uid_from_value(value) {
779 Some(actual) if expected != actual => {
780 return Err(BusError::Backend(
781 "idempotency UID does not match event metadata UID".to_owned(),
782 ));
783 }
784 None => {
788 return Err(BusError::Backend(
789 "idempotent publish requires event metadata.uid to match the UID".to_owned(),
790 ));
791 }
792 _ => {}
793 }
794 }
795 let partition = partition_for(key, u32::try_from(parts.len()).unwrap_or(u32::MAX));
796 let slot = parts
797 .get(partition as usize)
798 .ok_or_else(|| BusError::Backend(format!("partition {partition} out of range")))?;
799 let mut part = slot.lock();
800 if let Some(uid) = event_uid {
801 if let Some(offset) = part.seen_event_uids.get(uid).copied() {
802 return Ok(PublishAck {
803 topic: topic.to_owned(),
804 partition,
805 offset,
806 });
807 }
808 }
809 let offset = part.next_offset;
810 let record = StoredEvent {
811 partition,
812 offset,
813 key: key.to_owned(),
814 value: value.clone(),
815 stored_at: av_core::time::now_ms(),
816 };
817 let line = serde_json::to_string(&record)?;
818 part.writer.write_all(line.as_bytes())?;
819 part.writer.write_all(b"\n")?;
820 part.writer.flush()?;
821 part.writer.sync_data()?;
822 part.next_offset = part
823 .next_offset
824 .checked_add(1)
825 .ok_or_else(|| BusError::Backend("embedded offset overflow".to_owned()))?;
826 if let Some(uid) = event_uid {
827 part.seen_event_uids.insert(uid.to_owned(), offset);
828 let mapping = serde_json::to_string(&EventUidOffset {
829 event_uid: uid.to_owned(),
830 offset,
831 })?;
832 let sidecar_created = !part.idempotency_path.exists();
839 let mut idempotency = fs::OpenOptions::new()
840 .create(true)
841 .append(true)
842 .open(&part.idempotency_path)?;
843 idempotency.write_all(mapping.as_bytes())?;
844 idempotency.write_all(b"\n")?;
845 idempotency.sync_data()?;
846 if sidecar_created {
847 if let Some(parent) = part.idempotency_path.parent() {
848 av_core::fsutil::sync_directory(parent)?;
849 }
850 }
851 }
852 Ok(PublishAck {
853 topic: topic.to_owned(),
854 partition,
855 offset,
856 })
857 }
858}
859
860impl EventBus for EmbeddedBroker {
861 fn set_control_key(&self, _key: [u8; 32]) -> Result<(), BusError> {
862 #[cfg(feature = "cold-store")]
863 if let Some(archive) = &self.cold_archive {
864 archive.set_control_key(_key)?;
865 }
866 Ok(())
867 }
868
869 fn publish(&self, topic: &str, key: &str, value: &serde_json::Value) -> Result<PublishAck, BusError> {
870 self.publish_with_uid(topic, key, value, event_uid_from_value(value))
871 }
872
873 fn publish_idempotent(
874 &self,
875 topic: &str,
876 key: &str,
877 value: &serde_json::Value,
878 event_uid: &str,
879 ) -> Result<PublishAck, BusError> {
880 self.publish_with_uid(topic, key, value, Some(event_uid))
881 }
882
883 fn fetch(
884 &self,
885 topic: &str,
886 partition: u32,
887 offset: u64,
888 max: usize,
889 ) -> Result<Vec<StoredEvent>, BusError> {
890 let parts = self
891 .partitions
892 .get(topic)
893 .ok_or_else(|| BusError::UnknownTopic(topic.to_owned()))?;
894 let slot = parts
895 .get(partition as usize)
896 .ok_or_else(|| BusError::Backend(format!("partition {partition} out of range")))?;
897 let part = slot.lock();
900 if max == 0 || !part.path.exists() {
901 return Ok(Vec::new());
902 }
903 let reader = BufReader::new(fs::File::open(&part.path)?);
904 let mut out = Vec::with_capacity(max.min(1024));
905 for line in reader.lines() {
906 let line = line?;
907 if line.is_empty() {
908 continue;
909 }
910 let ev: StoredEvent = serde_json::from_str(&line)?;
911 if ev.offset < offset {
912 continue;
913 }
914 out.push(ev);
915 if out.len() >= max {
916 break;
917 }
918 }
919 Ok(out)
920 }
921
922 fn partitions(&self, topic: &str) -> Result<u32, BusError> {
923 self.partitions
924 .get(topic)
925 .map(|p| u32::try_from(p.len()).unwrap_or(u32::MAX))
926 .ok_or_else(|| BusError::UnknownTopic(topic.to_owned()))
927 }
928
929 fn topics(&self) -> Vec<String> {
930 let mut t: Vec<String> = self.partitions.keys().cloned().collect();
931 t.sort();
932 t
933 }
934
935 fn maintenance(&self, now_ms: u64) -> Result<u64, BusError> {
936 EmbeddedBroker::enforce_retention(self, now_ms)
937 }
938}