use google_cloud_gax::options::RequestOptionsBuilder;
use google_cloud_gax::retry_policy::{NeverRetry, RetryPolicyExt};
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinSet;
use tokio::time::Sleep;
use tokio_util::sync::CancellationToken;
use super::actor::batch_resolve_publish_futures;
use super::options::HedgingOptions;
use super::token_bucket::TokenBucket;
use crate::error::PublishError;
use crate::generated::gapic_dataplane::client::Publisher as GapicPublisher;
use crate::model::PublishResponse;
type BatchSenders = Vec<oneshot::Sender<Result<String, PublishError>>>;
pub(crate) struct BatchState {
pub msgs: Arc<Vec<crate::model::Message>>,
pub txs: Mutex<
Option<(
BatchSenders,
tokio::sync::oneshot::Sender<crate::Result<()>>,
)>,
>,
pub client: GapicPublisher,
pub topic: String,
pub token_bucket: Arc<TokenBucket>,
pub start_time: Option<wkt::Timestamp>,
pub start_instant: tokio::time::Instant,
pub total_timeout: Option<Duration>,
pub cancel_token: CancellationToken,
}
impl BatchState {
pub(crate) fn new(
msgs: Vec<crate::model::Message>,
txs: BatchSenders,
client: GapicPublisher,
topic: String,
token_bucket: Arc<TokenBucket>,
total_timeout: Option<Duration>,
done_tx: tokio::sync::oneshot::Sender<crate::Result<()>>,
) -> Self {
Self {
msgs: Arc::new(msgs),
txs: Mutex::new(Some((txs, done_tx))),
client,
topic,
token_bucket,
start_time: wkt::Timestamp::try_from(std::time::SystemTime::now()).ok(),
start_instant: tokio::time::Instant::now(),
total_timeout,
cancel_token: CancellationToken::new(),
}
}
pub(crate) async fn send_initial(&self) {
let request = self
.client
.publish()
.set_topic(self.topic.clone())
.set_messages((*self.msgs).clone())
.set_pubsub_client_telemetry_header(0, self.start_time);
tokio::select! {
_ = self.cancel_token.cancelled() => {}
res = request.send() => {
self.complete(res);
}
}
}
pub(crate) async fn send_hedged_rpc(&self, attempt_count: i32) {
if self.cancel_token.is_cancelled() {
return;
}
const MAX_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(10);
let timeout = self
.total_timeout
.map(|t| {
let elapsed = self.start_instant.elapsed();
t.saturating_sub(elapsed)
})
.unwrap_or(MAX_ATTEMPT_TIMEOUT)
.min(MAX_ATTEMPT_TIMEOUT);
if timeout.is_zero() {
return;
}
let request = self
.client
.publish()
.set_topic(self.topic.clone())
.set_messages((*self.msgs).clone())
.set_pubsub_client_telemetry_header(attempt_count, self.start_time)
.with_retry_policy(NeverRetry.with_time_limit(timeout));
tokio::select! {
_ = self.cancel_token.cancelled() => {}
res = request.send() => {
if let Ok(resp) = res {
self.complete(Ok(resp));
}
}
}
}
fn complete(&self, resp: crate::Result<PublishResponse>) {
let mut lock = self.txs.lock().unwrap();
if let Some((txs, done_tx)) = lock.take() {
self.cancel_token.cancel();
if resp.is_ok() {
self.token_bucket.refill();
}
let _ = done_tx.send(batch_resolve_publish_futures(resp, txs));
}
}
}
struct HedgeItem {
state: Arc<BatchState>,
deadline: tokio::time::Instant,
attempt_count: i32,
}
#[derive(Debug, Clone)]
pub(crate) struct HedgingSchedulerHandle {
pub tx: mpsc::UnboundedSender<Arc<BatchState>>,
pub token_bucket: Arc<TokenBucket>,
}
impl HedgingSchedulerHandle {
pub(crate) fn dispatch(
&self,
msgs: Vec<crate::model::Message>,
txs: Vec<oneshot::Sender<Result<String, PublishError>>>,
client: GapicPublisher,
topic: String,
total_timeout: Option<Duration>,
inflight: &mut JoinSet<crate::Result<()>>,
) {
let (done_tx, done_rx) = oneshot::channel();
let state = Arc::new(BatchState::new(
msgs,
txs,
client,
topic,
self.token_bucket.clone(),
total_timeout,
done_tx,
));
inflight.spawn(async move {
let res = done_rx.await.map_err(crate::Error::io)?; res
});
let state_clone = state.clone();
tokio::spawn(async move {
state_clone.send_initial().await;
});
let _ = self.tx.send(state);
}
}
pub(crate) struct HedgingScheduler {
rx: mpsc::UnboundedReceiver<Arc<BatchState>>,
queue: VecDeque<HedgeItem>,
delay: Duration,
timer: Option<Pin<Box<Sleep>>>,
}
impl HedgingScheduler {
fn new(rx: mpsc::UnboundedReceiver<Arc<BatchState>>, delay: Duration) -> Self {
Self {
rx,
queue: VecDeque::new(),
delay,
timer: None,
}
}
pub(crate) fn spawn(opts: HedgingOptions) -> HedgingSchedulerHandle {
let token_bucket = Arc::new(TokenBucket::new(opts.max_tokens, opts.refill_ratio));
let (tx, rx) = mpsc::unbounded_channel();
let scheduler = Self::new(rx, opts.delay);
tokio::spawn(scheduler.run());
HedgingSchedulerHandle { tx, token_bucket }
}
fn handle_new_batch(&mut self, state: Arc<BatchState>) {
let deadline = tokio::time::Instant::now() + self.delay;
self.queue.push_back(HedgeItem {
state,
deadline,
attempt_count: 1,
});
if self.timer.is_none() {
self.timer = Some(Box::pin(tokio::time::sleep_until(deadline)));
}
}
fn handle_expired_hedges(&mut self) {
let now = tokio::time::Instant::now();
while let Some(item) = self.queue.front() {
if item.deadline <= now {
let item = self.queue.pop_front().unwrap();
if !item.state.cancel_token.is_cancelled() && item.state.token_bucket.try_acquire()
{
let state = item.state.clone();
tokio::spawn(async move {
state.send_hedged_rpc(item.attempt_count).await;
});
self.queue.push_back(HedgeItem {
deadline: now + self.delay,
state: item.state,
attempt_count: item.attempt_count + 1,
});
}
} else {
break;
}
}
self.timer = self
.queue
.front()
.map(|item| Box::pin(tokio::time::sleep_until(item.deadline)));
}
pub(crate) async fn run(mut self) {
loop {
tokio::select! {
item = self.rx.recv() => {
match item {
Some(state) => self.handle_new_batch(state),
None => {
break;
}
}
}
_ = async { self.timer.as_mut().unwrap().await }, if self.timer.is_some() => {
self.handle_expired_hedges();
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::google::pubsub::v1::pubsub_client_telemetry::Operation;
use crate::model::Message;
use crate::publisher::publish_telemetry::parse_pubsub_client_telemetry_header;
use google_cloud_test_macros::tokio_test_no_panics;
mockall::mock! {
#[derive(Debug)]
GapicPublisher {}
impl crate::generated::gapic_dataplane::stub::Publisher for GapicPublisher {
async fn publish(&self, req: crate::model::PublishRequest, _options: crate::RequestOptions) -> crate::Result<crate::Response<crate::model::PublishResponse>>;
}
}
mockall::mock! {
#[derive(Debug)]
GapicPublisherWithFuture {}
impl crate::generated::gapic_dataplane::stub::Publisher for GapicPublisherWithFuture {
fn publish(&self, req: crate::model::PublishRequest, _options: google_cloud_gax::options::RequestOptions) -> impl Future<Output=google_cloud_gax::Result<google_cloud_gax::response::Response<crate::model::PublishResponse>>> + Send;
}
}
fn mock_publish_response(msg_id: &str) -> crate::Result<crate::Response<PublishResponse>> {
Ok(crate::Response::from(
PublishResponse::new().set_message_ids([msg_id]),
))
}
#[allow(clippy::type_complexity)]
fn test_batch_state(
client: GapicPublisher,
token_bucket: Arc<TokenBucket>,
) -> (
Arc<BatchState>,
oneshot::Receiver<Result<String, PublishError>>,
oneshot::Receiver<crate::Result<()>>,
) {
test_batch_state_with_timeout(client, token_bucket, None)
}
#[allow(clippy::type_complexity)]
fn test_batch_state_with_timeout(
client: GapicPublisher,
token_bucket: Arc<TokenBucket>,
total_timeout: Option<Duration>,
) -> (
Arc<BatchState>,
oneshot::Receiver<Result<String, PublishError>>,
oneshot::Receiver<crate::Result<()>>,
) {
let (tx, rx) = oneshot::channel();
let (done_tx, done_rx) = oneshot::channel();
let state = Arc::new(BatchState::new(
vec![Message::new().set_data("test")],
vec![tx],
client,
"topic".to_string(),
token_bucket,
total_timeout,
done_tx,
));
(state, rx, done_rx)
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_initial_succeeds_fast() -> anyhow::Result<()> {
let mut mock = MockGapicPublisher::new();
mock.expect_publish()
.return_once(|_, _| mock_publish_response("msg-initial"));
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
state.send_initial().await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-initial");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_initial_fails() -> anyhow::Result<()> {
let mut mock = MockGapicPublisher::new();
mock.expect_publish().return_once(|_, _| {
Err(crate::Error::io(std::io::Error::other(
"fatal network error",
)))
});
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
state.send_initial().await;
let publish_res = rx.await?;
assert!(publish_res.is_err());
assert!(state.cancel_token.is_cancelled());
let done_res = done_rx.await?;
assert!(done_res.is_err());
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_cancellation_when_initial_finishes_first() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
let mut seq = mockall::Sequence::new();
mock.expect_publish()
.times(1)
.in_sequence(&mut seq)
.returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_millis(50)).await;
mock_publish_response("msg-initial")
})
});
mock.expect_publish()
.times(1)
.in_sequence(&mut seq)
.returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_secs(10)).await;
mock_publish_response("msg-hedged")
})
});
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
assert!(!state.cancel_token.is_cancelled());
let initial_handle = {
let state = state.clone();
tokio::spawn(async move {
state.send_initial().await;
})
};
let hedged_handle = {
let state = state.clone();
tokio::spawn(async move {
state.send_hedged_rpc(1).await;
})
};
initial_handle.await?;
assert!(state.cancel_token.is_cancelled());
let _ = hedged_handle.await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-initial");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_cancellation_when_hedged_succeeds_first() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
let mut seq = mockall::Sequence::new();
mock.expect_publish()
.times(1)
.in_sequence(&mut seq)
.returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_secs(10)).await;
mock_publish_response("msg-initial")
})
});
mock.expect_publish()
.times(1)
.in_sequence(&mut seq)
.returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_millis(20)).await;
mock_publish_response("msg-hedged")
})
});
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
let initial_handle = {
let state = state.clone();
tokio::spawn(async move {
state.send_initial().await;
})
};
let hedged_handle = {
let state = state.clone();
tokio::spawn(async move {
state.send_hedged_rpc(1).await;
})
};
let _ = hedged_handle.await;
assert!(state.cancel_token.is_cancelled());
initial_handle.await?;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-hedged");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_hedged_errors_ignored() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
let mut seq = mockall::Sequence::new();
mock.expect_publish()
.times(1)
.in_sequence(&mut seq)
.returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_secs(10)).await;
mock_publish_response("msg-initial")
})
});
mock.expect_publish()
.times(1)
.in_sequence(&mut seq)
.returning(|_, _| {
Box::pin(async {
Err(crate::Error::io(std::io::Error::other(
"fatal network error",
)))
})
});
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
let initial_handle = {
let state = state.clone();
tokio::spawn(async move {
state.send_initial().await;
})
};
let hedged_handle = {
let state = state.clone();
tokio::spawn(async move {
state.send_hedged_rpc(1).await;
})
};
let _ = hedged_handle.await;
assert!(!state.cancel_token.is_cancelled());
initial_handle.await?;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-initial");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_hedged_rpc_skipped_if_already_cancelled() -> anyhow::Result<()> {
let mock = MockGapicPublisher::new();
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, _rx, _done_rx) = test_batch_state(client, token_bucket);
state.cancel_token.cancel();
state.send_hedged_rpc(1).await;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_hedged_succeeds_when_initial_hangs() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
mock.expect_publish().times(1).returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_secs(60)).await;
mock_publish_response("msg-initial")
})
});
mock.expect_publish()
.times(1)
.returning(|_, _| Box::pin(async { mock_publish_response("msg-hedged") }));
let client = GapicPublisher::from_stub(mock);
let opts = HedgingOptions {
delay: Duration::from_millis(100),
max_tokens: 10,
refill_ratio: 0.1,
};
let scheduler_handle = HedgingScheduler::spawn(opts);
let token_bucket = scheduler_handle.token_bucket.clone();
for _ in 0..10 {
token_bucket.refill();
}
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
let state_clone = state.clone();
tokio::spawn(async move {
let _ = state_clone.send_initial().await;
});
scheduler_handle.tx.send(state.clone())?;
tokio::time::advance(Duration::from_millis(150)).await;
tokio::task::yield_now().await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-hedged");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_scheduler_shuts_down_promptly_when_batch_done() -> anyhow::Result<()> {
let mock = MockGapicPublisher::new();
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (tx, rx) = mpsc::unbounded_channel();
let scheduler = HedgingScheduler::new(rx, Duration::from_secs(10));
let task = tokio::spawn(scheduler.run());
let (state, _rx, done_rx) = test_batch_state(client, token_bucket);
state.complete(Ok(
PublishResponse::new().set_message_ids(["msg".to_string()])
));
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
tx.send(state)?;
drop(tx);
tokio::time::timeout(Duration::from_millis(100), task).await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_multiple_hedged_attempts() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
mock.expect_publish().times(1).returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_secs(10)).await;
mock_publish_response("msg-initial")
})
});
mock.expect_publish().times(1).returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_secs(10)).await;
mock_publish_response("msg-hedged-1")
})
});
mock.expect_publish()
.times(1)
.returning(|_, _| Box::pin(async { mock_publish_response("msg-hedged-2") }));
let client = GapicPublisher::from_stub(mock);
let opts = HedgingOptions {
delay: Duration::from_millis(100),
max_tokens: 10,
refill_ratio: 0.1,
};
let scheduler_handle = HedgingScheduler::spawn(opts);
let token_bucket = scheduler_handle.token_bucket.clone();
for _ in 0..20 {
token_bucket.refill();
}
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
let state_clone = state.clone();
tokio::spawn(async move {
state_clone.send_initial().await;
});
scheduler_handle.tx.send(state.clone())?;
tokio::time::advance(Duration::from_millis(150)).await;
tokio::task::yield_now().await;
assert!(!state.cancel_token.is_cancelled());
tokio::time::advance(Duration::from_millis(100)).await;
tokio::task::yield_now().await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-hedged-2");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_throttled_hedge_discarded_and_initial_completes() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
mock.expect_publish().times(1).returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_millis(200)).await;
mock_publish_response("msg-initial")
})
});
let client = GapicPublisher::from_stub(mock);
let opts = HedgingOptions {
delay: Duration::from_millis(100),
max_tokens: 10,
refill_ratio: 0.1,
};
let scheduler_handle = HedgingScheduler::spawn(opts);
let token_bucket = scheduler_handle.token_bucket.clone();
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
let state_clone = state.clone();
tokio::spawn(async move {
state_clone.send_initial().await;
});
scheduler_handle.tx.send(state.clone())?;
tokio::time::advance(Duration::from_millis(100)).await;
tokio::task::yield_now().await;
assert!(!state.cancel_token.is_cancelled());
tokio::time::advance(Duration::from_millis(150)).await;
tokio::task::yield_now().await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-initial");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_scheduler_discards_already_completed_batch() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
mock.expect_publish().times(1).returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_millis(20)).await;
mock_publish_response("msg-initial")
})
});
let client = GapicPublisher::from_stub(mock);
let opts = HedgingOptions {
delay: Duration::from_millis(100),
max_tokens: 10,
refill_ratio: 0.1,
};
let scheduler_handle = HedgingScheduler::spawn(opts);
let token_bucket = scheduler_handle.token_bucket.clone();
for _ in 0..10 {
token_bucket.refill();
}
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
let state_clone = state.clone();
tokio::spawn(async move {
state_clone.send_initial().await;
});
scheduler_handle.tx.send(state.clone())?;
tokio::time::advance(Duration::from_millis(50)).await;
tokio::task::yield_now().await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-initial");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
tokio::time::advance(Duration::from_millis(100)).await;
tokio::task::yield_now().await;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_scheduler_multiple_concurrent_batches() -> anyhow::Result<()> {
let mut mock_a = MockGapicPublisherWithFuture::new();
mock_a.expect_publish().times(1).returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_secs(10)).await;
mock_publish_response("msg-initial-a")
})
});
mock_a
.expect_publish()
.times(1)
.returning(|_, _| Box::pin(async { mock_publish_response("msg-hedged-a") }));
let mut mock_b = MockGapicPublisherWithFuture::new();
mock_b.expect_publish().times(1).returning(|_, _| {
Box::pin(async {
tokio::time::sleep(Duration::from_secs(10)).await;
mock_publish_response("msg-initial-b")
})
});
mock_b
.expect_publish()
.times(1)
.returning(|_, _| Box::pin(async { mock_publish_response("msg-hedged-b") }));
let opts = HedgingOptions {
delay: Duration::from_millis(100),
max_tokens: 10,
refill_ratio: 0.1,
};
let scheduler_handle = HedgingScheduler::spawn(opts);
let token_bucket = scheduler_handle.token_bucket.clone();
for _ in 0..20 {
token_bucket.refill();
}
let (state_a, rx_a, done_rx_a) =
test_batch_state(GapicPublisher::from_stub(mock_a), token_bucket.clone());
let (state_b, rx_b, done_rx_b) =
test_batch_state(GapicPublisher::from_stub(mock_b), token_bucket.clone());
let state_a_clone = state_a.clone();
tokio::spawn(async move {
state_a_clone.send_initial().await;
});
scheduler_handle.tx.send(state_a.clone())?;
tokio::time::advance(Duration::from_millis(50)).await;
tokio::task::yield_now().await;
let state_b_clone = state_b.clone();
tokio::spawn(async move {
state_b_clone.send_initial().await;
});
scheduler_handle.tx.send(state_b.clone())?;
tokio::time::advance(Duration::from_millis(60)).await;
tokio::task::yield_now().await;
let msg_a = rx_a.await??;
assert_eq!(msg_a, "msg-hedged-a");
assert!(state_a.cancel_token.is_cancelled());
assert!(done_rx_a.await.is_ok_and(|r| r.is_ok()));
assert!(!state_b.cancel_token.is_cancelled());
tokio::time::advance(Duration::from_millis(50)).await;
tokio::task::yield_now().await;
let msg_b = rx_b.await??;
assert_eq!(msg_b, "msg-hedged-b");
assert!(state_b.cancel_token.is_cancelled());
assert!(done_rx_b.await.is_ok_and(|r| r.is_ok()));
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_hedging_telemetry_progression() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
let recorded_ops = Arc::new(std::sync::Mutex::new(Vec::new()));
let ops_clone = recorded_ops.clone();
mock.expect_publish().times(3).returning(move |_, options| {
let telemetry = parse_pubsub_client_telemetry_header(&options)
.expect("telemetry header should be present and valid");
let attempt = if let Some(Operation::PublishOperation(ref op)) = telemetry.operation {
ops_clone.lock().unwrap().push(*op);
op.hedged_attempt_count
} else {
-1
};
Box::pin(async move {
if attempt < 2 {
tokio::time::sleep(Duration::from_secs(10)).await;
}
mock_publish_response("msg-done")
})
});
let client = GapicPublisher::from_stub(mock);
let opts = HedgingOptions {
delay: Duration::from_millis(100),
max_tokens: 10,
refill_ratio: 0.1,
};
let scheduler_handle = HedgingScheduler::spawn(opts);
let token_bucket = scheduler_handle.token_bucket.clone();
for _ in 0..20 {
token_bucket.refill();
}
let (state, rx, done_rx) = test_batch_state(client, token_bucket);
let state_clone = state.clone();
tokio::spawn(async move {
state_clone.send_initial().await;
});
scheduler_handle.tx.send(state.clone())?;
tokio::time::advance(Duration::from_millis(150)).await;
tokio::task::yield_now().await;
assert!(!state.cancel_token.is_cancelled());
tokio::time::advance(Duration::from_millis(100)).await;
tokio::task::yield_now().await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-done");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
let ops = recorded_ops.lock().unwrap().clone();
assert_eq!(ops.len(), 3);
assert_eq!(ops[0].hedged_attempt_count, 0);
assert_eq!(ops[1].hedged_attempt_count, 1);
assert_eq!(ops[2].hedged_attempt_count, 2);
let start_time = ops[0].publish_start_time;
assert!(start_time.is_some());
assert_eq!(ops[1].publish_start_time, start_time);
assert_eq!(ops[2].publish_start_time, start_time);
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_hedged_rpc_timeout_clamped_to_remaining_time() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
mock.expect_publish().times(1).returning(|_, options| {
let policy = options.retry_policy();
let policy = policy.as_ref().expect("retry policy must be set");
let state = google_cloud_gax::retry_state::RetryState::new(false)
.set_start(tokio::time::Instant::now().into_std());
let remaining = policy.remaining_time(&state);
assert_eq!(remaining, Some(Duration::from_secs(3)));
Box::pin(async { mock_publish_response("msg-hedged") })
});
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, rx, done_rx) =
test_batch_state_with_timeout(client, token_bucket, Some(Duration::from_secs(5)));
tokio::time::advance(Duration::from_secs(2)).await;
state.send_hedged_rpc(1).await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-hedged");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_hedged_rpc_timeout_capped_at_max_attempt_timeout() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
mock.expect_publish().times(1).returning(|_, options| {
let policy = options.retry_policy();
let policy = policy.as_ref().expect("retry policy must be set");
let state = google_cloud_gax::retry_state::RetryState::new(false)
.set_start(tokio::time::Instant::now().into_std());
let remaining = policy.remaining_time(&state);
assert_eq!(remaining, Some(Duration::from_secs(10)));
Box::pin(async { mock_publish_response("msg-hedged") })
});
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, rx, done_rx) =
test_batch_state_with_timeout(client, token_bucket, Some(Duration::from_secs(600)));
tokio::time::advance(Duration::from_secs(1)).await;
state.send_hedged_rpc(1).await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-hedged");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_hedged_rpc_skipped_when_total_timeout_expired() -> anyhow::Result<()> {
let mock = MockGapicPublisher::new();
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, _rx, _done_rx) =
test_batch_state_with_timeout(client, token_bucket, Some(Duration::from_millis(500)));
tokio::time::advance(Duration::from_secs(1)).await;
state.send_hedged_rpc(1).await;
assert!(!state.cancel_token.is_cancelled());
Ok(())
}
#[tokio_test_no_panics(start_paused = true)]
async fn test_hedged_rpc_without_total_timeout() -> anyhow::Result<()> {
let mut mock = MockGapicPublisherWithFuture::new();
mock.expect_publish().times(1).returning(|_, options| {
let policy = options.retry_policy();
let policy = policy.as_ref().expect("retry policy must be set");
let state = google_cloud_gax::retry_state::RetryState::new(false)
.set_start(tokio::time::Instant::now().into_std());
let remaining = policy.remaining_time(&state);
assert_eq!(remaining, Some(Duration::from_secs(10)));
Box::pin(async { mock_publish_response("msg-hedged") })
});
let client = GapicPublisher::from_stub(mock);
let token_bucket = Arc::new(TokenBucket::new(10, 0.1));
let (state, rx, done_rx) = test_batch_state_with_timeout(client, token_bucket, None);
state.send_hedged_rpc(1).await;
let msg_id = rx.await??;
assert_eq!(msg_id, "msg-hedged");
assert!(state.cancel_token.is_cancelled());
done_rx.await??;
Ok(())
}
}