1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//! NDJSON line encoding and tolerant, whole-file decoding.
//!
//! Why: [`super::recovery`] (seq high-water mark) and [`super::replay`]
//! (replay-on-reconnect) both need "every event a day file holds, in order",
//! and both must survive the one on-disk defect a crash can actually leave
//! behind: a final line cut off mid-write. Sharing one read path here is what
//! keeps that tolerance rule from being implemented — and drifting — twice.
//! What: [`encode_line`] is the write side: one `HarnessEvent` as JSON plus a
//! trailing `\n`. [`read_events`] is the read side: reads the whole file,
//! parses each non-empty line, and treats a parse failure on the LAST
//! non-empty line as a truncated write (skipped, logged at debug, not an
//! error) — a parse failure on any earlier line is still skipped so one
//! corrupt record cannot make the rest of a day's history unreadable, but it
//! logs at `warn` because it is not the expected shape of a crash.
//! Test: `super::tests::read_events_skips_a_truncated_final_line`,
//! `super::tests::read_events_skips_a_corrupt_interior_line_and_keeps_reading`,
//! `super::tests::read_events_returns_empty_for_an_empty_file`.
use Path;
use HarnessEvent;
use LogError;
/// Serialize `event` as one NDJSON line, trailing newline included.
///
/// Why infallible: `HarnessEvent` derives `Serialize` over plain, always-
/// representable field types (no `f64::NAN`-style JSON landmine in this
/// envelope), so a real serialization failure here would be a bug in the type
/// itself, not a runtime condition callers should have to handle. `expect` is
/// the intentional narrow use per this crate's "no `unwrap()`, reserve
/// `expect()` for invariants that can never occur at runtime" rule.
pub
/// Read every event `path` holds, tolerating a truncated final line.
///
/// What: reads the whole file, splits on `\n`, drops empty/whitespace-only
/// lines, and parses each remaining one as a `HarnessEvent`. A parse failure
/// on the last non-empty line is assumed to be a write the process died
/// mid-way through and is silently skipped (this is the contract callers
/// rely on: "a truncated final line is skipped, not fatal"). A parse failure
/// anywhere else is also skipped, so one corrupt line cannot cost every event
/// after it, but is logged at `warn` since it is not the truncated-tail case.
///
/// # Errors
///
/// [`LogError::Io`] only for the read itself failing (permissions, the file
/// vanishing between listing and reading). A missing file is NOT an error —
/// callers that list files before reading them should not normally hit this,
/// but a `NotFound` here returns an empty `Vec` rather than erroring, so a
/// file rotated away between listing and reading degrades to "nothing found"
/// rather than aborting a whole replay.
///
/// Test: `super::tests::read_events_skips_a_truncated_final_line`,
/// `super::tests::read_events_skips_a_corrupt_interior_line_and_keeps_reading`,
/// `super::tests::read_events_returns_empty_for_an_empty_file`.
pub async