1use {
15 super::{
16 contract::{Isolation, RecoveryCheck, Semantics},
17 ledger::SeqSet,
18 lifecycle::{Checkpoint, LevelRange},
19 recovery::RecoveryEvidence,
20 },
21 krabka_units::{prelude::*, serde_units},
22 serde::{Deserialize, Serialize},
23 std::{collections::BTreeMap, str},
24};
25
26#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
28pub struct Evidence {
29 pub contract: String,
31 pub contract_digest: String,
34 pub seed: u64,
35 pub broker_image: String,
37 pub started_unix_ms: i64,
38 pub ended_unix_ms: i64,
39 #[serde(default)]
43 pub environment: BTreeMap<String, String>,
44 pub producers: Vec<ProducerLedger>,
45 pub readers: Vec<ReaderLedger>,
46 pub commits: Vec<CommitCheck>,
47 pub faults: Vec<FaultRecord>,
48 pub recoveries: Vec<RecoveryRecord>,
49 pub paths: Vec<PathMeasurement>,
50 #[serde(default)]
52 pub injected: Vec<InjectedLoss>,
53 #[serde(default)]
57 pub releases: BTreeMap<String, String>,
58 #[serde(default)]
61 pub source_supports: BTreeMap<String, LevelRange>,
62 #[serde(default)]
64 pub checkpoints: Vec<Checkpoint>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub recovery: Option<RecoveryEvidence>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct ProducerLedger {
74 pub path: String,
75 pub producer: String,
77 pub semantics: Semantics,
78 pub attempted: u64,
80 pub acked: SeqSet,
82 pub failed: SeqSet,
85 pub retried: SeqSet,
87 #[serde(default)]
89 pub transactions: Vec<TxnLedger>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct TxnLedger {
95 pub id: u64,
96 pub seqs: SeqSet,
97 pub outcome: TxnOutcome,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum TxnOutcome {
104 Committed,
106 Aborted,
109 Unknown,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct ReaderLedger {
118 pub path: String,
119 pub isolation: Isolation,
120 pub producer: String,
121 pub seen: SeqSet,
122 #[serde(default)]
124 pub duplicates: Vec<DuplicateRead>,
125 #[serde(default)]
127 pub latest_by_key: BTreeMap<u64, u64>,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132pub struct DuplicateRead {
133 pub seq: u64,
134 pub positions: Vec<(i32, i64)>,
136}
137
138pub const FINAL_COMMIT_CHECK: &str = "final";
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct CommitCheck {
144 pub group: String,
145 pub topic: String,
146 pub partition: i32,
147 pub fault: String,
149 pub acknowledged: i64,
151 pub fetched: Option<i64>,
154 pub log_end: i64,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub struct FaultRecord {
163 pub id: String,
165 pub kind: String,
166 pub target: String,
168 pub role: String,
171 #[serde(default)]
173 pub node: Option<i32>,
174 pub injected_unix_ms: i64,
175 pub cleared_unix_ms: Option<i64>,
177 #[serde(default)]
179 pub cleanup_error: Option<String>,
180 #[serde(default)]
182 pub detail: String,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct RecoveryRecord {
188 pub fault: String,
189 pub check: RecoveryCheck,
190 pub from_unix_ms: i64,
193 pub recovered_unix_ms: Option<i64>,
195}
196
197impl RecoveryRecord {
198 #[must_use]
204 pub fn took(&self) -> Option<Time> {
205 let recovered = self.recovered_unix_ms?;
206 (recovered >= self.from_unix_ms)
207 .then(|| Time::from_millis(recovered.saturating_sub(self.from_unix_ms)))
208 }
209}
210
211#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
213pub struct LatencySummary {
214 #[serde(with = "serde_units::numeric::nanos_i64")]
215 pub p50: Time,
216 #[serde(with = "serde_units::numeric::nanos_i64")]
217 pub p99: Time,
218 #[serde(with = "serde_units::numeric::nanos_i64")]
219 pub p999: Time,
220 pub count: u64,
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229pub struct PathMeasurement {
230 pub path: String,
231 pub produce_steady: LatencySummary,
232 pub produce_disturbed: LatencySummary,
233 pub consume_steady: LatencySummary,
234 pub consume_disturbed: LatencySummary,
235 #[serde(with = "serde_units::human::frequency")]
237 pub steady_rate: Frequency,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct InjectedLoss {
243 pub path: String,
244 pub producer: String,
245 pub seq: u64,
246 pub partition: i32,
247 pub offset: i64,
248}
249
250#[must_use]
253pub fn record_tag(producer: &str, seq: u64) -> String { format!("{producer}/{seq};") }
254
255#[must_use]
258pub fn parse_record_tag(value: &[u8]) -> Option<(&str, u64)> {
259 let (tag, _) = value.split_at_checked(value.iter().position(|byte| *byte == b';')?)?;
260 let tag = str::from_utf8(tag).ok()?;
261 let (producer, seq) = tag.rsplit_once('/')?;
262 Some((producer, seq.parse().ok()?))
263}
264
265#[cfg(test)]
266mod tests {
267 use {
268 super::{RecoveryRecord, parse_record_tag, record_tag},
269 crate::soak::contract::RecoveryCheck,
270 assert2::check,
271 krabka_units::prelude::*,
272 };
273
274 #[yare::parameterized(
275 plain = { b"p-0/17;xxxx", Some(("p-0", 17)) },
276 slash_in_name = { b"path/p-1/3;", Some(("path/p-1", 3)) },
277 no_terminator = { b"p-0/17xxxx", None },
278 no_number = { b"p-0/;", None },
279 foreign = { b"hello world", None },
280 )]
281 fn a_record_value_names_its_send(value: &[u8], expected: Option<(&str, u64)>) {
282 check!(parse_record_tag(value) == expected);
283 }
284
285 #[test]
286 fn a_tag_round_trips() {
287 let mut value = record_tag("classic-idempotent-0", 42).into_bytes();
288 value.extend_from_slice(b"padding");
289 check!(parse_record_tag(&value) == Some(("classic-idempotent-0", 42)));
290 }
291
292 #[yare::parameterized(
293 observed = { Some(2_500), Some(millis(1_500)) },
294 never = { None, None },
295 clock_step_back = { Some(900), None },
296 same_instant = { Some(1_000), Some(Time::ZERO) },
297 )]
298 fn a_recovery_reports_how_long_it_took(recovered: Option<i64>, took: Option<Time>) {
299 let record = RecoveryRecord {
300 fault: "f#0".into(),
301 check: RecoveryCheck::IsrRejoin,
302 from_unix_ms: 1_000,
303 recovered_unix_ms: recovered,
304 };
305 check!(record.took() == took);
306 }
307}