1use crate::error::FaucetError;
30use std::sync::atomic::{AtomicU64, Ordering};
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
39#[non_exhaustive]
40pub struct WriteReport {
41 pub accepted: usize,
44 pub coalesced: usize,
47 pub skipped: usize,
50}
51
52impl WriteReport {
53 pub fn accepted(accepted: usize) -> Self {
55 Self {
56 accepted,
57 coalesced: 0,
58 skipped: 0,
59 }
60 }
61
62 pub fn with_coalesced(mut self, coalesced: usize) -> Self {
64 self.coalesced = coalesced;
65 self
66 }
67
68 pub fn with_skipped(mut self, skipped: usize) -> Self {
70 self.skipped = skipped;
71 self
72 }
73
74 pub fn keyed(offered: usize, planned: usize) -> Self {
77 Self::accepted(planned).with_coalesced(offered.saturating_sub(planned))
78 }
79
80 pub fn accounted(&self) -> usize {
82 self.accepted
83 .saturating_add(self.coalesced)
84 .saturating_add(self.skipped)
85 }
86
87 pub fn check(&self, offered: usize) -> Result<(), FaucetError> {
91 if self.accounted() >= offered {
92 Ok(())
93 } else {
94 Err(FaucetError::RowsUnaccounted {
95 offered: offered as u64,
96 accepted: self.accepted as u64,
97 })
98 }
99 }
100}
101
102#[derive(Debug, Default)]
107pub struct RowAccounts {
108 transform_in: AtomicU64,
109 transform_out: AtomicU64,
110 offered: AtomicU64,
111 accepted: AtomicU64,
112 coalesced: AtomicU64,
113 skipped: AtomicU64,
114 failed: AtomicU64,
115 quarantined: AtomicU64,
116 dropped: AtomicU64,
117 unaccounted: AtomicU64,
118 non_finite: AtomicU64,
119}
120
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
123#[non_exhaustive]
124#[serde(default)]
125pub struct RowAccountsSnapshot {
126 pub transform_in: u64,
128 pub transform_out: u64,
130 pub offered: u64,
132 pub accepted: u64,
134 pub coalesced: u64,
136 pub skipped: u64,
138 pub failed: u64,
140 pub quarantined: u64,
142 pub dropped: u64,
144 pub unaccounted: u64,
146 pub non_finite: u64,
149}
150
151impl RowAccountsSnapshot {
152 pub fn filtered(&self) -> u64 {
154 self.transform_in.saturating_sub(self.transform_out)
155 }
156
157 pub fn balanced(&self) -> bool {
159 self.accepted
160 + self.coalesced
161 + self.skipped
162 + self.failed
163 + self.quarantined
164 + self.dropped
165 >= self.offered
166 && self.unaccounted == 0
167 }
168
169 pub fn summary_line(&self) -> String {
171 let mut line = format!("rows: offered {}, accepted {}", self.offered, self.accepted);
172 for (label, n) in [
173 ("coalesced", self.coalesced),
174 ("skipped", self.skipped),
175 ("failed to DLQ", self.failed),
176 ("quarantined", self.quarantined),
177 ("dropped", self.dropped),
178 ("filtered by transforms", self.filtered()),
179 ("unaccounted", self.unaccounted),
180 ("non-finite values written as null", self.non_finite),
181 ] {
182 if n > 0 {
183 line.push_str(&format!(", {label} {n}"));
184 }
185 }
186 line
187 }
188}
189
190impl RowAccounts {
191 pub fn new() -> Self {
193 Self::default()
194 }
195
196 pub fn snapshot(&self) -> RowAccountsSnapshot {
198 let g = |a: &AtomicU64| a.load(Ordering::Relaxed);
199 RowAccountsSnapshot {
200 transform_in: g(&self.transform_in),
201 transform_out: g(&self.transform_out),
202 offered: g(&self.offered),
203 accepted: g(&self.accepted),
204 coalesced: g(&self.coalesced),
205 skipped: g(&self.skipped),
206 failed: g(&self.failed),
207 quarantined: g(&self.quarantined),
208 dropped: g(&self.dropped),
209 unaccounted: g(&self.unaccounted),
210 non_finite: g(&self.non_finite),
211 }
212 }
213
214 pub(crate) fn add_non_finite(&self, n: u64) {
215 self.non_finite.fetch_add(n, Ordering::Relaxed);
216 }
217
218 pub(crate) fn add_transform(&self, input: usize, output: usize) {
219 self.transform_in.fetch_add(input as u64, Ordering::Relaxed);
220 self.transform_out
221 .fetch_add(output as u64, Ordering::Relaxed);
222 }
223}
224
225#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
227pub(crate) struct PageAccount {
228 pub(crate) offered: usize,
229 pub(crate) accepted: usize,
230 pub(crate) coalesced: usize,
231 pub(crate) skipped: usize,
232 pub(crate) failed: usize,
233 pub(crate) quarantined: usize,
234 pub(crate) dropped: usize,
235}
236
237impl PageAccount {
238 pub(crate) fn new(offered: usize) -> Self {
239 Self {
240 offered,
241 ..Self::default()
242 }
243 }
244
245 pub(crate) fn add_report(&mut self, r: WriteReport) {
246 self.accepted += r.accepted;
247 self.coalesced += r.coalesced;
248 self.skipped += r.skipped;
249 }
250
251 fn accounted(&self) -> usize {
252 self.accepted
253 + self.coalesced
254 + self.skipped
255 + self.failed
256 + self.quarantined
257 + self.dropped
258 }
259
260 pub(crate) fn settle(
263 &self,
264 accounts: Option<&RowAccounts>,
265 labels: (&str, &str, &str),
266 ) -> Result<(), FaucetError> {
267 let missing = self.offered.saturating_sub(self.accounted());
268 if let Some(a) = accounts {
269 let add = |c: &AtomicU64, n: usize| {
270 c.fetch_add(n as u64, Ordering::Relaxed);
271 };
272 add(&a.offered, self.offered);
273 add(&a.accepted, self.accepted);
274 add(&a.coalesced, self.coalesced);
275 add(&a.skipped, self.skipped);
276 add(&a.failed, self.failed);
277 add(&a.quarantined, self.quarantined);
278 add(&a.dropped, self.dropped);
279 add(&a.unaccounted, missing);
280 }
281 if missing == 0 {
282 return Ok(());
283 }
284 unaccounted_metric(labels, missing);
285 tracing::error!(
286 pipeline = labels.0,
287 row = labels.1,
288 connector = labels.2,
289 offered = self.offered,
290 accepted = self.accepted,
291 missing,
292 "sink accounted for fewer rows than it was offered; failing the page"
293 );
294 Err(FaucetError::RowsUnaccounted {
295 offered: self.offered as u64,
296 accepted: self.accepted as u64,
297 })
298 }
299}
300
301pub(crate) fn unaccounted_metric(labels: (&str, &str, &str), n: usize) {
303 metrics::counter!(
304 "faucet_rows_unaccounted_total",
305 "pipeline" => labels.0.to_string(),
306 "row" => labels.1.to_string(),
307 "connector" => labels.2.to_string(),
308 )
309 .increment(n as u64);
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn report_accounts_for_coalesced_rows() {
318 assert!(WriteReport::accepted(3).check(3).is_ok());
319 assert!(WriteReport::accepted(4).check(3).is_ok());
320 assert!(WriteReport::keyed(5, 3).check(5).is_ok());
321 assert_eq!(WriteReport::keyed(5, 3).coalesced, 2);
322 assert_eq!(WriteReport::keyed(2, 3).coalesced, 0);
323 let err = WriteReport::accepted(2).check(3).unwrap_err();
324 assert!(matches!(
325 err,
326 FaucetError::RowsUnaccounted {
327 offered: 3,
328 accepted: 2
329 }
330 ));
331 assert_eq!(WriteReport::default().accounted(), 0);
332 assert!(WriteReport::accepted(1).with_skipped(2).check(3).is_ok());
333 }
334
335 #[test]
336 fn page_account_settles_into_the_run_counters() {
337 let run = RowAccounts::new();
338 let mut page = PageAccount::new(10);
339 page.add_report(WriteReport::keyed(6, 5));
340 page.failed = 1;
341 page.quarantined = 2;
342 page.dropped = 1;
343 page.settle(Some(&run), ("p", "r", "c")).unwrap();
344 let s = run.snapshot();
345 assert_eq!(s.offered, 10);
346 assert_eq!(s.accepted, 5);
347 assert_eq!(s.coalesced, 1);
348 assert!(s.balanced());
349 assert_eq!(s.unaccounted, 0);
350
351 let mut short = PageAccount::new(4);
352 short.add_report(WriteReport::accepted(3));
353 let err = short.settle(Some(&run), ("p", "r", "c")).unwrap_err();
354 assert!(matches!(
355 err,
356 FaucetError::RowsUnaccounted {
357 offered: 4,
358 accepted: 3
359 }
360 ));
361 let s = run.snapshot();
362 assert_eq!(s.unaccounted, 1);
363 assert!(!s.balanced());
364 assert!(PageAccount::new(0).settle(None, ("p", "r", "c")).is_ok());
365 }
366
367 #[test]
368 fn summary_line_lists_only_non_zero_buckets() {
369 let run = RowAccounts::new();
370 run.add_transform(12, 10);
371 let mut page = PageAccount::new(10);
372 page.add_report(WriteReport::accepted(9));
373 page.quarantined = 1;
374 page.settle(Some(&run), ("p", "r", "c")).unwrap();
375 let s = run.snapshot();
376 assert_eq!(s.filtered(), 2);
377 assert_eq!(
378 s.summary_line(),
379 "rows: offered 10, accepted 9, quarantined 1, filtered by transforms 2"
380 );
381 let expanded = RowAccountsSnapshot {
382 transform_in: 1,
383 transform_out: 3,
384 ..Default::default()
385 };
386 assert_eq!(expanded.filtered(), 0);
387 }
388}