use super::{api, ProjectTopicName, PubSubRetryCheck};
use crate::{
auth::grpc::{AuthGrpcService, OAuthTokenSource},
retry_policy::{exponential_backoff, ExponentialBackoff, RetryOperation, RetryPolicy},
};
use futures::{future::BoxFuture, ready, stream, Sink, SinkExt, TryFutureExt};
use pin_project::pin_project;
use prost::Message;
use std::{
cmp::Ordering,
convert::Infallible,
error::Error as StdError,
fmt,
future::Future,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
const MB: usize = 1000 * 1000;
const MAX_ATTR_PER_MESSAGE: usize = 100;
const MAX_ATTR_KEY_BYTES: usize = 256;
const MAX_ATTR_VALUE_BYTES: usize = 1024;
const MAX_MESSAGES_PER_PUBLISH: usize = 1000;
const MAX_DATA_FIELD_BYTES: usize = 10 * MB;
const MAX_PUBLISH_REQUEST_BYTES: usize = 10 * MB;
type ApiPublisherClient<C> = api::publisher_client::PublisherClient<
AuthGrpcService<tonic::transport::Channel, OAuthTokenSource<C>>,
>;
type Drain = futures::sink::Drain<api::PubsubMessage>;
type FlushOutput<Si, E> = (Si, Result<(), SinkError<E>>);
#[derive(Debug, thiserror::Error)]
#[error("failed to publish message(s)")]
pub struct PublishError {
#[source]
pub source: tonic::Status,
pub messages: Vec<api::PubsubMessage>,
}
impl From<PublishError> for tonic::Status {
fn from(err: PublishError) -> Self {
err.source
}
}
impl<E> From<PublishError> for SinkError<E> {
fn from(err: PublishError) -> Self {
SinkError::Publish(err)
}
}
#[derive(Debug)]
pub enum SinkError<E = Infallible> {
Publish(PublishError),
Response(E),
}
impl<E: fmt::Display> fmt::Display for SinkError<E> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SinkError::Publish(err) => fmt::Display::fmt(&err, f),
SinkError::Response(err) => fmt::Display::fmt(&err, f),
}
}
}
impl<E: StdError + 'static> StdError for SinkError<E> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
SinkError::Publish(err) => Some(err as &_),
SinkError::Response(err) => Some(err as &_),
}
}
}
impl From<SinkError<Infallible>> for tonic::Status {
fn from(err: SinkError<Infallible>) -> Self {
match err {
SinkError::Publish(err) => tonic::Status::from(err),
SinkError::Response(err) => {
let _: Infallible = err;
unreachable!()
}
}
}
}
config_default! {
#[derive(Debug, Clone, Copy, Eq, PartialEq, serde::Deserialize)]
#[non_exhaustive]
pub struct PublishConfig {
#[serde(with = "humantime_serde")]
@default(Duration::from_secs(5), "PublishConfig::default_timeout")
pub timeout: Duration,
}
}
#[pin_project(project=PublishTopicSinkProjection)]
pub struct PublishTopicSink<
C = crate::DefaultConnector,
Retry = ExponentialBackoff<PubSubRetryCheck>,
ResponseSink: Sink<api::PubsubMessage> = Drain,
> {
client: ApiPublisherClient<C>,
buffer: PublishBuffer,
#[pin]
flush_state: FlushState<ResponseSink>,
retry_policy: Retry,
config: PublishConfig,
_pin: std::marker::PhantomPinned,
}
#[pin_project(project=FlushStateProjection, project_replace=FlushStateReplace)]
enum FlushState<ResponseSink: Sink<api::PubsubMessage>> {
NotFlushing(ResponseSink),
Transitioning,
Flushing(#[pin] BoxFuture<'static, FlushOutput<ResponseSink, ResponseSink::Error>>),
}
impl<C> PublishTopicSink<C, ExponentialBackoff<PubSubRetryCheck>, Drain> {
pub(super) fn new(
client: ApiPublisherClient<C>,
topic: ProjectTopicName,
config: PublishConfig,
) -> Self
where
C: crate::Connect + Clone + Send + Sync + 'static,
{
PublishTopicSink {
client,
buffer: PublishBuffer::new(String::from(topic)),
flush_state: FlushState::NotFlushing(futures::sink::drain()),
retry_policy: ExponentialBackoff::new(
PubSubRetryCheck::default(),
exponential_backoff::Config::default(),
),
config,
_pin: std::marker::PhantomPinned,
}
}
}
impl<C, Retry, ResponseSink: Sink<api::PubsubMessage>> PublishTopicSink<C, Retry, ResponseSink> {
pub fn with_response_sink<Si>(self, sink: Si) -> PublishTopicSink<C, Retry, Si>
where
Si: Sink<api::PubsubMessage>,
{
PublishTopicSink {
flush_state: FlushState::NotFlushing(sink),
retry_policy: self.retry_policy,
client: self.client,
buffer: self.buffer,
config: self.config,
_pin: self._pin,
}
}
pub fn with_retry_policy<R>(self, retry_policy: R) -> PublishTopicSink<C, R, ResponseSink>
where
R: RetryPolicy<api::PublishRequest, tonic::Status>,
{
PublishTopicSink {
retry_policy,
client: self.client,
buffer: self.buffer,
flush_state: self.flush_state,
config: self.config,
_pin: self._pin,
}
}
}
impl<C, Retry, ResponseSink> Sink<api::PubsubMessage> for PublishTopicSink<C, Retry, ResponseSink>
where
C: crate::Connect + Clone + Send + Sync + 'static,
Retry: RetryPolicy<api::PublishRequest, tonic::Status> + 'static,
Retry::RetryOp: Send + 'static,
<Retry::RetryOp as RetryOperation<api::PublishRequest, tonic::Status>>::Sleep: Send + 'static,
ResponseSink: Sink<api::PubsubMessage> + Unpin + Send + 'static,
{
type Error = SinkError<ResponseSink::Error>;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.project().poll_flush_projected(cx, false)
}
fn start_send(
mut self: Pin<&mut Self>,
message: api::PubsubMessage,
) -> Result<(), Self::Error> {
self.buffer.push(message)?;
Ok(())
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.project().poll_flush_projected(cx, true)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(self.as_mut().poll_flush(cx))?;
match self.project().flush_state.project() {
FlushStateProjection::NotFlushing(response_sink) => response_sink
.poll_close_unpin(cx)
.map_err(SinkError::Response),
_ => unreachable!("just flushed, state should be not flushing"),
}
}
}
impl<'pin, C, Retry, ResponseSink> PublishTopicSinkProjection<'pin, C, Retry, ResponseSink>
where
C: crate::Connect + Clone + Send + Sync + 'static,
Retry: RetryPolicy<api::PublishRequest, tonic::Status> + 'static,
Retry::RetryOp: Send + 'static,
<Retry::RetryOp as RetryOperation<api::PublishRequest, tonic::Status>>::Sleep: Send + 'static,
ResponseSink: Sink<api::PubsubMessage> + Unpin + Send + 'static,
{
fn poll_flush_projected(
&mut self,
cx: &mut Context<'_>,
flush_everything: bool,
) -> Poll<Result<(), SinkError<ResponseSink::Error>>> {
use FlushStateProjection::{Flushing, NotFlushing, Transitioning};
loop {
return match self.flush_state.as_mut().project() {
Transitioning => {
panic!("the sink cannot be used after recovering from a panic")
}
Flushing(mut flush_fut) => {
let (response_sink, flush_result) = ready!(flush_fut.as_mut().poll(cx));
self.flush_state.set(FlushState::NotFlushing(response_sink));
flush_result?;
continue;
}
NotFlushing(_) if self.buffer.is_empty() => Poll::Ready(Ok(())),
NotFlushing(_)
if !flush_everything
&& self.buffer.compare_flush_threshold() == Ordering::Less =>
{
Poll::Ready(Ok(()))
}
NotFlushing(_) => {
let request = self.buffer.swap_buffers()?;
let response_sink = match self
.flush_state
.as_mut()
.project_replace(FlushState::Transitioning)
{
FlushStateReplace::NotFlushing(response_sink) => response_sink,
_ => unreachable!("already checked state"),
};
let flush_fut = Self::flush(
self.client,
request,
response_sink,
self.retry_policy,
self.config.timeout,
);
self.flush_state
.set(FlushState::Flushing(Box::pin(flush_fut)));
continue;
}
};
}
}
fn flush(
client: &mut ApiPublisherClient<C>,
mut request: api::PublishRequest,
mut response_sink: ResponseSink,
retry_policy: &mut Retry,
timeout: Duration,
) -> impl Future<Output = FlushOutput<ResponseSink, ResponseSink::Error>> {
let mut client = client.clone();
let mut retry = retry_policy.new_operation();
async move {
let response = loop {
let mut tonic_request = tonic::Request::new(request.clone());
tonic_request.set_timeout(timeout);
let publish = client.publish(tonic_request);
let publish_fut = tokio::time::timeout(timeout, publish).map_err(
|tokio::time::error::Elapsed { .. }| {
tonic::Status::deadline_exceeded("publish attempt timed out")
},
);
break match publish_fut.await {
Ok(Ok(response)) => Ok(response.into_inner()),
Err(status) | Ok(Err(status)) => match retry.check_retry(&request, &status) {
Some(sleep) => {
sleep.await;
continue;
}
None => Err(status),
},
};
};
let response = match response {
Ok(response) => response,
Err(status) => {
return (
response_sink,
Err(SinkError::Publish(PublishError {
source: status,
messages: request.messages,
})),
);
}
};
if response.message_ids.len() != request.messages.len() {
return (
response_sink,
Err(SinkError::Publish(PublishError {
source: tonic::Status::internal(format!(
"publish response had {} ids when expected {}",
response.message_ids.len(),
request.messages.len()
)),
messages: request.messages,
})),
);
}
for (message, message_id) in request.messages.iter_mut().zip(response.message_ids) {
message.message_id = message_id;
}
let response_result = response_sink
.send_all(&mut stream::iter(request.messages.into_iter().map(Ok)))
.await
.map_err(SinkError::Response);
(response_sink, response_result)
}
}
}
struct PublishBuffer {
request: api::PublishRequest,
encoded_len: usize,
}
impl PublishBuffer {
fn new(topic: String) -> Self {
let request = api::PublishRequest {
topic,
messages: Vec::with_capacity(MAX_MESSAGES_PER_PUBLISH),
};
let buffer = PublishBuffer {
encoded_len: request.encoded_len(),
request,
};
if buffer.compare_flush_threshold() > Ordering::Less {
panic!(
"publish request exceeds size limit with topic alone ({} bytes)",
buffer.encoded_len
);
}
buffer
}
fn is_empty(&self) -> bool {
self.request.messages.is_empty()
}
fn compare_flush_threshold(&self) -> Ordering {
let messages = self.request.messages.len();
let bytes = self.encoded_len;
if messages > MAX_MESSAGES_PER_PUBLISH || bytes > MAX_PUBLISH_REQUEST_BYTES {
Ordering::Greater
} else if messages == MAX_MESSAGES_PER_PUBLISH || bytes == MAX_PUBLISH_REQUEST_BYTES {
Ordering::Equal
} else {
Ordering::Less
}
}
fn push(&mut self, message: api::PubsubMessage) -> Result<(), PublishError> {
macro_rules! check_argument {
($value:expr, $limit:expr, $fmt_string:literal) => {
check_argument!($value, $value, $limit, $fmt_string)
};
($value_str:expr, $value:expr, $limit:expr, $fmt_string:literal) => {{
let value_str = $value_str;
let value = $value;
let limit = $limit;
if value > limit {
return Err(PublishError {
source: tonic::Status::invalid_argument(format!(
$fmt_string,
value_str, limit
)),
messages: vec![message],
});
}
}};
}
check_argument!(
message.attributes.len(),
MAX_ATTR_PER_MESSAGE,
"message has {} attributes, exceeds max {}"
);
check_argument!(
message.data.len(),
MAX_DATA_FIELD_BYTES,
"message has {} data bytes, exceeds max {} bytes"
);
for (key, value) in message.attributes.iter() {
check_argument!(
&key,
key.len(),
MAX_ATTR_KEY_BYTES,
"message attribute key {} exceeds max {} bytes"
);
check_argument!(
&value,
value.len(),
MAX_ATTR_VALUE_BYTES,
"message attribute value {} exceeds max {} bytes"
);
}
self.request.messages.push(message);
self.encoded_len = self.request.encoded_len();
Ok(())
}
fn pop(&mut self) -> Option<api::PubsubMessage> {
let msg = self.request.messages.pop();
self.encoded_len = self.request.encoded_len();
msg
}
fn swap_buffers(&mut self) -> Result<api::PublishRequest, PublishError> {
let mut new_messages = Vec::with_capacity(MAX_MESSAGES_PER_PUBLISH);
if self.compare_flush_threshold() == Ordering::Greater {
let len_before_pop = self.encoded_len;
match self.pop() {
None => {
unreachable!()
}
Some(message) if self.is_empty() => {
return Err(PublishError {
source: tonic::Status::invalid_argument(format!(
"submitted message which exceeds request size limit ({} bytes, limit \
{})",
len_before_pop, MAX_PUBLISH_REQUEST_BYTES,
)),
messages: vec![message],
});
}
Some(message) => {
new_messages.push(message);
assert!(
self.compare_flush_threshold() == Ordering::Less,
"start_send was previously called without a corresponding poll_ready"
);
}
}
}
let new_request = api::PublishRequest {
topic: self.request.topic.clone(),
messages: new_messages,
};
let old_buffer = std::mem::replace(
self,
PublishBuffer {
encoded_len: new_request.encoded_len(),
request: new_request,
},
);
Ok(old_buffer.request)
}
}
#[cfg(test)]
mod test {
use super::*;
#[cfg(feature = "emulators")]
use crate::pubsub;
use futures::{channel::mpsc, SinkExt, StreamExt};
use std::{collections::BTreeMap, iter};
impl<E: PartialEq> PartialEq for SinkError<E> {
fn eq(&self, other: &SinkError<E>) -> bool {
use SinkError::{Publish, Response};
match (self, other) {
(
Publish(PublishError {
source: status1,
messages: messages1,
}),
Publish(PublishError {
source: status2,
messages: messages2,
}),
) => {
status1.code() == status2.code() && messages1 == messages2
}
(Response(err1), Response(err2)) => err1 == err2,
_ => false,
}
}
}
fn dummy_client() -> ApiPublisherClient<crate::DefaultConnector> {
ApiPublisherClient::new(crate::auth::grpc::oauth_grpc(
tonic::transport::channel::Endpoint::from_static("https://localhost").connect_lazy(),
None,
vec![],
))
}
fn dummy_sink() -> PublishTopicSink {
PublishTopicSink::new(
dummy_client(),
ProjectTopicName::new("dummy-project", "dummy-topic"),
PublishConfig::default(),
)
}
#[tokio::test]
async fn too_many_attributes() {
let mut sink = Box::pin(dummy_sink());
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
assert_eq!(Poll::Ready(Ok(())), sink.poll_ready_unpin(&mut cx));
let attributes = (0..(MAX_ATTR_PER_MESSAGE + 1))
.map(|n| (n.to_string(), n.to_string()))
.collect::<BTreeMap<String, String>>();
let message = api::PubsubMessage {
attributes,
..api::PubsubMessage::default()
};
assert_eq!(
Err(SinkError::Publish(PublishError {
source: tonic::Status::invalid_argument(""),
messages: vec![message.clone()]
})),
sink.start_send_unpin(message)
);
}
#[tokio::test]
async fn too_long_attribute_key() {
let mut sink = Box::pin(dummy_sink());
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
assert_eq!(Poll::Ready(Ok(())), sink.poll_ready_unpin(&mut cx));
let attributes = {
let mut map = BTreeMap::new();
map.insert(
iter::repeat('a')
.take(MAX_ATTR_KEY_BYTES + 1)
.collect::<String>(),
"".into(),
);
map
};
let message = api::PubsubMessage {
attributes,
..api::PubsubMessage::default()
};
assert_eq!(
Err(SinkError::Publish(PublishError {
source: tonic::Status::invalid_argument(""),
messages: vec![message.clone()]
})),
sink.start_send_unpin(message)
);
}
#[tokio::test]
async fn too_long_attribute_value() {
let mut sink = Box::pin(dummy_sink());
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
assert_eq!(Poll::Ready(Ok(())), sink.poll_ready_unpin(&mut cx));
let attributes = {
let mut map = BTreeMap::new();
map.insert(
"".into(),
iter::repeat('a')
.take(MAX_ATTR_VALUE_BYTES + 1)
.collect::<String>(),
);
map
};
let message = api::PubsubMessage {
attributes,
..api::PubsubMessage::default()
};
assert_eq!(
Err(SinkError::Publish(PublishError {
source: tonic::Status::invalid_argument(""),
messages: vec![message.clone()]
})),
sink.start_send_unpin(message)
);
}
#[tokio::test]
async fn too_large_message_data() {
let mut sink = Box::pin(dummy_sink());
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
assert_eq!(Poll::Ready(Ok(())), sink.poll_ready_unpin(&mut cx));
let message = api::PubsubMessage {
data: vec![0; MAX_DATA_FIELD_BYTES + 1].into(),
..api::PubsubMessage::default()
};
assert_eq!(
Err(SinkError::Publish(PublishError {
source: tonic::Status::invalid_argument(""),
messages: vec![message.clone()]
})),
sink.start_send_unpin(message)
);
}
#[tokio::test]
async fn too_large_combined_message_size() {
let mut sink = Box::pin(dummy_sink());
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
assert_eq!(Poll::Ready(Ok(())), sink.poll_ready_unpin(&mut cx));
let message = api::PubsubMessage {
data: vec![0; MAX_DATA_FIELD_BYTES - 1].into(),
attributes: iter::once(("a".to_string(), "b".to_string())).collect(),
..api::PubsubMessage::default()
};
assert_eq!(Ok(()), sink.start_send_unpin(message.clone()));
assert_eq!(
Poll::Ready(Err(SinkError::Publish(PublishError {
source: tonic::Status::invalid_argument(""),
messages: vec![message],
}))),
sink.poll_ready_unpin(&mut cx)
);
}
#[tokio::test]
async fn did_not_poll_ready() {
let mut sink = Box::pin(dummy_sink());
let message = api::PubsubMessage {
data: vec![0; MAX_DATA_FIELD_BYTES * 2 / 3].into(),
..api::PubsubMessage::default()
};
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
assert_eq!(Poll::Ready(Ok(())), sink.poll_ready_unpin(&mut cx));
assert_eq!(Ok(()), sink.start_send_unpin(message.clone()));
assert_eq!(Ok(()), sink.start_send_unpin(message.clone()));
assert_eq!(Ok(()), sink.start_send_unpin(message));
assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|| sink.poll_ready_unpin(&mut cx)
))
.is_err());
}
#[test]
#[should_panic]
fn buffer_topic_too_large() {
let too_large_topic = std::iter::repeat('a')
.take(MAX_PUBLISH_REQUEST_BYTES)
.collect::<String>();
PublishBuffer::new(too_large_topic);
}
#[cfg(feature = "emulators")]
#[tokio::test]
async fn message_responses_in_order() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let emulator = pubsub::emulator::EmulatorClient::new().await?;
let project_topic =
ProjectTopicName::new(emulator.project(), "message_responses_in_order_test");
let project_subscription = pubsub::ProjectSubscriptionName::new(
emulator.project(),
"message_responses_in_order_test_subscription",
);
let pubsub_config = pubsub::PubSubConfig {
endpoint: emulator.endpoint(),
..pubsub::PubSubConfig::default()
};
let mut publisher = emulator
.builder()
.build_pubsub_publisher(pubsub_config.clone())
.await?;
publisher
.create_topic(api::Topic {
name: project_topic.clone().into(),
..api::Topic::default()
})
.await?;
let mut subscriber = emulator
.builder()
.build_pubsub_subscriber(pubsub_config)
.await?;
subscriber
.create_subscription(api::Subscription {
name: project_subscription.clone().into(),
topic: project_topic.clone().into(),
..api::Subscription::default()
})
.await?;
let (response_sink, responses) = futures::channel::mpsc::unbounded();
let publish_topic_sink = publisher
.publish_topic_sink(project_topic.clone())
.with_response_sink(response_sink);
futures::stream::iter(0..10)
.map(|n| api::PubsubMessage {
data: vec![n; MAX_PUBLISH_REQUEST_BYTES / 4].into(),
attributes: iter::once(("msg_num".into(), format!("{}", n))).collect(),
..api::PubsubMessage::default()
})
.map(Ok)
.forward(publish_topic_sink)
.await?;
let sent_messages = responses.collect::<Vec<_>>().await;
let mut received_messages = Vec::new();
while received_messages.len() < sent_messages.len() {
received_messages.extend(
subscriber
.pull(api::PullRequest {
subscription: project_subscription.clone().into(),
max_messages: 10,
..api::PullRequest::default()
})
.await?
.into_inner()
.received_messages
.into_iter()
.filter_map(|msg| {
Some(api::PubsubMessage {
publish_time: None,
..msg.message?
})
}),
);
}
assert_eq!(sent_messages, received_messages);
Ok(())
}
#[tokio::test]
async fn user_sink_closed_no_flush() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (user_sink, mut user_sink_receiver) = mpsc::unbounded();
let mut publish_sink = dummy_sink().with_response_sink(user_sink);
assert_eq!(Poll::Pending, futures::poll!(user_sink_receiver.next()));
publish_sink.close().await?;
assert_eq!(Poll::Ready(None), futures::poll!(user_sink_receiver.next()));
Ok(())
}
#[cfg(feature = "emulators")]
#[tokio::test]
async fn user_sink_closed_with_flush() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let emulator = pubsub::emulator::EmulatorClient::new().await?;
let project_topic = ProjectTopicName::new(emulator.project(), "user_sink_test");
let mut publisher = emulator
.builder()
.build_pubsub_publisher(pubsub::PubSubConfig {
endpoint: emulator.endpoint(),
..pubsub::PubSubConfig::default()
})
.await?;
publisher
.create_topic(api::Topic {
name: project_topic.clone().into(),
..api::Topic::default()
})
.await?;
let (user_sink, mut user_sink_receiver) = futures::channel::mpsc::unbounded();
let mut publish_sink = publisher
.publish_topic_sink(project_topic.clone())
.with_response_sink(user_sink);
let message = api::PubsubMessage {
data: "test-data".as_bytes().to_vec().into(),
..api::PubsubMessage::default()
};
publish_sink.feed(message.clone()).await?;
assert_eq!(Poll::Pending, futures::poll!(user_sink_receiver.next()));
publish_sink.close().await?;
assert_eq!(
Poll::Ready(Some(message)),
futures::poll!(user_sink_receiver.next()).map(|msg| Some(api::PubsubMessage {
message_id: "".into(),
..msg?
}))
);
assert_eq!(Poll::Ready(None), futures::poll!(user_sink_receiver.next()));
Ok(())
}
}