use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{mpsc, oneshot};
use tokio::time::{Instant, sleep_until};
use tsoracle_core::Timestamp;
use crate::MAX_TIMESTAMPS_PER_RPC;
use crate::error::ClientError;
use crate::response::TimestampRange;
const QUEUE_CAPACITY: usize = 4096;
pub(crate) struct Waiter {
pub count: u32,
pub respond: oneshot::Sender<Result<Vec<Timestamp>, ClientError>>,
}
pub(crate) struct Driver {
tx: mpsc::Sender<Waiter>,
}
type RpcFn = Arc<
dyn Fn(u32) -> futures::future::BoxFuture<'static, Result<TimestampRange, ClientError>>
+ Send
+ Sync,
>;
type BatchHandle = tokio::task::JoinHandle<()>;
impl Driver {
pub fn spawn<F>(rpc: F, flush_interval: Duration) -> Self
where
F: Fn(u32) -> futures::future::BoxFuture<'static, Result<TimestampRange, ClientError>>
+ Send
+ Sync
+ 'static,
{
let (tx, rx) = mpsc::channel(QUEUE_CAPACITY / 2);
let handle = tokio::spawn(driver_task(Arc::new(rpc), rx, flush_interval));
tokio::spawn(crate::driver_supervisor::observe_driver_handle(handle));
Driver { tx }
}
pub async fn request(&self, count: u32) -> Result<Vec<Timestamp>, ClientError> {
let (resp_tx, resp_rx) = oneshot::channel();
self.tx
.send(Waiter {
count,
respond: resp_tx,
})
.await
.map_err(|_| ClientError::DriverGone)?;
resp_rx.await.map_err(|_| ClientError::DriverGone)?
}
}
async fn driver_task(rpc: RpcFn, mut rx: mpsc::Receiver<Waiter>, flush_interval: Duration) {
let mut queue: VecDeque<Waiter> = VecDeque::new();
let mut first_arrival: Option<Instant> = None;
let mut in_flight: Option<BatchHandle> = None;
loop {
if let Some(handle) = in_flight.as_mut() {
tokio::select! {
biased;
completed = handle => {
in_flight = None;
set_in_flight_gauge(0);
if let Err(_join_err) = completed {
#[cfg(feature = "tracing")]
tracing::debug!(
error = %_join_err,
"tsoracle client batch task failed; in-flight waiters were notified via DriverGone"
);
}
}
next = rx.recv(), if queue.len() < QUEUE_CAPACITY / 2 => {
match next {
Some(w) => enqueue(&mut queue, &mut first_arrival, w),
None => return,
}
}
}
} else {
if queue.is_empty() {
match rx.recv().await {
Some(w) => enqueue(&mut queue, &mut first_arrival, w),
None => return,
}
}
if flush_interval > Duration::ZERO
&& let Some(first) = first_arrival
{
let deadline = first + flush_interval;
loop {
tokio::select! {
biased;
_ = sleep_until(deadline) => break,
next = rx.recv(), if queue.len() < QUEUE_CAPACITY / 2 => {
match next {
Some(w) => enqueue(&mut queue, &mut first_arrival, w),
None => return,
}
}
}
}
}
first_arrival = None;
let chunks = chunk_queue(&mut queue);
set_queue_depth_gauge(queue.len());
if chunks.is_empty() {
continue;
}
let rpc_fn = rpc.clone();
in_flight = Some(tokio::spawn(
async move { run_chunks(rpc_fn, chunks).await },
));
set_in_flight_gauge(1);
}
}
}
fn chunk_queue(queue: &mut VecDeque<Waiter>) -> Vec<(u32, VecDeque<Waiter>)> {
let mut chunks: Vec<(u32, VecDeque<Waiter>)> = Vec::new();
let mut current: VecDeque<Waiter> = VecDeque::new();
let mut current_total: u32 = 0;
while let Some(w) = queue.pop_front() {
if w.count == 0 || w.count > MAX_TIMESTAMPS_PER_RPC {
let count = w.count;
reply_to_waiter(w, Err(ClientError::InvalidCount(count)));
continue;
}
let fits = current_total
.checked_add(w.count)
.is_some_and(|sum| sum <= MAX_TIMESTAMPS_PER_RPC);
if !fits {
if !current.is_empty() {
chunks.push((current_total, std::mem::take(&mut current)));
}
current_total = 0;
}
current_total += w.count;
current.push_back(w);
}
if !current.is_empty() {
chunks.push((current_total, current));
}
chunks
}
async fn run_chunks(rpc_fn: RpcFn, chunks: Vec<(u32, VecDeque<Waiter>)>) {
let mut failed: Option<ClientError> = None;
for (count, mut waiters) in chunks {
let result = match &failed {
Some(e) => Err(clone_client_error(e)),
None => {
let result = rpc_fn(count).await;
if let Err(ref e) = result {
failed = Some(clone_client_error(e));
}
result
}
};
deliver(&mut waiters, result, count);
}
}
fn enqueue(queue: &mut VecDeque<Waiter>, first_arrival: &mut Option<Instant>, waiter: Waiter) {
if first_arrival.is_none() {
*first_arrival = Some(Instant::now());
}
queue.push_back(waiter);
set_queue_depth_gauge(queue.len());
}
#[inline]
fn set_queue_depth_gauge(depth: usize) {
#[cfg(feature = "metrics")]
metrics::gauge!("tsoracle.client.driver.queue_depth").set(depth as f64);
#[cfg(not(feature = "metrics"))]
let _ = depth;
}
#[inline]
fn set_in_flight_gauge(state: u8) {
#[cfg(feature = "metrics")]
metrics::gauge!("tsoracle.client.driver.in_flight").set(f64::from(state));
#[cfg(not(feature = "metrics"))]
let _ = state;
}
#[inline]
fn reply_to_waiter(waiter: Waiter, outcome: Result<Vec<Timestamp>, ClientError>) {
if waiter.respond.send(outcome).is_err() {
record_abandoned_waiter();
}
}
#[inline]
fn record_abandoned_waiter() {
#[cfg(feature = "metrics")]
metrics::counter!("tsoracle.client.driver.abandoned_waiters.total").increment(1);
}
fn deliver(
waiters: &mut VecDeque<Waiter>,
result: Result<TimestampRange, ClientError>,
expected: u32,
) {
match result {
Ok(range) => {
if range.count() != expected {
let msg = format!(
"tsoracle protocol violation: requested {} timestamps, server returned {}",
expected,
range.count(),
);
while let Some(w) = waiters.pop_front() {
reply_to_waiter(
w,
Err(ClientError::Rpc(tonic::Status::internal(msg.clone()))),
);
}
return;
}
let mut iter = range.iter();
while let Some(w) = waiters.pop_front() {
let slice: Vec<Timestamp> = iter.by_ref().take(w.count as usize).collect();
debug_assert_eq!(
slice.len(),
w.count as usize,
"chunk_queue established total == sum(count); a short slice means \
either chunk_queue or the response-length check above is wrong"
);
reply_to_waiter(w, Ok(slice));
}
}
Err(e) => {
while let Some(w) = waiters.pop_front() {
reply_to_waiter(w, Err(clone_client_error(&e)));
}
}
}
}
fn clone_client_error(error: &ClientError) -> ClientError {
match error {
ClientError::Rpc(status) => {
let mut cloned = tonic::Status::new(status.code(), status.message());
*cloned.metadata_mut() = status.metadata().clone();
ClientError::Rpc(cloned)
}
ClientError::Transport(transport_error) => {
ClientError::TransportFanout(transport_error.to_string())
}
ClientError::TransportFanout(message) => ClientError::TransportFanout(message.clone()),
ClientError::NoReachableEndpoints => ClientError::NoReachableEndpoints,
ClientError::InvalidEndpoint(endpoint) => ClientError::InvalidEndpoint(endpoint.clone()),
ClientError::InvalidCount(count) => ClientError::InvalidCount(*count),
ClientError::Connector(source) => ClientError::Connector(source.to_string().into()),
ClientError::DriverGone => ClientError::DriverGone,
ClientError::SeqUncertain => ClientError::SeqUncertain,
ClientError::InvalidSeqKey => ClientError::InvalidSeqKey,
}
}
#[cfg(test)]
mod tests {
use super::*;
use parking_lot::Mutex;
use tsoracle_core::LOGICAL_MAX;
fn recording_ok_rpc(
calls: Arc<Mutex<Vec<u32>>>,
) -> impl Fn(u32) -> futures::future::BoxFuture<'static, Result<TimestampRange, ClientError>>
+ Send
+ Sync
+ 'static {
move |count: u32| {
let calls = calls.clone();
Box::pin(async move {
calls.lock().push(count);
Ok(TimestampRange::new(1_000, 0, count))
})
}
}
#[tokio::test]
async fn coalesced_batch_above_per_rpc_cap_is_chunked() {
let calls: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(Vec::new()));
let driver = Arc::new(Driver::spawn(
recording_ok_rpc(calls.clone()),
Duration::from_millis(50),
));
let mut handles = Vec::new();
for _ in 0..4 {
let driver = driver.clone();
handles.push(tokio::spawn(async move { driver.request(100_000).await }));
}
let results = futures::future::join_all(handles).await;
for result in &results {
let timestamps = result
.as_ref()
.expect("task join")
.as_ref()
.expect("request must succeed");
assert_eq!(
timestamps.len(),
100_000,
"each waiter must get its full count"
);
}
let observed = calls.lock().clone();
assert!(
observed.iter().all(|&count| count <= LOGICAL_MAX + 1),
"every RPC must respect the per-call cap; observed counts: {observed:?}",
);
let total: u64 = observed.iter().map(|&count| count as u64).sum();
assert_eq!(
total, 400_000,
"exactly 4 * 100_000 timestamps must be issued across all chunks"
);
assert!(
observed.len() >= 2,
"a 400_000-timestamp coalesced batch must be split into >= 2 RPCs; observed: {observed:?}",
);
}
#[tokio::test]
async fn waiter_above_per_rpc_cap_gets_invalid_count() {
let calls: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(Vec::new()));
let driver = Driver::spawn(recording_ok_rpc(calls), Duration::ZERO);
let result = driver.request(LOGICAL_MAX + 2).await;
assert!(
matches!(result, Err(ClientError::InvalidCount(count)) if count == LOGICAL_MAX + 2),
"expected InvalidCount({}), got {:?}",
LOGICAL_MAX + 2,
result
);
}
#[tokio::test]
async fn oversize_waiter_does_not_poison_sibling_waiters() {
let calls: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(Vec::new()));
let driver = Arc::new(Driver::spawn(
recording_ok_rpc(calls),
Duration::from_millis(50),
));
let small1 = {
let driver = driver.clone();
tokio::spawn(async move { driver.request(5).await })
};
let oversize = {
let driver = driver.clone();
tokio::spawn(async move { driver.request(LOGICAL_MAX + 2).await })
};
let small2 = {
let driver = driver.clone();
tokio::spawn(async move { driver.request(7).await })
};
let small1_r = small1.await.unwrap();
let oversize_r = oversize.await.unwrap();
let small2_r = small2.await.unwrap();
assert!(
matches!(oversize_r, Err(ClientError::InvalidCount(_))),
"oversize waiter must get InvalidCount, got {oversize_r:?}",
);
assert_eq!(
small1_r.expect("small1 must succeed").len(),
5,
"sibling small waiter must still get its full count"
);
assert_eq!(
small2_r.expect("small2 must succeed").len(),
7,
"sibling small waiter must still get its full count"
);
}
#[tokio::test]
async fn short_response_errors_waiters_in_chunk() {
let rpc = |count: u32| -> futures::future::BoxFuture<
'static,
Result<TimestampRange, ClientError>,
> {
Box::pin(async move {
let short = count.saturating_sub(1);
Ok(TimestampRange::new(1_000, 0, short))
})
};
let driver = Driver::spawn(rpc, Duration::ZERO);
let result = driver.request(5).await;
assert!(
matches!(result, Err(ClientError::Rpc(_))),
"short response must error, got {result:?}",
);
}
#[tokio::test]
async fn long_response_errors_waiters_in_chunk() {
let rpc = |count: u32| -> futures::future::BoxFuture<
'static,
Result<TimestampRange, ClientError>,
> {
Box::pin(async move {
let long = count.saturating_add(3);
Ok(TimestampRange::new(1_000, 0, long))
})
};
let driver = Driver::spawn(rpc, Duration::ZERO);
let result = driver.request(5).await;
assert!(
matches!(result, Err(ClientError::Rpc(_))),
"long response must error, got {result:?}",
);
}
#[test]
fn clone_client_error_preserves_rpc_code_and_message() {
let original = ClientError::Rpc(tonic::Status::failed_precondition("nope"));
let cloned = clone_client_error(&original);
match cloned {
ClientError::Rpc(status) => {
assert_eq!(status.code(), tonic::Code::FailedPrecondition);
assert_eq!(status.message(), "nope");
}
other => panic!("expected Rpc, got {other:?}"),
}
}
#[test]
fn clone_client_error_preserves_rpc_metadata_trailers() {
use tonic::metadata::{BinaryMetadataKey, MetadataValue};
use tsoracle_proto::v1::LEADER_HINT_TRAILER_KEY;
let hint_key = BinaryMetadataKey::from_bytes(LEADER_HINT_TRAILER_KEY.as_bytes())
.expect("LEADER_HINT_TRAILER_KEY must be a valid binary metadata key");
let hint_payload: &[u8] = &[1, 2, 3, 4];
let mut status = tonic::Status::failed_precondition("not leader");
status
.metadata_mut()
.insert_bin(hint_key.clone(), MetadataValue::from_bytes(hint_payload));
status
.metadata_mut()
.insert("x-trace-id", "abc-123".parse().expect("valid ascii value"));
match clone_client_error(&ClientError::Rpc(status)) {
ClientError::Rpc(cloned) => {
assert_eq!(cloned.code(), tonic::Code::FailedPrecondition);
assert_eq!(cloned.message(), "not leader");
let trailer = cloned
.metadata()
.get_bin(hint_key)
.expect("leader-hint trailer must survive the clone");
assert_eq!(
trailer.to_bytes().expect("base64-decodable").as_ref(),
hint_payload,
);
assert_eq!(
cloned
.metadata()
.get("x-trace-id")
.expect("ascii header must survive the clone"),
"abc-123",
);
}
other => panic!("expected Rpc, got {other:?}"),
}
}
#[tokio::test]
async fn clone_client_error_maps_transport_to_transport_fanout() {
let endpoint = tonic::transport::Endpoint::from_static("http://127.0.0.1:1");
let transport_err = endpoint
.connect()
.await
.expect_err("connecting to a closed port must fail");
let expected_message = transport_err.to_string();
let original = ClientError::Transport(transport_err);
match clone_client_error(&original) {
ClientError::TransportFanout(message) => {
assert_eq!(message, expected_message);
}
other => panic!("expected TransportFanout, got {other:?}"),
}
}
#[test]
fn clone_client_error_preserves_simple_variants() {
let no_endpoints = clone_client_error(&ClientError::NoReachableEndpoints);
assert!(matches!(no_endpoints, ClientError::NoReachableEndpoints));
let invalid_endpoint =
clone_client_error(&ClientError::InvalidEndpoint("garbage://".into()));
match invalid_endpoint {
ClientError::InvalidEndpoint(s) => assert_eq!(s, "garbage://"),
other => panic!("expected InvalidEndpoint, got {other:?}"),
}
let invalid_count = clone_client_error(&ClientError::InvalidCount(99));
match invalid_count {
ClientError::InvalidCount(c) => assert_eq!(c, 99),
other => panic!("expected InvalidCount, got {other:?}"),
}
let driver_gone = clone_client_error(&ClientError::DriverGone);
assert!(matches!(driver_gone, ClientError::DriverGone));
let seq_uncertain = clone_client_error(&ClientError::SeqUncertain);
assert!(matches!(seq_uncertain, ClientError::SeqUncertain));
let invalid_seq_key = clone_client_error(&ClientError::InvalidSeqKey);
assert!(matches!(invalid_seq_key, ClientError::InvalidSeqKey));
}
#[test]
fn clone_client_error_reclones_transport_fanout_preserving_message() {
let original = ClientError::TransportFanout("dial 10.0.0.7:50051 refused".into());
match clone_client_error(&original) {
ClientError::TransportFanout(message) => {
assert_eq!(message, "dial 10.0.0.7:50051 refused");
}
other => panic!("expected TransportFanout, got {other:?}"),
}
}
#[tokio::test]
async fn run_chunks_fails_subsequent_chunks_fast() {
use std::sync::atomic::{AtomicUsize, Ordering};
let rpc_calls = Arc::new(AtomicUsize::new(0));
let calls_for_rpc = rpc_calls.clone();
let rpc = move |_count: u32| -> futures::future::BoxFuture<
'static,
Result<TimestampRange, ClientError>,
> {
calls_for_rpc.fetch_add(1, Ordering::Relaxed);
Box::pin(async {
Err(ClientError::Rpc(tonic::Status::unavailable(
"synthetic outage",
)))
})
};
let driver = Arc::new(Driver::spawn(rpc, Duration::from_millis(10)));
let mut handles = Vec::new();
for _ in 0..4 {
let driver_handle = driver.clone();
handles.push(tokio::spawn(async move {
driver_handle.request(LOGICAL_MAX + 1).await
}));
}
let results = futures::future::join_all(handles).await;
for r in results {
let outer = r.expect("join");
assert!(
matches!(outer, Err(ClientError::Rpc(_))),
"every waiter must see an Rpc error, got {outer:?}",
);
}
let rpc_count = rpc_calls.load(Ordering::Relaxed);
assert_eq!(rpc_count, 1, "fail-fast must issue exactly one RPC");
}
#[tokio::test]
async fn driver_bounds_in_flight_batch_to_queue_capacity() {
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::watch;
let calls: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(Vec::new()));
let rpc_invocations = Arc::new(AtomicUsize::new(0));
let (released_tx, released_rx) = watch::channel(false);
let calls_for_rpc = calls.clone();
let rpc_invocations_for_rpc = rpc_invocations.clone();
let released_rx_for_rpc = released_rx.clone();
let rpc = move |count: u32| -> futures::future::BoxFuture<
'static,
Result<TimestampRange, ClientError>,
> {
let is_first = rpc_invocations_for_rpc.fetch_add(1, Ordering::SeqCst) == 0;
let mut released = released_rx_for_rpc.clone();
let calls = calls_for_rpc.clone();
Box::pin(async move {
if is_first {
while !*released.borrow() {
released
.changed()
.await
.expect("watch sender must outlive the rpc");
}
}
calls.lock().push(count);
Ok(TimestampRange::new(1_000, 0, count))
})
};
let driver = Arc::new(Driver::spawn(rpc, Duration::ZERO));
let total = 2 * QUEUE_CAPACITY;
let mut handles = Vec::with_capacity(total);
for _ in 0..total {
let driver = driver.clone();
handles.push(tokio::spawn(async move { driver.request(1).await }));
}
for _ in 0..2_000 {
tokio::task::yield_now().await;
}
released_tx.send(true).expect("release the stalled rpc");
let results = futures::future::join_all(handles).await;
for result in results {
let outer = result.expect("join must succeed");
let timestamps = outer.expect("request must succeed");
assert_eq!(timestamps.len(), 1);
}
let observed = calls.lock().clone();
let max_count = *observed.iter().max().expect("at least one rpc was issued");
let batches = observed.len();
assert!(
max_count <= QUEUE_CAPACITY as u32,
"max batch count ({max_count}) exceeded documented bound ({QUEUE_CAPACITY}); fired {total} requests, observed {batches} batches"
);
}
#[tokio::test]
async fn first_chunk_delivers_before_slow_second_chunk_completes() {
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Notify;
let rpc_invocations = Arc::new(AtomicUsize::new(0));
let release_second = Arc::new(Notify::new());
let rpc_invocations_for_rpc = rpc_invocations.clone();
let release_second_for_rpc = release_second.clone();
let rpc = move |count: u32| -> futures::future::BoxFuture<
'static,
Result<TimestampRange, ClientError>,
> {
let invocation = rpc_invocations_for_rpc.fetch_add(1, Ordering::SeqCst);
let release = release_second_for_rpc.clone();
Box::pin(async move {
if invocation >= 1 {
release.notified().await;
}
Ok(TimestampRange::new(1_000, 0, count))
})
};
let driver = Arc::new(Driver::spawn(rpc, Duration::from_millis(50)));
let first = {
let driver = driver.clone();
tokio::spawn(async move { driver.request(LOGICAL_MAX + 1).await })
};
let second = {
let driver = driver.clone();
tokio::spawn(async move { driver.request(LOGICAL_MAX + 1).await })
};
let first_timestamps = tokio::time::timeout(Duration::from_secs(2), first)
.await
.expect("first chunk must deliver before second chunk's rpc completes")
.expect("join")
.expect("first request must succeed");
assert_eq!(first_timestamps.len(), (LOGICAL_MAX + 1) as usize);
assert!(
!second.is_finished(),
"second waiter must still be pending while its chunk's rpc is stalled",
);
release_second.notify_waiters();
let second_timestamps = second
.await
.expect("join")
.expect("second request must succeed");
assert_eq!(second_timestamps.len(), (LOGICAL_MAX + 1) as usize);
}
#[tokio::test]
async fn rpc_closure_panic_surfaces_as_driver_gone() {
let rpc = |_count: u32| -> futures::future::BoxFuture<
'static,
Result<TimestampRange, ClientError>,
> {
Box::pin(async {
panic!("synthetic panic exercising driver supervision");
})
};
let driver = Driver::spawn(rpc, Duration::ZERO);
let result = driver.request(5).await;
assert!(
matches!(result, Err(ClientError::DriverGone)),
"panicking RPC closure must surface as DriverGone, got {result:?}",
);
}
#[tokio::test(start_paused = true)]
async fn lone_waiter_dispatches_after_flush_interval() {
let calls: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(Vec::new()));
let flush = Duration::from_millis(100);
let driver = Driver::spawn(recording_ok_rpc(calls.clone()), flush);
let start = Instant::now();
let timestamps = driver.request(5).await.expect("request must succeed");
let elapsed = start.elapsed();
assert_eq!(
timestamps.len(),
5,
"lone waiter must receive its full count"
);
assert_eq!(
calls.lock().clone(),
vec![5],
"exactly one RPC of count 5 must be issued",
);
assert!(
elapsed >= flush,
"dispatch fired at {elapsed:?}, before the {flush:?} flush deadline",
);
}
mod proptest_invariants {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
// Each case spins up a small runtime and spawns up to ~150
// tasks; 64 cases is enough to exercise interesting
// schedules without making the suite slow.
cases: 64,
.. ProptestConfig::default()
})]
#[test]
fn random_schedules_serve_every_waiter_correctly(
counts in prop::collection::vec(1u32..=MAX_TIMESTAMPS_PER_RPC, 1..150),
flush_micros in 0u64..2_000,
) {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio runtime must build");
runtime.block_on(async {
let calls: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(Vec::new()));
let driver = Arc::new(Driver::spawn(
recording_ok_rpc(calls.clone()),
Duration::from_micros(flush_micros),
));
let mut handles = Vec::with_capacity(counts.len());
for count in counts.iter().copied() {
let driver = driver.clone();
handles.push(tokio::spawn(async move {
driver.request(count).await
}));
}
let results = futures::future::join_all(handles).await;
let mut total_served: u64 = 0;
for (idx, (requested, result)) in counts
.iter()
.zip(results.into_iter())
.enumerate()
{
let timestamps = result
.expect("join must succeed")
.expect("request must succeed");
let served = timestamps.len();
assert_eq!(served, *requested as usize, "request {idx} requested {requested}, served {served}");
total_served += served as u64;
}
let observed = calls.lock().clone();
for rpc_count in &observed {
assert!(*rpc_count <= MAX_TIMESTAMPS_PER_RPC, "rpc dispatched with count {rpc_count} > per-call cap {MAX_TIMESTAMPS_PER_RPC}; observed: {observed:?}");
assert!(*rpc_count > 0, "rpc dispatched with count 0");
}
let total_requested: u64 = counts.iter().map(|c| u64::from(*c)).sum();
let total_rpc: u64 = observed.iter().map(|c| u64::from(*c)).sum();
assert_eq!(total_served, total_requested, "served {total_served} timestamps, requested {total_requested}");
assert_eq!(total_rpc, total_requested, "rpc-side total {total_rpc} != requested {total_requested}");
});
}
}
}
#[cfg(feature = "metrics")]
mod abandoned_waiter_metric {
use super::*;
use metrics_util::{
MetricKind,
debugging::{DebugValue, DebuggingRecorder},
};
type RecordedMetric = (
metrics_util::CompositeKey,
Option<metrics::Unit>,
Option<metrics::SharedString>,
DebugValue,
);
const METRIC: &str = "tsoracle.client.driver.abandoned_waiters.total";
fn counter(snapshot: &[RecordedMetric], name: &str) -> u64 {
for (composite, _unit, _desc, value) in snapshot {
if composite.kind() == MetricKind::Counter && composite.key().name() == name {
if let DebugValue::Counter(n) = value {
return *n;
}
}
}
0
}
#[test]
fn dropped_receiver_increments_counter() {
let recorder = DebuggingRecorder::new();
let snapshotter = recorder.snapshotter();
metrics::with_local_recorder(&recorder, || {
let (respond, rx) = oneshot::channel();
drop(rx);
reply_to_waiter(Waiter { count: 1, respond }, Ok(Vec::new()));
});
assert_eq!(
counter(&snapshotter.snapshot().into_vec(), METRIC),
1,
"a send to a dropped receiver must count exactly one abandoned waiter",
);
}
#[test]
fn live_receiver_does_not_increment_counter() {
let recorder = DebuggingRecorder::new();
let snapshotter = recorder.snapshotter();
metrics::with_local_recorder(&recorder, || {
let (respond, _rx) = oneshot::channel();
reply_to_waiter(Waiter { count: 1, respond }, Ok(Vec::new()));
});
assert_eq!(
counter(&snapshotter.snapshot().into_vec(), METRIC),
0,
"a delivery to a live receiver must not count an abandoned waiter",
);
}
}
}