use super::options::BatchingOptions;
use crate::generated::gapic_dataplane::client::Publisher as GapicPublisher;
use futures::StreamExt as _;
use futures::stream::FuturesUnordered;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::{mpsc, oneshot};
const MAX_DELAY: Duration = Duration::from_secs(60 * 60 * 24); const MAX_MESSAGES: u32 = 1000;
const MAX_BYTES: u32 = 1e7 as u32;
#[derive(Debug)]
pub struct Publisher {
#[allow(dead_code)]
pub(crate) batching_options: BatchingOptions,
tx: UnboundedSender<ToWorker>,
}
impl Publisher {
pub fn publish(&self, msg: crate::model::PubsubMessage) -> crate::model_ext::PublishHandle {
let (tx, rx) = tokio::sync::oneshot::channel();
if self
.tx
.send(ToWorker::Publish(BundledMessage { msg, tx }))
.is_err()
{
}
crate::model_ext::PublishHandle { rx }
}
pub async fn flush(&self) {
let (tx, rx) = oneshot::channel();
if self.tx.send(ToWorker::Flush(tx)).is_err() {
}
rx.await
.expect("the client library should not release the sender");
}
}
#[derive(Clone, Debug)]
pub struct PublisherBuilder {
pub(crate) inner: GapicPublisher,
topic: String,
batching_options: BatchingOptions,
}
impl PublisherBuilder {
pub(crate) fn new(client: GapicPublisher, topic: String) -> Self {
Self {
inner: client,
topic,
batching_options: BatchingOptions::default(),
}
}
pub fn set_message_count_threshold(mut self, threshold: u32) -> PublisherBuilder {
self.batching_options = self.batching_options.set_message_count_threshold(threshold);
self
}
pub fn set_delay_threshold(mut self, threshold: Duration) -> PublisherBuilder {
self.batching_options = self.batching_options.set_delay_threshold(threshold);
self
}
pub fn build(self) -> Publisher {
let batching_options = BatchingOptions::new()
.set_delay_threshold(
self.batching_options
.delay_threshold
.clamp(Duration::ZERO, MAX_DELAY),
)
.set_message_count_threshold(
self.batching_options
.message_count_threshold
.clamp(0, MAX_MESSAGES),
)
.set_byte_threshold(self.batching_options.byte_threshold.clamp(0, MAX_BYTES));
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let worker = Worker::new(self.topic, self.inner, batching_options.clone(), rx);
tokio::spawn(worker.run());
Publisher {
batching_options,
tx,
}
}
}
enum ToWorker {
Publish(BundledMessage),
Flush(oneshot::Sender<()>),
}
#[derive(Debug)]
struct BundledMessage {
pub msg: crate::model::PubsubMessage,
pub tx: oneshot::Sender<crate::Result<String>>,
}
#[derive(Debug)]
struct Worker {
topic_name: String,
client: GapicPublisher,
#[allow(dead_code)]
batching_options: BatchingOptions,
rx: mpsc::UnboundedReceiver<ToWorker>,
}
impl Worker {
fn new(
topic_name: String,
client: GapicPublisher,
batching_options: BatchingOptions,
rx: mpsc::UnboundedReceiver<ToWorker>,
) -> Self {
Self {
topic_name,
client,
rx,
batching_options,
}
}
async fn run(mut self) {
let mut batch = Batch::new();
let delay = self.batching_options.delay_threshold;
let message_limit = self.batching_options.message_count_threshold;
let mut inflight = FuturesUnordered::new();
let timer = tokio::time::sleep(delay);
tokio::pin!(timer);
loop {
tokio::select! {
_ = inflight.next(), if !inflight.is_empty() => {},
_ = &mut timer, if !batch.is_empty() => {
batch.flush(self.client.clone(), self.topic_name.clone(), &mut inflight);
}
msg = self.rx.recv() => {
match msg {
Some(ToWorker::Publish(msg)) => {
if batch.is_empty() {
timer.as_mut().reset(tokio::time::Instant::now() + delay);
}
batch.push(msg);
if batch.len() as u32 >= message_limit {
batch.flush(self.client.clone(), self.topic_name.clone(), &mut inflight);
}
},
Some(ToWorker::Flush(tx)) => {
batch.flush(self.client.clone(), self.topic_name.clone(), &mut inflight);
let mut flushing = std::mem::take(&mut inflight);
while flushing.next().await.is_some() {}
let _ = tx.send(());
},
None => {
batch.flush(self.client.clone(), self.topic_name.clone(), &mut inflight);
break;
}
}
}
}
}
}
}
#[derive(Debug)]
struct Batch {
messages: Vec<BundledMessage>,
}
impl Default for Batch {
fn default() -> Self {
Self::new()
}
}
impl Batch {
fn new() -> Self {
Batch {
messages: Vec::new(),
}
}
fn is_empty(&self) -> bool {
self.messages.is_empty()
}
fn len(&self) -> usize {
self.messages.len()
}
fn push(&mut self, msg: BundledMessage) {
self.messages.push(msg);
}
fn flush(
&mut self,
client: GapicPublisher,
topic: String,
inflight: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
) {
if self.is_empty() {
return;
}
let batch_to_send = Self {
messages: self.messages.drain(..).collect(),
};
inflight.push(tokio::spawn(batch_to_send.send(client, topic)));
}
async fn send(self, client: GapicPublisher, topic: String) {
let (msgs, txs): (Vec<_>, Vec<_>) = self
.messages
.into_iter()
.map(|msg| (msg.msg, msg.tx))
.unzip();
let request = client.publish().set_topic(topic).set_messages(msgs);
match request.send().await {
Err(e) => {
let e = Arc::new(e);
for tx in txs {
let _ = tx.send(Err(gax::error::Error::io(e.clone())));
}
}
Ok(result) => {
txs.into_iter()
.zip(result.message_ids.into_iter())
.for_each(|(tx, result)| {
let _ = tx.send(Ok(result));
});
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{client::PublisherFactory, publisher::options::BatchingOptions};
use crate::{
generated::gapic_dataplane::client::Publisher as GapicPublisher,
model::{PublishResponse, PubsubMessage},
};
mockall::mock! {
#[derive(Debug)]
GapicPublisher {}
impl crate::generated::gapic_dataplane::stub::Publisher for GapicPublisher {
async fn publish(&self, req: crate::model::PublishRequest, _options: gax::options::RequestOptions) -> gax::Result<gax::response::Response<crate::model::PublishResponse>>;
}
}
#[tokio::test]
async fn test_worker_success() {
let mut mock = MockGapicPublisher::new();
mock.expect_publish()
.returning({
|r, _| {
assert_eq!(r.topic, "my-topic");
assert_eq!(r.messages.len(), 1);
let id = String::from_utf8(r.messages[0].data.to_vec()).unwrap();
Ok(gax::response::Response::from(
PublishResponse::new().set_message_ids(vec![id]),
))
}
})
.times(2);
let client = GapicPublisher::from_stub(mock);
let publisher = PublisherBuilder::new(client, "my-topic".to_string())
.set_message_count_threshold(1_u32)
.build();
let messages = vec![
PubsubMessage::new().set_data("hello".to_string()),
PubsubMessage::new().set_data("world".to_string()),
];
let mut handles = Vec::new();
for msg in messages {
let handle = publisher.publish(msg.clone());
handles.push((msg, handle));
}
for (id, rx) in handles.into_iter() {
let got = rx.await.expect("expected message id");
let id = String::from_utf8(id.data.to_vec()).unwrap();
assert_eq!(got, id);
}
}
#[tokio::test(start_paused = true)]
async fn test_drop_publisher() {
let mut mock = MockGapicPublisher::new();
mock.expect_publish().return_once({
|r, _| {
assert_eq!(r.topic, "my-topic");
let ids = r
.messages
.iter()
.map(|m| String::from_utf8(m.data.to_vec()).unwrap());
Ok(gax::response::Response::from(
PublishResponse::new().set_message_ids(ids),
))
}
});
let client = GapicPublisher::from_stub(mock);
let publisher = PublisherBuilder::new(client, "my-topic".to_string())
.set_message_count_threshold(1000_u32)
.set_delay_threshold(Duration::from_secs(60))
.build();
let start = tokio::time::Instant::now();
let messages = vec![
PubsubMessage::new().set_data("hello".to_string()),
PubsubMessage::new().set_data("world".to_string()),
];
let mut handles = Vec::new();
for msg in messages {
let handle = publisher.publish(msg.clone());
handles.push((msg, handle));
}
drop(publisher);
for (id, rx) in handles.into_iter() {
let got = rx.await.expect("expected message id");
let id = String::from_utf8(id.data.to_vec()).unwrap();
assert_eq!(got, id);
assert_eq!(start.elapsed(), Duration::ZERO);
}
}
#[tokio::test]
async fn test_worker_error() {
let mut mock = MockGapicPublisher::new();
mock.expect_publish()
.returning({
|r, _| {
assert_eq!(r.topic, "my-topic");
assert_eq!(r.messages.len(), 1);
Err(gax::error::Error::io("io error"))
}
})
.times(2);
let client = GapicPublisher::from_stub(mock);
let publisher = PublisherBuilder::new(client, "my-topic".to_string())
.set_message_count_threshold(1_u32)
.build();
let messages = vec![
PubsubMessage::new().set_data("hello".to_string()),
PubsubMessage::new().set_data("world".to_string()),
];
let mut handles = Vec::new();
for msg in messages {
let handle = publisher.publish(msg.clone());
handles.push(handle);
}
for rx in handles.into_iter() {
let got = rx.await;
assert!(got.is_err());
}
}
#[tokio::test(start_paused = true)]
async fn test_worker_flush() {
let mut mock = MockGapicPublisher::new();
mock.expect_publish().returning({
|r, _| {
assert_eq!(r.topic, "my-topic");
let ids = r
.messages
.iter()
.map(|m| String::from_utf8(m.data.to_vec()).unwrap());
Ok(gax::response::Response::from(
PublishResponse::new().set_message_ids(ids),
))
}
});
let client = GapicPublisher::from_stub(mock);
let publisher = PublisherBuilder::new(client, "my-topic".to_string())
.set_message_count_threshold(1000_u32)
.set_delay_threshold(Duration::from_secs(60))
.build();
let start = tokio::time::Instant::now();
let messages = vec![
PubsubMessage::new().set_data("hello".to_string()),
PubsubMessage::new().set_data("world".to_string()),
];
let mut handles = Vec::new();
for msg in messages {
let handle = publisher.publish(msg.clone());
handles.push((msg, handle));
}
publisher.flush().await;
assert_eq!(start.elapsed(), Duration::ZERO);
let post = publisher.publish(PubsubMessage::new().set_data("after".to_string()));
for (id, rx) in handles.into_iter() {
let got = rx.await.expect("expected message id");
let id = String::from_utf8(id.data.to_vec()).unwrap();
assert_eq!(got, id);
assert_eq!(start.elapsed(), Duration::ZERO);
}
let got = post.await.expect("expected message id");
assert_eq!(got, "after");
assert_eq!(start.elapsed(), Duration::from_secs(60));
}
#[tokio::test(start_paused = true)]
async fn test_worker_drop_handles() {
let mut mock = MockGapicPublisher::new();
mock.expect_publish().return_once({
move |r, _| {
assert_eq!(r.topic, "my-topic");
let ids = r
.messages
.iter()
.map(|m| String::from_utf8(m.data.to_vec()).unwrap());
assert_eq!(ids.len(), 2);
let ids = ids.collect::<Vec<_>>();
assert_eq!(ids.clone(), vec!["hello", "world"]);
Ok(gax::response::Response::from(
PublishResponse::new().set_message_ids(ids),
))
}
});
let client = GapicPublisher::from_stub(mock);
let publisher = PublisherBuilder::new(client, "my-topic".to_string())
.set_message_count_threshold(1000_u32)
.set_delay_threshold(Duration::from_secs(60))
.build();
let start = tokio::time::Instant::now();
let messages = vec![
PubsubMessage::new().set_data("hello".to_string()),
PubsubMessage::new().set_data("world".to_string()),
];
for msg in messages {
publisher.publish(msg.clone());
}
publisher.flush().await;
assert_eq!(start.elapsed(), Duration::ZERO);
}
#[tokio::test(start_paused = true)]
async fn test_empty_flush() {
let mock = MockGapicPublisher::new();
let client = GapicPublisher::from_stub(mock);
let publisher = PublisherBuilder::new(client, "my-topic".to_string()).build();
let start = tokio::time::Instant::now();
publisher.flush().await;
assert_eq!(start.elapsed(), Duration::ZERO);
}
#[tokio::test]
async fn test_batching_message_count_success() {
let mut mock = MockGapicPublisher::new();
mock.expect_publish().return_once({
|r, _| {
assert_eq!(r.topic, "my-topic");
assert_eq!(r.messages.len(), 2);
let ids = r
.messages
.iter()
.map(|m| String::from_utf8(m.data.to_vec()).unwrap());
Ok(gax::response::Response::from(
PublishResponse::new().set_message_ids(ids),
))
}
});
let client = GapicPublisher::from_stub(mock);
let publisher = PublisherBuilder::new(client, "my-topic".to_string())
.set_message_count_threshold(2_u32)
.set_delay_threshold(std::time::Duration::MAX)
.build();
let messages = vec![
PubsubMessage::new().set_data("hello".to_string()),
PubsubMessage::new().set_data("world".to_string()),
];
let mut handles = Vec::new();
for msg in messages {
let handle = publisher.publish(msg.clone());
handles.push((msg, handle));
}
for (id, rx) in handles.into_iter() {
let got = rx.await.expect("expected message id");
let id = String::from_utf8(id.data.to_vec()).unwrap();
assert_eq!(got, id);
}
}
#[tokio::test]
async fn test_batching_message_count_error() {
let mut mock = MockGapicPublisher::new();
mock.expect_publish().return_once({
|r, _| {
assert_eq!(r.topic, "my-topic");
assert_eq!(r.messages.len(), 2);
Err(gax::error::Error::io("io error"))
}
});
let client = GapicPublisher::from_stub(mock);
let publisher = PublisherBuilder::new(client, "my-topic".to_string())
.set_message_count_threshold(2_u32)
.set_delay_threshold(std::time::Duration::MAX)
.build();
let messages = vec![
PubsubMessage::new().set_data("hello".to_string()),
PubsubMessage::new().set_data("world".to_string()),
];
let mut handles = Vec::new();
for msg in messages {
let handle = publisher.publish(msg.clone());
handles.push(handle);
}
for rx in handles.into_iter() {
let got = rx.await;
assert!(got.is_err());
}
}
#[tokio::test(start_paused = true)]
async fn test_batching_messages_send_on_timeout() {
let mut mock = MockGapicPublisher::new();
mock.expect_publish().returning({
|r, _| {
assert_eq!(r.topic, "my-topic");
let ids = r
.messages
.iter()
.map(|m| String::from_utf8(m.data.to_vec()).unwrap());
Ok(gax::response::Response::from(
PublishResponse::new().set_message_ids(ids),
))
}
});
let client = GapicPublisher::from_stub(mock);
let delay = std::time::Duration::from_millis(10);
let publisher = PublisherBuilder::new(client, "my-topic".to_string())
.set_message_count_threshold(u32::MAX)
.set_delay_threshold(delay)
.build();
for _ in 0..3 {
let start = tokio::time::Instant::now();
let messages = vec![
PubsubMessage::new().set_data("hello".to_string()),
PubsubMessage::new().set_data("world".to_string()),
];
let mut handles = Vec::new();
for msg in messages {
let handle = publisher.publish(msg.clone());
handles.push((msg, handle));
}
for (id, rx) in handles.into_iter() {
let got = rx.await.expect("expected message id");
let id = String::from_utf8(id.data.to_vec()).unwrap();
assert_eq!(got, id);
assert_eq!(
start.elapsed(),
delay,
"batch of messages should have sent after {:?}",
delay
)
}
}
}
#[tokio::test]
async fn builder() -> anyhow::Result<()> {
let factory = PublisherFactory::builder().build().await?;
let builder = factory.publisher("projects/my-project/topics/my-topic".to_string());
let publisher = builder.set_message_count_threshold(1_u32).build();
assert_eq!(publisher.batching_options.message_count_threshold, 1_u32);
Ok(())
}
#[tokio::test]
async fn default_batching() -> anyhow::Result<()> {
let client = PublisherFactory::builder().build().await?;
let publisher = client
.publisher("projects/my-project/topics/my-topic".to_string())
.build();
assert_eq!(
publisher.batching_options.message_count_threshold,
BatchingOptions::default().message_count_threshold
);
assert_eq!(
publisher.batching_options.byte_threshold,
BatchingOptions::default().byte_threshold
);
assert_eq!(
publisher.batching_options.delay_threshold,
BatchingOptions::default().delay_threshold
);
Ok(())
}
#[tokio::test]
async fn test_builder_clamping() -> anyhow::Result<()> {
let oversized_options = BatchingOptions::new()
.set_delay_threshold(MAX_DELAY + Duration::from_secs(1))
.set_message_count_threshold(MAX_MESSAGES + 1)
.set_byte_threshold(MAX_BYTES + 1);
let client = PublisherFactory::builder().build().await?;
let publisher = client
.publisher("projects/my-project/topics/my-topic".to_string())
.set_delay_threshold(oversized_options.delay_threshold)
.set_message_count_threshold(oversized_options.message_count_threshold)
.build();
let got = publisher.batching_options;
assert_eq!(got.delay_threshold, MAX_DELAY);
assert_eq!(got.message_count_threshold, MAX_MESSAGES);
let normal_options = BatchingOptions::new()
.set_delay_threshold(Duration::from_secs(10))
.set_message_count_threshold(10_u32);
let publisher = client
.publisher("projects/my-project/topics/my-topic".to_string())
.set_delay_threshold(normal_options.delay_threshold)
.set_message_count_threshold(normal_options.message_count_threshold)
.build();
let got = publisher.batching_options;
assert_eq!(got.delay_threshold, normal_options.delay_threshold);
assert_eq!(
got.message_count_threshold,
normal_options.message_count_threshold
);
Ok(())
}
fn create_bundled_message_helper(
data: String,
) -> (
BundledMessage,
tokio::sync::oneshot::Receiver<crate::Result<String>>,
) {
let (tx, rx) = tokio::sync::oneshot::channel();
(
BundledMessage {
tx,
msg: PubsubMessage::new().set_data(data),
},
rx,
)
}
#[tokio::test]
async fn test_push_batch() {
let mut batch = Batch::new();
assert!(batch.is_empty());
let (message_a, _rx_a) = create_bundled_message_helper("hello".to_string());
batch.push(message_a);
assert_eq!(batch.len(), 1);
let (message_b, _rx_b) = create_bundled_message_helper(", ".to_string());
batch.push(message_b);
assert_eq!(batch.len(), 2);
let (message_c, _rx_c) = create_bundled_message_helper("world".to_string());
batch.push(message_c);
assert_eq!(batch.len(), 3);
}
}