#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::fmt;
use std::future::{Future, ready};
use std::sync::Arc;
use bytes::BytesMut;
use google_cloud_pubsub::client::Publisher as GcpPublisher;
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::{Binding, Bindings};
use ruststream::runtime::{PublishBuilder, PublishSink};
use ruststream::{HeaderMap, OutgoingMessage, PairError, PublishPolicy, Publisher, Take};
#[cfg(feature = "asyncapi")]
use serde::Serialize;
use tokio::runtime::Handle;
use crate::broker::{ConnectedPubSubBroker, Core, CoreCell, Link};
use crate::error::{PubSubError, box_err};
use crate::message::{PARTITION_KEY_HEADER, header_text, to_gcp_message};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PubSubPublishOptions {
pub ordering_key: Option<String>,
}
#[derive(Clone)]
pub struct PubSubPublisher {
cell: CoreCell,
default_ordering_key: Option<Arc<str>>,
}
impl fmt::Debug for PubSubPublisher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PubSubPublisher")
.field("default_ordering_key", &self.default_ordering_key)
.finish_non_exhaustive()
}
}
impl PubSubPublisher {
pub(crate) fn new(cell: CoreCell) -> Self {
Self {
cell,
default_ordering_key: None,
}
}
pub(crate) fn with_default_ordering_key(mut self, key: Option<Arc<str>>) -> Self {
self.default_ordering_key = key;
self
}
fn link(&self) -> Result<&Link, PubSubError> {
self.cell.get().ok_or(PubSubError::NotConnected)
}
async fn publisher_for(&self, core: &Core, topic: &str) -> GcpPublisher {
let name = core.topic_name(topic);
let mut publishers = core.publishers.lock().await;
if let Some(publisher) = publishers.get(&name) {
return publisher.clone();
}
let publisher = {
let connect_runtime = core.runtime_slot().runtime();
let _entered = connect_runtime.as_ref().map(Handle::enter);
core.base_publisher.publisher(name.clone()).build()
};
publishers.insert(name, publisher.clone());
publisher
}
}
pub(crate) fn resolve_ordering_key<'a>(
headers: &'a HeaderMap,
options: Option<&'a PubSubPublishOptions>,
policy: Option<&'a str>,
) -> Result<Option<&'a str>, String> {
if let Some(key) = options.and_then(|options| options.ordering_key.as_deref()) {
return Ok(Some(key));
}
if let Some(value) = headers.get(PARTITION_KEY_HEADER) {
return header_text(PARTITION_KEY_HEADER, value).map(Some);
}
Ok(policy)
}
impl Publisher for PubSubPublisher {
type Payload = Take;
type Error = PubSubError;
type Options = PubSubPublishOptions;
async fn publish(
&self,
msg: OutgoingMessage<'_, BytesMut>,
options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
let core = match self.link()? {
Link::Service(core) => core,
#[cfg(feature = "testing")]
Link::InProcess(project) => {
let (name, payload, headers) = msg.into_parts();
let refused = |reason: String| PubSubError::Publish {
topic: project.topic_path(name),
source: reason.into(),
};
let key =
resolve_ordering_key(&headers, options, self.default_ordering_key.as_deref())
.map_err(refused)?;
let message = to_gcp_message(payload, &headers, key).map_err(refused)?;
return project.publish(name, &message);
}
};
core.ensure_open()?;
let name = msg.name();
let (_, payload, headers) = msg.into_parts();
let refused = |reason: String| PubSubError::Publish {
topic: core.topic_name(name),
source: reason.into(),
};
let key = resolve_ordering_key(&headers, options, self.default_ordering_key.as_deref())
.map_err(refused)?;
let message = to_gcp_message(payload, &headers, key).map_err(refused)?;
let publisher = self.publisher_for(core, name).await;
match publisher.publish(message).await {
Ok(_message_id) => Ok(()),
Err(err) => {
if let Some(key) = key {
publisher.resume_publish(key.to_owned());
}
Err(PubSubError::Publish {
topic: core.topic_name(name),
source: box_err(err),
})
}
}
}
}
pub trait PubSubOrdering {
#[must_use]
fn ordering_key(self, key: impl Into<String>) -> Self;
}
impl<Sink, Body, Enc, Hdrs, Dest> PubSubOrdering for PublishBuilder<Sink, Body, Enc, Hdrs, Dest>
where
Sink: PublishSink<Options = PubSubPublishOptions>,
{
fn ordering_key(mut self, key: impl Into<String>) -> Self {
self.options_mut()
.get_or_insert_with(PubSubPublishOptions::default)
.ordering_key = Some(key.into());
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[must_use]
pub struct PubSubPublish {
ordering_key: Option<String>,
}
impl PubSubPublish {
pub fn ordering_key(mut self, key: impl Into<String>) -> Self {
self.ordering_key = Some(key.into());
self
}
pub(crate) fn default_key(&self) -> Option<Arc<str>> {
self.ordering_key.as_deref().map(Arc::from)
}
#[cfg(feature = "asyncapi")]
pub(crate) fn message_binding(&self) -> Bindings {
let Some(ordering_key) = self.ordering_key.as_deref() else {
return Bindings::new();
};
let body = PubSubMessageBinding { ordering_key };
Binding::new("googlepubsub", BINDING_VERSION, &body)
.map_or_else(|_| Bindings::new(), |binding| Bindings::new().with(binding))
}
}
impl PublishPolicy<ConnectedPubSubBroker> for PubSubPublish {
type Live = PubSubPublisher;
fn pair(
self,
connected: &ConnectedPubSubBroker,
) -> impl Future<Output = Result<Self::Live, PairError>> {
let key = self.default_key();
ready(Ok(connected.publisher().with_default_ordering_key(key)))
}
#[cfg(feature = "asyncapi")]
fn message_bindings(&self, _channel: &str) -> Bindings {
self.message_binding()
}
}
#[cfg(feature = "asyncapi")]
const BINDING_VERSION: &str = "0.2.0";
#[cfg(feature = "asyncapi")]
#[derive(Serialize)]
struct PubSubMessageBinding<'a> {
#[serde(rename = "orderingKey")]
ordering_key: &'a str,
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use ruststream::HeaderMap;
use super::*;
fn keyed_header(value: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(PARTITION_KEY_HEADER, value.to_owned());
headers
}
#[test]
fn the_messages_own_setting_wins_over_the_header_and_the_policy() {
let options = PubSubPublishOptions {
ordering_key: Some("step".to_owned()),
};
let headers = keyed_header("header");
let key = resolve_ordering_key(&headers, Some(&options), Some("policy"));
assert_eq!(key, Ok(Some("step")));
}
#[test]
fn the_partition_key_header_orders_a_publish() {
let headers = keyed_header("header");
let key = resolve_ordering_key(&headers, None, Some("policy"));
assert_eq!(key, Ok(Some("header")));
}
#[test]
fn a_publish_that_names_nothing_takes_the_policys_key() {
let headers = HeaderMap::new();
let key = resolve_ordering_key(&headers, None, Some("policy"));
assert_eq!(key, Ok(Some("policy")));
}
#[test]
fn a_publish_with_no_key_anywhere_is_unordered() {
let headers = HeaderMap::new();
assert_eq!(resolve_ordering_key(&headers, None, None), Ok(None));
}
#[test]
fn a_partition_key_header_that_is_not_text_is_refused() {
let mut headers = HeaderMap::new();
headers.insert(PARTITION_KEY_HEADER, Bytes::from_static(&[0x80, 0xff]));
let refused = resolve_ordering_key(&headers, None, Some("policy"))
.expect_err("a non-UTF-8 key has no ordering-key form");
assert!(refused.contains(PARTITION_KEY_HEADER), "{refused}");
}
}