use std::{
convert::TryFrom,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use async_channel as mpmc;
use futures::{
pin_mut,
stream::{self, Stream},
StreamExt,
};
use pin_project::pin_project;
use tonic::metadata::MetadataValue;
use tracing::debug;
use crate::{
auth::grpc::{AuthGrpcService, OAuthTokenSource},
pubsub::{api, PubSubRetryCheck},
retry_policy::{exponential_backoff, ExponentialBackoff, RetryOperation, RetryPolicy},
};
config_default! {
#[derive(Debug, Clone, Copy, Eq, PartialEq, serde::Deserialize)]
pub struct StreamSubscriptionConfig {
#[serde(with = "humantime_serde")]
@default(Duration::from_secs(10), "StreamSubscriptionConfig::default_stream_ack_deadline")
pub stream_ack_deadline: Duration,
@default(1000, "StreamSubscriptionConfig::default_max_outstanding_messages")
pub max_outstanding_messages: i64,
@default(0, "StreamSubscriptionConfig::default_max_outstanding_bytes")
pub max_outstanding_bytes: i64,
#[deprecated]
@default(0, "StreamSubscriptionConfig::default_ack_channel_capacity")
pub ack_channel_capacity: usize,
}
}
#[derive(Debug, PartialEq)]
enum UserAck {
Ack { id: String },
Modify { id: String, seconds: i32 },
}
#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
#[error("cannot ack/nack/modify because the stream was dropped")]
pub struct AcknowledgeError {
_private: (),
}
impl AcknowledgeError {
fn from_send_err<T>(_err: mpmc::SendError<T>) -> Self {
AcknowledgeError { _private: () }
}
}
const MAX_DEADLINE_SEC: u32 = 600;
#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
pub enum ModifyAcknowledgeError {
#[error(transparent)]
Modify(#[from] AcknowledgeError),
#[error("deadline must be between 0 and {MAX_DEADLINE_SEC} seconds, given {seconds}")]
InvalidDeadline {
seconds: u32,
},
}
#[derive(Debug)]
pub struct AcknowledgeToken {
id: String,
channel: mpmc::Sender<UserAck>,
delivery_attempt: i32,
}
impl AcknowledgeToken {
pub async fn ack(self) -> Result<(), AcknowledgeError> {
self.channel
.send(UserAck::Ack { id: self.id })
.await
.map_err(AcknowledgeError::from_send_err)?;
Ok(())
}
pub async fn nack(self) -> Result<(), AcknowledgeError> {
self.channel
.send(UserAck::Modify {
id: self.id,
seconds: 0,
})
.await
.map_err(AcknowledgeError::from_send_err)?;
Ok(())
}
pub async fn modify_deadline(&mut self, seconds: u32) -> Result<(), ModifyAcknowledgeError> {
if seconds > MAX_DEADLINE_SEC {
return Err(ModifyAcknowledgeError::InvalidDeadline { seconds });
}
self.channel
.send(UserAck::Modify {
id: self.id.clone(),
seconds: i32::try_from(seconds).expect("deadline must fit in i32"),
})
.await
.map_err(|err| ModifyAcknowledgeError::Modify(AcknowledgeError::from_send_err(err)))?;
Ok(())
}
pub fn delivery_attempt(&self) -> i32 {
self.delivery_attempt
}
}
fn create_initial_streaming_pull_request(
subscription: String,
client_id: String,
config: &StreamSubscriptionConfig,
) -> api::StreamingPullRequest {
api::StreamingPullRequest {
subscription,
client_id,
stream_ack_deadline_seconds: i32::try_from(config.stream_ack_deadline.as_secs())
.expect("ack deadline seconds should fit in i32"),
max_outstanding_messages: config.max_outstanding_messages,
modify_deadline_seconds: Vec::default(),
modify_deadline_ack_ids: Vec::default(),
max_outstanding_bytes: config.max_outstanding_bytes,
ack_ids: Vec::default(),
}
}
fn create_subsequent_streaming_pull_request(
ack_ids: Vec<String>,
modify_deadline_seconds: Vec<i32>,
modify_deadline_ack_ids: Vec<String>,
config: &StreamSubscriptionConfig,
) -> api::StreamingPullRequest {
api::StreamingPullRequest {
ack_ids,
subscription: String::default(),
client_id: String::default(),
stream_ack_deadline_seconds: i32::try_from(config.stream_ack_deadline.as_secs())
.expect("ack deadline seconds should fit in i32"),
max_outstanding_messages: 0,
modify_deadline_seconds,
modify_deadline_ack_ids,
max_outstanding_bytes: 0,
}
}
fn create_streaming_pull_request_stream(
subscription: String,
client_id: String,
user_acks: impl Stream<Item = UserAck>,
config: StreamSubscriptionConfig,
) -> impl Stream<Item = api::StreamingPullRequest> {
async_stream::stream! {
yield create_initial_streaming_pull_request(subscription, client_id, &config);
const MAX_PER_REQUEST_CHANGES: usize = 1000; pin_mut!(user_acks);
let mut user_ack_batches = user_acks.ready_chunks(MAX_PER_REQUEST_CHANGES);
while let Some(user_ack_batch) = user_ack_batches.next().await {
let mut acks = Vec::with_capacity(user_ack_batch.len());
let mut deadlines = Vec::new();
let mut deadline_acks = Vec::new();
for user_ack in user_ack_batch {
match user_ack {
UserAck::Ack { id } => acks.push(id),
UserAck::Modify { id, seconds } => {
deadlines.push(seconds);
deadline_acks.push(id);
}
};
}
yield create_subsequent_streaming_pull_request(
acks,
deadlines,
deadline_acks,
&config
);
}
}
}
#[pin_project]
pub struct StreamSubscription<C = crate::DefaultConnector, R = ExponentialBackoff<PubSubRetryCheck>>
{
state: StreamState<C, R>,
_p: std::marker::PhantomPinned,
}
enum StreamState<C, R> {
Initialized {
client: api::subscriber_client::SubscriberClient<
AuthGrpcService<tonic::transport::Channel, OAuthTokenSource<C>>,
>,
subscription: String,
config: StreamSubscriptionConfig,
retry_policy: R,
},
Transition,
Streaming(
stream::BoxStream<'static, Result<(AcknowledgeToken, api::PubsubMessage), tonic::Status>>,
),
}
impl<C> StreamSubscription<C> {
pub(super) fn new(
client: api::subscriber_client::SubscriberClient<
AuthGrpcService<tonic::transport::Channel, OAuthTokenSource<C>>,
>,
subscription: String,
config: StreamSubscriptionConfig,
) -> Self {
StreamSubscription {
state: StreamState::Initialized {
client,
subscription,
config,
retry_policy: ExponentialBackoff::new(
PubSubRetryCheck::default(),
Self::default_retry_configuration(),
),
},
_p: std::marker::PhantomPinned,
}
}
pub fn default_retry_configuration() -> exponential_backoff::Config {
exponential_backoff::Config {
initial_interval: Duration::from_millis(100),
max_interval: Duration::from_secs(10),
multiplier: 2.0,
..Default::default()
}
}
}
impl<C, OldR> StreamSubscription<C, OldR> {
pub fn with_retry_policy<R>(self, new_retry_policy: R) -> StreamSubscription<C, R>
where
R: RetryPolicy<(), tonic::Status>,
{
use StreamState::Initialized;
StreamSubscription {
state: match self.state {
Initialized {
client,
subscription,
config,
retry_policy: _old,
} => Initialized {
client,
subscription,
config,
retry_policy: new_retry_policy,
},
_ => unreachable!(
"state only changes in `poll_next`, which can't be called while `self` is \
movable"
),
},
_p: self._p,
}
}
}
impl<C, R> Stream for StreamSubscription<C, R>
where
C: crate::Connect + Clone + Send + Sync + 'static,
R: RetryPolicy<(), tonic::Status> + Send + 'static,
R::RetryOp: Send + 'static,
<R::RetryOp as RetryOperation<(), tonic::Status>>::Sleep: Send + 'static,
{
type Item = Result<(AcknowledgeToken, api::PubsubMessage), tonic::Status>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
use StreamState::{Initialized, Streaming, Transition};
let this = self.project();
loop {
return match this.state {
Initialized { .. } => {
match std::mem::replace(this.state, Transition) {
Initialized {
client,
subscription,
config,
retry_policy,
} => {
*this.state = Streaming(Box::pin(stream_from_client(
client,
subscription,
config,
retry_policy,
)));
continue;
}
_ => unreachable!("just checked state"),
}
}
Transition => {
unreachable!("transition state should be transient and not witnessable")
}
Streaming(stream) => stream.poll_next_unpin(cx),
};
}
}
}
fn stream_from_client<C, R>(
mut client: api::subscriber_client::SubscriberClient<
AuthGrpcService<tonic::transport::Channel, OAuthTokenSource<C>>,
>,
subscription: String,
config: StreamSubscriptionConfig,
mut retry_policy: R,
) -> impl Stream<Item = Result<(AcknowledgeToken, api::PubsubMessage), tonic::Status>> + Send + 'static
where
C: crate::Connect + Clone + Send + Sync + 'static,
R: RetryPolicy<(), tonic::Status> + Send + 'static,
R::RetryOp: Send + 'static,
<R::RetryOp as RetryOperation<(), tonic::Status>>::Sleep: Send + 'static,
{
let subscription_meta =
MetadataValue::from_str(&subscription).expect("valid subscription metadata");
let client_id = uuid::Uuid::new_v4().to_string();
let (sender, receiver) = mpmc::bounded(
usize::try_from(config.max_outstanding_messages)
.expect("outstanding messages should fit in usize"),
);
async_stream::stream! {
let mut retry_op = None;
'reconnect: loop {
let request_stream = create_streaming_pull_request_stream(
subscription.clone(),
client_id.clone(),
receiver.clone(),
config,
);
debug!(message="connecting streaming pull stream", %subscription, %client_id);
let mut error = match client
.streaming_pull(request_stream)
.await
.map(|response| response.into_inner())
{
Err(err) => err,
Ok(mut message_stream) => 'read: loop {
match message_stream.next().await {
None => break 'read tonic::Status::aborted("unexpected end of stream"),
Some(Err(err)) => break 'read err,
Some(Ok(response)) => {
retry_op = None;
for message in response.received_messages {
let ack_token = AcknowledgeToken {
id: message.ack_id,
channel: sender.clone(),
delivery_attempt: message.delivery_attempt,
};
let message = match message.message {
Some(msg) => msg,
None => break 'read tonic::Status::internal(
"message should be populated by RPC server"
),
};
yield Ok((ack_token, message));
}
continue 'read;
}
}
}
};
let should_retry = retry_op
.get_or_insert_with(|| retry_policy.new_operation())
.check_retry(&(), &error);
match should_retry {
Some(backoff_sleep) => {
backoff_sleep.await;
continue 'reconnect;
}
None => {
error.metadata_mut().insert("subscription", subscription_meta);
yield Err(error);
break 'reconnect;
}
}
}
}
}
#[cfg(test)]
mod test {
use rand::Rng;
use super::*;
#[test]
fn streaming_pull_request_stream() {
let subscription = "test-subscription";
let client_id = "test-id";
let (mut sender, receiver) = futures::channel::mpsc::unbounded();
let requests = create_streaming_pull_request_stream(
subscription.into(),
client_id.into(),
receiver,
StreamSubscriptionConfig {
max_outstanding_messages: 2000,
max_outstanding_bytes: 3000,
stream_ack_deadline: Duration::from_secs(20),
..Default::default()
},
);
pin_mut!(requests);
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
assert_eq!(
Poll::Ready(Some(api::StreamingPullRequest {
subscription: subscription.into(),
ack_ids: vec![],
modify_deadline_seconds: vec![],
modify_deadline_ack_ids: vec![],
stream_ack_deadline_seconds: 20,
client_id: client_id.into(),
max_outstanding_messages: 2000,
max_outstanding_bytes: 3000,
})),
requests.as_mut().poll_next(&mut cx)
);
assert_eq!(Poll::Pending, requests.as_mut().poll_next(&mut cx));
assert_eq!(
Ok(()),
sender.unbounded_send(UserAck::Ack { id: "1st".into() })
);
assert_eq!(
Poll::Ready(Some(api::StreamingPullRequest {
subscription: String::new(),
ack_ids: vec!["1st".into()],
modify_deadline_seconds: vec![],
modify_deadline_ack_ids: vec![],
stream_ack_deadline_seconds: 20,
client_id: String::new(),
max_outstanding_messages: 0,
max_outstanding_bytes: 0,
})),
requests.as_mut().poll_next(&mut cx)
);
assert_eq!(Poll::Pending, requests.as_mut().poll_next(&mut cx));
assert_eq!(
Ok(()),
sender.unbounded_send(UserAck::Ack { id: "2nd".into() })
);
assert_eq!(
Ok(()),
sender.unbounded_send(UserAck::Modify {
id: "3rd".into(),
seconds: 13
})
);
assert_eq!(
Ok(()),
sender.unbounded_send(UserAck::Ack { id: "4th".into() })
);
assert_eq!(
Ok(()),
sender.unbounded_send(UserAck::Modify {
id: "5th".into(),
seconds: 15
})
);
assert_eq!(
Poll::Ready(Some(api::StreamingPullRequest {
subscription: String::new(),
ack_ids: vec!["2nd".into(), "4th".into()],
modify_deadline_seconds: vec![13, 15],
modify_deadline_ack_ids: vec!["3rd".into(), "5th".into()],
stream_ack_deadline_seconds: 20,
client_id: String::new(),
max_outstanding_messages: 0,
max_outstanding_bytes: 0,
})),
requests.as_mut().poll_next(&mut cx)
);
assert_eq!(Poll::Pending, requests.as_mut().poll_next(&mut cx));
let inputs = std::iter::repeat_with(|| rand::thread_rng().gen::<bool>())
.enumerate()
.skip(6) .map(|(index, is_modify)| {
let id = index.to_string() + "th";
if is_modify {
UserAck::Modify {
id,
seconds: index as i32,
}
} else {
UserAck::Ack { id }
}
})
.take(1001)
.collect::<Vec<_>>();
let filter_acks = |acks: &[UserAck]| {
acks.iter()
.filter_map(|ack| match ack {
UserAck::Ack { id } => Some(id.clone()),
_ => None,
})
.collect::<Vec<_>>()
};
let filter_modifies = |acks: &[UserAck]| {
acks.iter()
.filter_map(|ack| match ack {
UserAck::Modify { id, seconds } => Some((id.clone(), *seconds)),
_ => None,
})
.collect::<Vec<_>>()
};
let expected_first_batch = api::StreamingPullRequest {
subscription: String::new(),
ack_ids: filter_acks(&inputs[..1000]),
modify_deadline_seconds: filter_modifies(&inputs[..1000])
.into_iter()
.map(|tup| tup.1)
.collect(),
modify_deadline_ack_ids: filter_modifies(&inputs[..1000])
.into_iter()
.map(|tup| tup.0)
.collect(),
stream_ack_deadline_seconds: 20,
client_id: String::new(),
max_outstanding_messages: 0,
max_outstanding_bytes: 0,
};
let expected_second_batch = api::StreamingPullRequest {
subscription: String::new(),
ack_ids: filter_acks(&inputs[1000..]),
modify_deadline_seconds: filter_modifies(&inputs[1000..])
.into_iter()
.map(|tup| tup.1)
.collect(),
modify_deadline_ack_ids: filter_modifies(&inputs[1000..])
.into_iter()
.map(|tup| tup.0)
.collect(),
stream_ack_deadline_seconds: 20,
client_id: String::new(),
max_outstanding_messages: 0,
max_outstanding_bytes: 0,
};
assert_eq!(
Ok(()),
inputs
.into_iter()
.try_for_each(|ack| sender.unbounded_send(ack))
);
assert_eq!(
Poll::Ready(Some(expected_first_batch)),
requests.as_mut().poll_next(&mut cx)
);
assert_eq!(
Poll::Ready(Some(expected_second_batch)),
requests.as_mut().poll_next(&mut cx)
);
assert_eq!(Poll::Pending, requests.as_mut().poll_next(&mut cx));
sender.disconnect();
assert_eq!(Poll::Ready(None), requests.as_mut().poll_next(&mut cx));
}
}