pub struct Log { /* private fields */ }Expand description
Caller-owned heap history with fixed limits and monotonic append watermarks.
Implementations§
Source§impl Log
impl Log
Sourcepub const fn new(limits: Limits) -> Self
pub const fn new(limits: Limits) -> Self
Construct an empty log without preallocating the configured maximum.
Examples found in repository?
77fn history() -> Log {
78 let mut log = Log::new(Limits {
79 max_entries: NonZeroUsize::new(3).unwrap(),
80 max_message_bytes: 32,
81 max_label_bytes: 16,
82 max_text_bytes: 128,
83 });
84 for (time, message) in ["a", "é", "🙂"].into_iter().enumerate() {
85 log.append(EntryInput {
86 created_at_secs: u64::try_from(time).unwrap(),
87 source: "worker",
88 topic: None,
89 level: Level::Info,
90 message,
91 })
92 .unwrap();
93 }
94 log
95}More examples
6fn main() {
7 let mut log = Log::new(Limits {
8 max_entries: NonZeroUsize::new(8).expect("positive capacity"),
9 max_message_bytes: 32,
10 max_label_bytes: 16,
11 max_text_bytes: 512,
12 });
13 for (created_at_secs, level, message) in [
14 (10, Level::Info, "delivery accepted"),
15 (
16 11,
17 Level::Warn,
18 "provider reply is still unknown; reconcile before retry",
19 ),
20 (12, Level::Ok, "original delivery reconciled"),
21 ] {
22 log.append(EntryInput {
23 created_at_secs,
24 level,
25 source: "worker",
26 topic: Some("delivery"),
27 message,
28 })
29 .expect("valid bounded entry");
30 }
31 let scan = log.visit_page(
32 Filter::default(),
33 PageRequest {
34 before: None,
35 max_results: NonZeroUsize::new(8).expect("positive results"),
36 max_scanned: NonZeroUsize::new(8).expect("positive scan"),
37 },
38 |entry| {
39 println!("{} {:?}: {}", entry.sequence, entry.level, entry.message);
40 ControlFlow::Continue(())
41 },
42 );
43 assert_eq!(scan.matched, 3);
44 assert_eq!(log.checkpoint().next_sequence(), Some(3));
45 let batch = log.prune_expired(16, 5, 1);
46 assert_eq!(batch.removed, 1);
47 assert_eq!(log.next_expiry_secs(5), Some(17));
48}Sourcepub const fn limits(&self) -> Limits
pub const fn limits(&self) -> Limits
Return the fixed bounds without copying retained messages.
Sourcepub const fn retained_text_bytes(&self) -> usize
pub const fn retained_text_bytes(&self) -> usize
Combined retained text bytes; excludes metadata, spare capacity and allocator overhead.
Sourcepub const fn checkpoint(&self) -> LogCheckpoint<'_>
pub const fn checkpoint(&self) -> LogCheckpoint<'_>
Borrow rows and append watermarks together, without copying or consuming the log.
Examples found in repository?
6fn main() {
7 let mut log = Log::new(Limits {
8 max_entries: NonZeroUsize::new(8).expect("positive capacity"),
9 max_message_bytes: 32,
10 max_label_bytes: 16,
11 max_text_bytes: 512,
12 });
13 for (created_at_secs, level, message) in [
14 (10, Level::Info, "delivery accepted"),
15 (
16 11,
17 Level::Warn,
18 "provider reply is still unknown; reconcile before retry",
19 ),
20 (12, Level::Ok, "original delivery reconciled"),
21 ] {
22 log.append(EntryInput {
23 created_at_secs,
24 level,
25 source: "worker",
26 topic: Some("delivery"),
27 message,
28 })
29 .expect("valid bounded entry");
30 }
31 let scan = log.visit_page(
32 Filter::default(),
33 PageRequest {
34 before: None,
35 max_results: NonZeroUsize::new(8).expect("positive results"),
36 max_scanned: NonZeroUsize::new(8).expect("positive scan"),
37 },
38 |entry| {
39 println!("{} {:?}: {}", entry.sequence, entry.level, entry.message);
40 ControlFlow::Continue(())
41 },
42 );
43 assert_eq!(scan.matched, 3);
44 assert_eq!(log.checkpoint().next_sequence(), Some(3));
45 let batch = log.prune_expired(16, 5, 1);
46 assert_eq!(batch.removed, 1);
47 assert_eq!(log.next_expiry_secs(5), Some(17));
48}Sourcepub fn snapshot(&self) -> LogSnapshot
pub fn snapshot(&self) -> LogSnapshot
Copy all retained rows and both append watermarks for explicit checkpointing. Work and allocation scale with retained history, not a cursor-page limit.
Sourcepub fn into_snapshot(self) -> LogSnapshot
pub fn into_snapshot(self) -> LogSnapshot
Consume the log into a checkpoint without cloning retained entries. Fixed limits, row order and both append watermarks are preserved, including empty histories and permanent sequence exhaustion.
Sourcepub fn restore(snapshot: LogSnapshot) -> Result<Self, RestoreError>
pub fn restore(snapshot: LogSnapshot) -> Result<Self, RestoreError>
Admit a complete checkpoint without replaying entries or reissuing identities.
Empty retained history preserves its original append watermarks. Admission proves internal consistency, not source authenticity or host continuity. Byte decoders must bound allocation before constructing the checkpoint.
§Errors
Rejects bounds, row ordering, watermarks and truncation states that cannot result from this engine’s append/retention operations.
Sourcepub fn append(
&mut self,
input: EntryInput<'_>,
) -> Result<AppendReceipt, AppendError>
pub fn append( &mut self, input: EntryInput<'_>, ) -> Result<AppendReceipt, AppendError>
Admit one row, evicting the oldest rows until both count and text ceilings fit.
§Errors
Returns a typed refusal for oversized labels, regressed time or exhausted sequence identities, or a row that cannot fit the text budget by itself. Validation completes before any eviction or mutation. Error priority is source, topic, regressed time, sequence exhaustion, then total text budget.
Examples found in repository?
77fn history() -> Log {
78 let mut log = Log::new(Limits {
79 max_entries: NonZeroUsize::new(3).unwrap(),
80 max_message_bytes: 32,
81 max_label_bytes: 16,
82 max_text_bytes: 128,
83 });
84 for (time, message) in ["a", "é", "🙂"].into_iter().enumerate() {
85 log.append(EntryInput {
86 created_at_secs: u64::try_from(time).unwrap(),
87 source: "worker",
88 topic: None,
89 level: Level::Info,
90 message,
91 })
92 .unwrap();
93 }
94 log
95}More examples
6fn main() {
7 let mut log = Log::new(Limits {
8 max_entries: NonZeroUsize::new(8).expect("positive capacity"),
9 max_message_bytes: 32,
10 max_label_bytes: 16,
11 max_text_bytes: 512,
12 });
13 for (created_at_secs, level, message) in [
14 (10, Level::Info, "delivery accepted"),
15 (
16 11,
17 Level::Warn,
18 "provider reply is still unknown; reconcile before retry",
19 ),
20 (12, Level::Ok, "original delivery reconciled"),
21 ] {
22 log.append(EntryInput {
23 created_at_secs,
24 level,
25 source: "worker",
26 topic: Some("delivery"),
27 message,
28 })
29 .expect("valid bounded entry");
30 }
31 let scan = log.visit_page(
32 Filter::default(),
33 PageRequest {
34 before: None,
35 max_results: NonZeroUsize::new(8).expect("positive results"),
36 max_scanned: NonZeroUsize::new(8).expect("positive scan"),
37 },
38 |entry| {
39 println!("{} {:?}: {}", entry.sequence, entry.level, entry.message);
40 ControlFlow::Continue(())
41 },
42 );
43 assert_eq!(scan.matched, 3);
44 assert_eq!(log.checkpoint().next_sequence(), Some(3));
45 let batch = log.prune_expired(16, 5, 1);
46 assert_eq!(batch.removed, 1);
47 assert_eq!(log.next_expiry_secs(5), Some(17));
48}Sourcepub fn prune_expired(
&mut self,
now_secs: u64,
max_age_secs: u64,
max_remove: usize,
) -> RetentionBatch
pub fn prune_expired( &mut self, now_secs: u64, max_age_secs: u64, max_remove: usize, ) -> RetentionBatch
Remove at most max_remove entries strictly older than the age cutoff.
An entry remains available through its full configured age second.
Examples found in repository?
6fn main() {
7 let mut log = Log::new(Limits {
8 max_entries: NonZeroUsize::new(8).expect("positive capacity"),
9 max_message_bytes: 32,
10 max_label_bytes: 16,
11 max_text_bytes: 512,
12 });
13 for (created_at_secs, level, message) in [
14 (10, Level::Info, "delivery accepted"),
15 (
16 11,
17 Level::Warn,
18 "provider reply is still unknown; reconcile before retry",
19 ),
20 (12, Level::Ok, "original delivery reconciled"),
21 ] {
22 log.append(EntryInput {
23 created_at_secs,
24 level,
25 source: "worker",
26 topic: Some("delivery"),
27 message,
28 })
29 .expect("valid bounded entry");
30 }
31 let scan = log.visit_page(
32 Filter::default(),
33 PageRequest {
34 before: None,
35 max_results: NonZeroUsize::new(8).expect("positive results"),
36 max_scanned: NonZeroUsize::new(8).expect("positive scan"),
37 },
38 |entry| {
39 println!("{} {:?}: {}", entry.sequence, entry.level, entry.message);
40 ControlFlow::Continue(())
41 },
42 );
43 assert_eq!(scan.matched, 3);
44 assert_eq!(log.checkpoint().next_sequence(), Some(3));
45 let batch = log.prune_expired(16, 5, 1);
46 assert_eq!(batch.removed, 1);
47 assert_eq!(log.next_expiry_secs(5), Some(17));
48}Sourcepub fn next_expiry_secs(&self, max_age_secs: u64) -> Option<u64>
pub fn next_expiry_secs(&self, max_age_secs: u64) -> Option<u64>
First second at which the oldest row expires; None for empty history or overflow.
Examples found in repository?
6fn main() {
7 let mut log = Log::new(Limits {
8 max_entries: NonZeroUsize::new(8).expect("positive capacity"),
9 max_message_bytes: 32,
10 max_label_bytes: 16,
11 max_text_bytes: 512,
12 });
13 for (created_at_secs, level, message) in [
14 (10, Level::Info, "delivery accepted"),
15 (
16 11,
17 Level::Warn,
18 "provider reply is still unknown; reconcile before retry",
19 ),
20 (12, Level::Ok, "original delivery reconciled"),
21 ] {
22 log.append(EntryInput {
23 created_at_secs,
24 level,
25 source: "worker",
26 topic: Some("delivery"),
27 message,
28 })
29 .expect("valid bounded entry");
30 }
31 let scan = log.visit_page(
32 Filter::default(),
33 PageRequest {
34 before: None,
35 max_results: NonZeroUsize::new(8).expect("positive results"),
36 max_scanned: NonZeroUsize::new(8).expect("positive scan"),
37 },
38 |entry| {
39 println!("{} {:?}: {}", entry.sequence, entry.level, entry.message);
40 ControlFlow::Continue(())
41 },
42 );
43 assert_eq!(scan.matched, 3);
44 assert_eq!(log.checkpoint().next_sequence(), Some(3));
45 let batch = log.prune_expired(16, 5, 1);
46 assert_eq!(batch.removed, 1);
47 assert_eq!(log.next_expiry_secs(5), Some(17));
48}Sourcepub fn page(&self, filter: Filter<'_>, request: PageRequest) -> Page<'_>
pub fn page(&self, filter: Filter<'_>, request: PageRequest) -> Page<'_>
Scan newest-first without cloning retained messages or counting every match.
Follow next_before with the same filters, including after an empty page.
Sourcepub fn visit_page<'a>(
&'a self,
filter: Filter<'_>,
request: PageRequest,
visit: impl FnMut(&'a Entry) -> ControlFlow<()>,
) -> PageScan
pub fn visit_page<'a>( &'a self, filter: Filter<'_>, request: PageRequest, visit: impl FnMut(&'a Entry) -> ControlFlow<()>, ) -> PageScan
Visit matching rows without allocating a page collection or cloning text.
Break stops after consuming that matched row; the returned cursor resumes
at the next older candidate. Empty filtered scans can still have a cursor.
Callback work/allocation belongs to the caller; filter/limits match page.
Examples found in repository?
6fn main() {
7 let mut log = Log::new(Limits {
8 max_entries: NonZeroUsize::new(8).expect("positive capacity"),
9 max_message_bytes: 32,
10 max_label_bytes: 16,
11 max_text_bytes: 512,
12 });
13 for (created_at_secs, level, message) in [
14 (10, Level::Info, "delivery accepted"),
15 (
16 11,
17 Level::Warn,
18 "provider reply is still unknown; reconcile before retry",
19 ),
20 (12, Level::Ok, "original delivery reconciled"),
21 ] {
22 log.append(EntryInput {
23 created_at_secs,
24 level,
25 source: "worker",
26 topic: Some("delivery"),
27 message,
28 })
29 .expect("valid bounded entry");
30 }
31 let scan = log.visit_page(
32 Filter::default(),
33 PageRequest {
34 before: None,
35 max_results: NonZeroUsize::new(8).expect("positive results"),
36 max_scanned: NonZeroUsize::new(8).expect("positive scan"),
37 },
38 |entry| {
39 println!("{} {:?}: {}", entry.sequence, entry.level, entry.message);
40 ControlFlow::Continue(())
41 },
42 );
43 assert_eq!(scan.matched, 3);
44 assert_eq!(log.checkpoint().next_sequence(), Some(3));
45 let batch = log.prune_expired(16, 5, 1);
46 assert_eq!(batch.removed, 1);
47 assert_eq!(log.next_expiry_secs(5), Some(17));
48}More examples
25fn export(
26 log: &Log,
27 filter: Filter<'_>,
28 request: PageRequest,
29 max_bytes: usize,
30) -> Result<ExportPage, ExportError> {
31 let mut body = String::new();
32 let mut emitted = 0;
33 let mut last_emitted = None;
34 let mut stopped_for_budget = Ok(false);
35 let scan = log.visit_page(filter, request, |entry| {
36 let encoded = format!("{}: {}\n", entry.sequence, entry.message);
37 if encoded.len() > max_bytes.saturating_sub(body.len()) {
38 stopped_for_budget = if emitted == 0 {
39 Err(ExportError::EntryTooLarge {
40 sequence: entry.sequence,
41 required_bytes: encoded.len(),
42 max_bytes,
43 })
44 } else {
45 Ok(true)
46 };
47 return ControlFlow::Break(());
48 }
49 body.push_str(&encoded);
50 emitted += 1;
51 last_emitted = Some(entry.sequence);
52 ControlFlow::Continue(())
53 });
54 let stopped_for_budget = stopped_for_budget?;
55 Ok(ExportPage {
56 body,
57 emitted,
58 scanned: scan.scanned,
59 // Break consumes its candidate in the engine. The candidate that did
60 // not fit has not been emitted, so resume from the last delivered row.
61 next_before: if stopped_for_budget {
62 last_emitted
63 } else {
64 scan.next_before
65 },
66 })
67}