use std::{
convert::{TryFrom, TryInto},
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},
grpc::StatusCodeSet,
pubsub::api,
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,
@default(2000, "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>,
}
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(())
}
}
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<StatusCodeSet>> {
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(
crate::pubsub::DEFAULT_RETRY_CODES | tonic::Code::Unavailable.into(),
exponential_backoff::Config::default(),
),
},
_p: std::marker::PhantomPinned,
}
}
}
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,
{
assert!(
config.ack_channel_capacity
>= config
.max_outstanding_messages
.try_into()
.expect("should be a valid usize")
);
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(config.ack_channel_capacity);
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 message_stream = client
.streaming_pull(request_stream)
.await
.map_err(|mut err| {
err.metadata_mut()
.insert("subscription", subscription_meta.clone());
err
})?
.into_inner();
while let Some(response_result) = message_stream.next().await {
match response_result {
Ok(response) => {
retry_op = None;
for message in response.received_messages {
let ack_token = AcknowledgeToken {
id: message.ack_id,
channel: sender.clone()
};
let message = message
.message
.expect("message should be populated by RPC server");
yield Ok((ack_token, message));
}
}
Err(mut err) => {
let should_retry = retry_op
.get_or_insert_with(|| retry_policy.new_operation())
.check_retry(&(), &err);
match should_retry {
Some(backoff_sleep) => {
backoff_sleep.await;
continue 'reconnect;
}
None => {
err.metadata_mut().insert("subscription", subscription_meta);
yield Err(err);
break;
}
}
}
}
};
break;
}
}
}
#[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,
ack_channel_capacity: 4000,
stream_ack_deadline: Duration::from_secs(20),
},
);
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));
}
}