1use crate::resilience::RetryClass;
23use crate::usage::{CostSignal, UsageMeter, UsageSide};
24use metrics::{Label, SharedString, counter, histogram};
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::sync::{Arc, OnceLock};
27use std::time::{Duration, Instant};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum RoundtripSide {
33 Source,
35 Sink,
37}
38
39impl RoundtripSide {
40 const fn counter_name(self) -> &'static str {
41 match self {
42 Self::Source => "faucet_source_roundtrips_total",
43 Self::Sink => "faucet_sink_roundtrips_total",
44 }
45 }
46
47 const fn histogram_name(self) -> &'static str {
48 match self {
49 Self::Source => "faucet_source_roundtrip_duration_seconds",
50 Self::Sink => "faucet_sink_roundtrip_duration_seconds",
51 }
52 }
53
54 const fn throttled_name(self) -> &'static str {
55 match self {
56 Self::Source => "faucet_source_throttled_total",
57 Self::Sink => "faucet_sink_throttled_total",
58 }
59 }
60
61 const fn throttle_wait_name(self) -> &'static str {
62 match self {
63 Self::Source => "faucet_source_throttle_wait_seconds",
64 Self::Sink => "faucet_sink_throttle_wait_seconds",
65 }
66 }
67
68 const fn retries_name(self) -> &'static str {
69 match self {
70 Self::Source => "faucet_source_retries_total",
71 Self::Sink => "faucet_sink_retries_total",
72 }
73 }
74}
75
76#[derive(Debug, Default)]
79pub struct ThrottleTally {
80 throttled: AtomicU64,
81 wait_nanos: AtomicU64,
82}
83
84impl ThrottleTally {
85 pub fn throttled(&self) -> u64 {
87 self.throttled.load(Ordering::Relaxed)
88 }
89
90 pub fn wait(&self) -> Duration {
92 Duration::from_nanos(self.wait_nanos.load(Ordering::Relaxed))
93 }
94}
95
96#[derive(Debug, Clone)]
104pub struct RoundtripRecorder {
105 side: RoundtripSide,
106 base: Vec<Label>,
108 meter: Option<Arc<UsageMeter>>,
111 connector: SharedString,
112 throttle: Arc<ThrottleTally>,
113}
114
115impl RoundtripSide {
116 fn usage_side(self) -> UsageSide {
117 match self {
118 Self::Source => UsageSide::Source,
119 Self::Sink => UsageSide::Sink,
120 }
121 }
122}
123
124impl RoundtripRecorder {
125 pub fn new(
127 side: RoundtripSide,
128 pipeline: impl Into<SharedString>,
129 row: impl Into<SharedString>,
130 connector: impl Into<SharedString>,
131 ) -> Self {
132 let connector: SharedString = connector.into();
133 Self {
134 side,
135 base: vec![
136 Label::new("pipeline", pipeline.into()),
137 Label::new("row", row.into()),
138 Label::new("connector", connector.clone()),
139 ],
140 meter: None,
141 connector,
142 throttle: Arc::new(ThrottleTally::default()),
143 }
144 }
145
146 pub fn throttle_tally(&self) -> Arc<ThrottleTally> {
148 Arc::clone(&self.throttle)
149 }
150
151 pub fn throttled(&self) {
155 counter!(self.side.throttled_name(), self.base.clone()).increment(1);
156 self.throttle.throttled.fetch_add(1, Ordering::Relaxed);
157 if let Some(m) = self.metered_source() {
158 m.add_throttled();
159 }
160 }
161
162 pub fn throttle_wait(&self, slept: Duration) {
166 histogram!(self.side.throttle_wait_name(), self.base.clone()).record(slept.as_secs_f64());
167 self.throttle.wait_nanos.fetch_add(
168 u64::try_from(slept.as_nanos()).unwrap_or(u64::MAX),
169 Ordering::Relaxed,
170 );
171 if let Some(m) = self.metered_source() {
172 m.add_throttle_wait(slept);
173 }
174 }
175
176 pub fn retry(&self, class: RetryClass) {
179 let mut labels = self.base.clone();
180 labels.push(Label::new("class", SharedString::const_str(class.as_str())));
181 counter!(self.side.retries_name(), labels).increment(1);
182 if let Some(m) = self.metered_source() {
183 m.add_source_retry(class.as_str());
184 }
185 }
186
187 fn metered_source(&self) -> Option<&Arc<UsageMeter>> {
188 match self.side {
189 RoundtripSide::Source => self.meter.as_ref(),
190 RoundtripSide::Sink => None,
191 }
192 }
193
194 pub fn with_meter(mut self, meter: Arc<UsageMeter>) -> Self {
196 self.meter = Some(meter);
197 self
198 }
199
200 pub fn signal(&self, kind: &'static str, unit: &'static str, quantity: f64) {
208 let mut labels = self.base.clone();
209 labels.push(Label::new("kind", SharedString::const_str(kind)));
210 labels.push(Label::new("unit", SharedString::const_str(unit)));
211 counter!("faucet_cost_signals_total", labels).increment(quantity.max(0.0).round() as u64);
212 if let Some(m) = &self.meter {
213 m.add_signal(CostSignal {
214 kind: kind.to_string(),
215 unit: unit.to_string(),
216 quantity,
217 side: self.side.usage_side(),
218 connector: self.connector.to_string(),
219 });
220 }
221 }
222
223 pub fn record(&self, op: &'static str) {
229 counter!(self.side.counter_name(), self.labels_for(op)).increment(1);
230 if let Some(m) = &self.meter {
231 m.add_roundtrip(self.side.usage_side(), op);
232 }
233 }
234
235 pub fn replication_key_missing(&self, n: u64) {
238 if n > 0 {
239 counter!(
240 "faucet_source_replication_key_missing_total",
241 self.base.clone()
242 )
243 .increment(n);
244 }
245 }
246
247 pub fn record_timed(&self, op: &'static str, elapsed: Duration) {
249 let labels = self.labels_for(op);
250 counter!(self.side.counter_name(), labels.clone()).increment(1);
251 histogram!(self.side.histogram_name(), labels).record(elapsed.as_secs_f64());
252 if let Some(m) = &self.meter {
253 m.add_roundtrip(self.side.usage_side(), op);
254 }
255 }
256
257 fn labels_for(&self, op: &'static str) -> Vec<Label> {
258 let mut labels = self.base.clone();
259 labels.push(Label::new("op", SharedString::const_str(op)));
260 labels
261 }
262
263 #[doc(hidden)]
267 pub fn labels_for_test(&self, op: &'static str) -> Vec<(String, String)> {
268 self.labels_for(op)
269 .into_iter()
270 .map(|l| (l.key().to_string(), l.value().to_string()))
271 .collect()
272 }
273}
274
275#[derive(Debug)]
280#[must_use = "the wait is recorded when the guard drops"]
281pub struct ThrottleWait {
282 recorder: Option<Arc<RoundtripRecorder>>,
283 start: Instant,
284}
285
286impl ThrottleWait {
287 pub fn start(recorder: Option<Arc<RoundtripRecorder>>) -> Self {
289 Self {
290 recorder,
291 start: Instant::now(),
292 }
293 }
294}
295
296impl Drop for ThrottleWait {
297 fn drop(&mut self) {
298 if let Some(r) = &self.recorder {
299 r.throttle_wait(self.start.elapsed());
300 }
301 }
302}
303
304#[allow(clippy::disallowed_methods)] pub async fn throttle_sleep(
309 recorder: Option<Arc<RoundtripRecorder>>,
310 wait: Duration,
311 cancel: Option<&tokio_util::sync::CancellationToken>,
312) -> bool {
313 let _timer = ThrottleWait::start(recorder);
314 match cancel {
315 Some(token) => {
316 tokio::select! {
317 biased;
318 _ = token.cancelled() => false,
319 _ = tokio::time::sleep(wait) => true,
320 }
321 }
322 None => {
323 tokio::time::sleep(wait).await;
324 true
325 }
326 }
327}
328
329pub fn throttle_warning(throttled: u64, wait: Duration, run: Duration) -> Option<String> {
332 if wait.is_zero() || run.is_zero() || wait.as_secs_f64() * 10.0 <= run.as_secs_f64() {
333 return None;
334 }
335 let pct = (wait.as_secs_f64() / run.as_secs_f64() * 100.0).min(100.0);
336 Some(format!(
337 "source spent {:.1}s of a {:.1}s run ({pct:.0}%) waiting on rate limits \
338 ({throttled} throttled responses); lower concurrency, stagger schedules or raise the quota",
339 wait.as_secs_f64(),
340 run.as_secs_f64(),
341 ))
342}
343
344pub fn describe_roundtrip_metrics() {
347 metrics::describe_counter!(
348 "faucet_cost_signals_total",
349 "Backend-reported usage a connector measured during a run (BigQuery bytes billed, streamed payload bytes, …), by kind and unit"
350 );
351 metrics::describe_counter!(
352 "faucet_source_roundtrips_total",
353 "Calls a source made to its upstream backend, by connector-defined op"
354 );
355 metrics::describe_counter!(
356 "faucet_sink_roundtrips_total",
357 "Calls a sink made to its upstream backend, by connector-defined op"
358 );
359 metrics::describe_histogram!(
360 "faucet_source_roundtrip_duration_seconds",
361 metrics::Unit::Seconds,
362 "Duration of one source round trip to its upstream backend"
363 );
364 metrics::describe_histogram!(
365 "faucet_sink_roundtrip_duration_seconds",
366 metrics::Unit::Seconds,
367 "Duration of one sink round trip to its upstream backend"
368 );
369 metrics::describe_counter!(
370 "faucet_source_throttled_total",
371 "Rate-limit responses (HTTP 429 and equivalents) a source received"
372 );
373 metrics::describe_histogram!(
374 "faucet_source_throttle_wait_seconds",
375 metrics::Unit::Seconds,
376 "Time a source actually slept because of one rate-limit response"
377 );
378 metrics::describe_counter!(
379 "faucet_source_replication_key_missing_total",
380 "Records an incremental source received without its replication key (kept, dropped or failed per on_missing_key)"
381 );
382 metrics::describe_counter!(
383 "faucet_source_retries_total",
384 "Retries a source made against its upstream backend, by retry class"
385 );
386}
387
388#[cfg(test)]
389#[allow(clippy::disallowed_methods)] mod tests {
391 use super::*;
392
393 #[test]
394 fn metered_recorders_feed_the_usage_meter_through_a_slot() {
395 let meter = Arc::new(crate::usage::UsageMeter::new());
396 for side in [RoundtripSide::Source, RoundtripSide::Sink] {
397 let slot = RecorderSlot::new();
398 slot.record("get");
399 slot.record_timed("get", Duration::from_millis(1));
400 slot.signal("bytes_billed", "bytes", 10.0);
401 assert!(slot.recorder().is_none());
402 slot.install(Arc::new(
403 RoundtripRecorder::new(side, "p", "r", "c").with_meter(meter.clone()),
404 ));
405 slot.record("get");
406 slot.record_timed("put", Duration::from_millis(2));
407 slot.signal("bytes_billed", "bytes", 1024.4);
408 assert!(slot.recorder().is_some());
409 }
410 let snap = meter.snapshot();
411 assert_eq!(snap.source_roundtrips["get"], 1);
412 assert_eq!(snap.source_roundtrips["put"], 1);
413 assert_eq!(snap.sink_roundtrips["get"], 1);
414 assert_eq!(snap.signals.len(), 2);
415 assert_eq!(snap.signals[0].kind, "bytes_billed");
416 assert_eq!(snap.signals[0].connector, "c");
417 assert_eq!(snap.signals[1].side, crate::usage::UsageSide::Sink);
418 }
419
420 #[test]
421 fn side_selects_the_metric_names() {
422 assert_eq!(
423 RoundtripSide::Source.counter_name(),
424 "faucet_source_roundtrips_total"
425 );
426 assert_eq!(
427 RoundtripSide::Sink.counter_name(),
428 "faucet_sink_roundtrips_total"
429 );
430 assert_eq!(
431 RoundtripSide::Source.histogram_name(),
432 "faucet_source_roundtrip_duration_seconds"
433 );
434 assert_eq!(
435 RoundtripSide::Sink.histogram_name(),
436 "faucet_sink_roundtrip_duration_seconds"
437 );
438 }
439
440 #[test]
441 fn labels_carry_the_universal_trio_plus_op() {
442 let r = RoundtripRecorder::new(RoundtripSide::Source, "p", "rowA", "rest");
443 let labels = r.labels_for_test("poll");
444 assert_eq!(
445 labels,
446 vec![
447 ("pipeline".to_string(), "p".to_string()),
448 ("row".to_string(), "rowA".to_string()),
449 ("connector".to_string(), "rest".to_string()),
450 ("op".to_string(), "poll".to_string()),
451 ],
452 "the trio must match every other metric, or this one can't be joined to them"
453 );
454 }
455
456 #[test]
457 fn op_is_the_only_thing_that_varies_between_calls() {
458 let r = RoundtripRecorder::new(RoundtripSide::Sink, "p", "", "s3");
461 let a = r.labels_for_test("put");
462 let b = r.labels_for_test("list");
463 assert_eq!(a[..3], b[..3]);
464 assert_eq!(a[3].1, "put");
465 assert_eq!(b[3].1, "list");
466 }
467
468 #[test]
469 fn record_emits_the_counter_under_an_installed_recorder() {
470 use crate::observability::decorator::source_tests::{LOCK, snapshotter};
471 use metrics_util::debugging::DebugValue;
472
473 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
474 let snap = snapshotter();
475 let r = RoundtripRecorder::new(RoundtripSide::Source, "pipe", "rowA", "rest");
476 r.record("submit");
477 r.record("poll");
478 r.record("poll");
479
480 let counts: Vec<(String, u64)> = snap
481 .snapshot()
482 .into_vec()
483 .into_iter()
484 .filter(|(k, _, _, _)| k.key().name() == "faucet_source_roundtrips_total")
485 .filter_map(|(k, _, _, v)| {
486 let op = k
487 .key()
488 .labels()
489 .find(|l| l.key() == "op")?
490 .value()
491 .to_string();
492 match v {
493 DebugValue::Counter(c) => Some((op, c)),
494 _ => None,
495 }
496 })
497 .collect();
498
499 let poll = counts.iter().find(|(op, _)| op == "poll").map(|(_, c)| *c);
500 let submit = counts
501 .iter()
502 .find(|(op, _)| op == "submit")
503 .map(|(_, c)| *c);
504 assert_eq!(submit, Some(1), "one submit: {counts:?}");
505 assert_eq!(
506 poll,
507 Some(2),
508 "each poll counts — the poll loop's overhead is the whole signal: {counts:?}"
509 );
510 }
511
512 #[test]
513 fn replication_key_missing_counts_through_the_slot() {
514 use crate::observability::decorator::source_tests::{LOCK, snapshotter};
515 use metrics_util::debugging::DebugValue;
516
517 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
518 let snap = snapshotter();
519 let slot = RecorderSlot::new();
520 slot.replication_key_missing(4);
521 slot.install(Arc::new(RoundtripRecorder::new(
522 RoundtripSide::Source,
523 "pipe",
524 "rowK",
525 "rest",
526 )));
527 slot.replication_key_missing(0);
528 slot.replication_key_missing(3);
529 let total: u64 = snap
530 .snapshot()
531 .into_vec()
532 .into_iter()
533 .filter(|(k, _, _, _)| {
534 k.key().name() == "faucet_source_replication_key_missing_total"
535 && k.key().labels().any(|l| l.value() == "rowK")
536 })
537 .map(|(_, _, _, v)| match v {
538 DebugValue::Counter(c) => c,
539 _ => 0,
540 })
541 .sum();
542 assert_eq!(total, 3);
543 }
544
545 #[test]
546 fn recording_without_an_installed_recorder_is_a_no_op() {
547 let r = RoundtripRecorder::new(RoundtripSide::Source, "p", "r", "postgres");
551 r.record("query");
552 r.record_timed("query", Duration::from_millis(5));
553 }
554
555 #[test]
556 fn throttling_feeds_the_tally_the_meter_and_the_metrics() {
557 use crate::observability::decorator::source_tests::{LOCK, snapshotter};
558 use metrics_util::debugging::DebugValue;
559
560 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
561 let snap = snapshotter();
562 let meter = Arc::new(UsageMeter::new());
563 let r = RoundtripRecorder::new(RoundtripSide::Source, "pipe", "rowT", "rest")
564 .with_meter(meter.clone());
565 let tally = r.throttle_tally();
566 r.throttled();
567 r.throttled();
568 r.throttle_wait(Duration::from_millis(300));
569 r.retry(RetryClass::RateLimited);
570 r.retry(RetryClass::Http5xx);
571 assert_eq!(tally.throttled(), 2);
572 assert_eq!(tally.wait(), Duration::from_millis(300));
573 let usage = meter.snapshot();
574 assert_eq!(usage.throttled, 2);
575 assert!((usage.throttle_wait_secs - 0.3).abs() < 1e-9);
576 assert_eq!(usage.source_retries["rate_limited"], 1);
577 assert_eq!(usage.source_retries["http_5xx"], 1);
578
579 let entries: Vec<_> = snap
580 .snapshot()
581 .into_vec()
582 .into_iter()
583 .filter(|(k, _, _, _)| k.key().labels().any(|l| l.value() == "rowT"))
584 .collect();
585 let counter = |name: &str, class: Option<&str>| {
586 entries.iter().find_map(|(k, _, _, v)| {
587 let class_ok = class.is_none_or(|c| {
588 k.key()
589 .labels()
590 .any(|l| l.key() == "class" && l.value() == c)
591 });
592 match v {
593 DebugValue::Counter(n) if k.key().name() == name && class_ok => Some(*n),
594 _ => None,
595 }
596 })
597 };
598 assert_eq!(counter("faucet_source_throttled_total", None), Some(2));
599 assert_eq!(
600 counter("faucet_source_retries_total", Some("rate_limited")),
601 Some(1)
602 );
603 assert_eq!(
604 counter("faucet_source_retries_total", Some("http_5xx")),
605 Some(1)
606 );
607 assert!(entries.iter().any(|(k, _, _, v)| {
608 k.key().name() == "faucet_source_throttle_wait_seconds"
609 && matches!(v, DebugValue::Histogram(h) if h.len() == 1)
610 }));
611 }
612
613 #[test]
614 fn sink_side_throttling_emits_metrics_but_stays_off_the_usage_record() {
615 let meter = Arc::new(UsageMeter::new());
616 let r =
617 RoundtripRecorder::new(RoundtripSide::Sink, "p", "r", "http").with_meter(meter.clone());
618 r.throttled();
619 r.throttle_wait(Duration::from_millis(5));
620 r.retry(RetryClass::Timeout);
621 assert_eq!(r.throttle_tally().throttled(), 1);
622 let usage = meter.snapshot();
623 assert_eq!(usage.throttled, 0);
624 assert!(usage.source_retries.is_empty());
625 assert_eq!(
626 RoundtripSide::Sink.throttled_name(),
627 "faucet_sink_throttled_total"
628 );
629 assert_eq!(
630 RoundtripSide::Sink.throttle_wait_name(),
631 "faucet_sink_throttle_wait_seconds"
632 );
633 assert_eq!(
634 RoundtripSide::Sink.retries_name(),
635 "faucet_sink_retries_total"
636 );
637 }
638
639 #[tokio::test]
640 async fn throttle_sleep_records_the_time_actually_slept() {
641 let r = Arc::new(RoundtripRecorder::new(
642 RoundtripSide::Source,
643 "p",
644 "r",
645 "rest",
646 ));
647 let tally = r.throttle_tally();
648 assert!(throttle_sleep(Some(r.clone()), Duration::from_millis(40), None).await);
649 let full = tally.wait();
650 assert!(full >= Duration::from_millis(40), "{full:?}");
651
652 let token = tokio_util::sync::CancellationToken::new();
653 let t = token.clone();
654 tokio::spawn(async move {
655 tokio::time::sleep(Duration::from_millis(30)).await;
656 t.cancel();
657 });
658 let completed =
659 throttle_sleep(Some(r.clone()), Duration::from_secs(30), Some(&token)).await;
660 assert!(!completed, "cancellation wins");
661 let partial = tally.wait() - full;
662 assert!(
663 partial >= Duration::from_millis(25) && partial < Duration::from_secs(5),
664 "the partial wait is recorded, not the requested 30 s: {partial:?}"
665 );
666
667 let token = tokio_util::sync::CancellationToken::new();
668 assert!(throttle_sleep(None, Duration::from_millis(1), Some(&token)).await);
669 }
670
671 #[tokio::test]
672 async fn a_dropped_sleep_still_records_its_partial_wait() {
673 let slot = RecorderSlot::new();
674 slot.throttled();
675 slot.retry(RetryClass::Connection);
676 drop(slot.throttle_wait_timer());
677 let r = Arc::new(RoundtripRecorder::new(
678 RoundtripSide::Source,
679 "p",
680 "r",
681 "rest",
682 ));
683 slot.install(r.clone());
684 slot.throttled();
685 slot.retry(RetryClass::Connection);
686 let tally = r.throttle_tally();
687 let sleeping = async {
688 let _t = slot.throttle_wait_timer();
689 tokio::time::sleep(Duration::from_secs(30)).await;
690 };
691 let _ = tokio::time::timeout(Duration::from_millis(30), sleeping).await;
692 assert_eq!(tally.throttled(), 1);
693 assert!(
694 tally.wait() >= Duration::from_millis(25),
695 "{:?}",
696 tally.wait()
697 );
698 assert!(tally.wait() < Duration::from_secs(5));
699 }
700
701 #[test]
702 fn warns_only_when_waiting_exceeds_a_tenth_of_the_run() {
703 assert_eq!(
704 throttle_warning(0, Duration::ZERO, Duration::from_secs(10)),
705 None
706 );
707 assert_eq!(
708 throttle_warning(3, Duration::from_secs(1), Duration::from_secs(10)),
709 None,
710 "exactly 10% does not warn"
711 );
712 assert_eq!(
713 throttle_warning(3, Duration::from_secs(1), Duration::ZERO),
714 None
715 );
716 let msg = throttle_warning(312, Duration::from_secs(2460), Duration::from_secs(3600))
717 .expect("68% warns");
718 assert!(msg.contains("2460.0s of a 3600.0s run (68%)"), "{msg}");
719 assert!(msg.contains("312 throttled responses"), "{msg}");
720 let capped = throttle_warning(1, Duration::from_secs(20), Duration::from_secs(10)).unwrap();
721 assert!(capped.contains("(100%)"), "{capped}");
722 }
723
724 #[test]
725 fn clone_shares_the_prebuilt_labels() {
726 let r = RoundtripRecorder::new(RoundtripSide::Source, "p", "r", "kafka");
727 let c = r.clone();
728 assert_eq!(r.labels_for_test("poll"), c.labels_for_test("poll"));
729 }
730}
731
732#[derive(Debug, Default)]
742pub struct RecorderSlot(OnceLock<Arc<RoundtripRecorder>>);
743
744impl RecorderSlot {
745 pub const fn new() -> Self {
746 Self(OnceLock::new())
747 }
748
749 pub fn install(&self, recorder: Arc<RoundtripRecorder>) {
751 let _ = self.0.set(recorder);
752 }
753
754 pub fn recorder(&self) -> Option<Arc<RoundtripRecorder>> {
757 self.0.get().cloned()
758 }
759
760 pub fn record(&self, op: &'static str) {
762 if let Some(r) = self.0.get() {
763 r.record(op);
764 }
765 }
766
767 pub fn record_timed(&self, op: &'static str, elapsed: Duration) {
769 if let Some(r) = self.0.get() {
770 r.record_timed(op, elapsed);
771 }
772 }
773
774 pub fn replication_key_missing(&self, n: u64) {
776 if let Some(r) = self.0.get() {
777 r.replication_key_missing(n);
778 }
779 }
780
781 pub fn throttled(&self) {
783 if let Some(r) = self.0.get() {
784 r.throttled();
785 }
786 }
787
788 pub fn retry(&self, class: RetryClass) {
790 if let Some(r) = self.0.get() {
791 r.retry(class);
792 }
793 }
794
795 pub fn throttle_wait_timer(&self) -> ThrottleWait {
797 ThrottleWait::start(self.recorder())
798 }
799
800 pub fn signal(&self, kind: &'static str, unit: &'static str, quantity: f64) {
802 if let Some(r) = self.0.get() {
803 r.signal(kind, unit, quantity);
804 }
805 }
806}