#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::collections::HashMap;
use std::future::{Future, ready};
use std::time::Duration;
use bytes::{Bytes, BytesMut};
use google_cloud_pubsub::model::Message as GcpMessage;
use google_cloud_pubsub::subscriber::handler::Handler;
#[cfg(feature = "testing")]
use ruststream::RawMessage;
use ruststream::{AckError, HeaderMap, IncomingMessage, Partitioned, Str};
use tokio::time::sleep;
use crate::error::{PubSubError, box_err};
#[cfg(feature = "testing")]
use crate::in_process::Settlement;
use crate::runtime_slot::RuntimeSlot;
use crate::subscription::DeliveryLimits;
pub const PARTITION_KEY_HEADER: &str = "partition-key";
pub const DELIVERY_ATTEMPT_HEADER: &str = "pubsub-delivery-attempt";
pub struct PubSubMessage {
payload: Bytes,
headers: HeaderMap,
handler: Settle,
limits: DeliveryLimits,
}
enum Settle {
Pull(Handler, RuntimeSlot),
#[cfg(feature = "testing")]
InProcess(Settlement),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<Settle>() == size_of::<(Handler, RuntimeSlot)>());
impl Settle {
fn delivery_attempt(&self) -> Option<i32> {
match self {
Self::Pull(handler, _) => handler.delivery_attempt(),
#[cfg(feature = "testing")]
Self::InProcess(settlement) => settlement.delivery_attempt(),
}
}
}
impl std::fmt::Debug for PubSubMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PubSubMessage")
.field("payload_len", &self.payload.len())
.finish_non_exhaustive()
}
}
impl PubSubMessage {
pub(crate) fn new(
message: GcpMessage,
handler: Handler,
limits: DeliveryLimits,
runtime: RuntimeSlot,
) -> Self {
Self::delivered(message, Settle::Pull(handler, runtime), limits)
}
#[cfg(feature = "testing")]
pub(crate) fn in_process(
message: GcpMessage,
settlement: Settlement,
limits: DeliveryLimits,
) -> Self {
Self::delivered(message, Settle::InProcess(settlement), limits)
}
fn delivered(message: GcpMessage, handler: Settle, limits: DeliveryLimits) -> Self {
let mut headers = headers_of(message.attributes, message.ordering_key, 1);
if let Some(attempt) = handler.delivery_attempt() {
headers.insert(
Str::from_static(DELIVERY_ATTEMPT_HEADER),
attempt.to_string(),
);
}
Self {
payload: message.data,
headers,
handler,
limits,
}
}
#[cfg(feature = "testing")]
pub(crate) fn published(topic: &str, message: GcpMessage) -> RawMessage {
let headers = headers_of(message.attributes, message.ordering_key, 0);
RawMessage::new(topic, message.data).with_headers(headers)
}
fn at_delivery_cap(&self) -> bool {
match (
self.limits.max_delivery_attempts,
self.handler.delivery_attempt(),
) {
(Some(cap), Some(attempt)) => attempt >= cap,
_ => false,
}
}
async fn reject(self) -> Result<(), AckError> {
let handler = match self.handler {
Settle::Pull(handler, _) => handler,
#[cfg(feature = "testing")]
Settle::InProcess(settlement) => {
settlement.reject();
return Ok(());
}
};
match handler {
Handler::ExactlyOnce(handler) => handler
.confirmed_nack()
.await
.map_err(|e| AckError::Broker(Box::new(e))),
handler => {
handler.nack();
Ok(())
}
}
}
}
impl Partitioned for PubSubMessage {
fn partition_key(&self) -> Option<&[u8]> {
self.headers.get(PARTITION_KEY_HEADER)
}
}
impl IncomingMessage for PubSubMessage {
fn payload(&self) -> &[u8] {
&self.payload
}
fn headers(&self) -> &HeaderMap {
&self.headers
}
fn redelivery_count(&self) -> Option<u64> {
self.handler
.delivery_attempt()
.and_then(|attempt| u64::try_from(attempt).ok())
}
async fn ack(self) -> Result<(), AckError> {
let handler = match self.handler {
Settle::Pull(handler, _) => handler,
#[cfg(feature = "testing")]
Settle::InProcess(settlement) => {
settlement.ack();
return Ok(());
}
};
match handler {
Handler::ExactlyOnce(handler) => handler
.confirmed_ack()
.await
.map_err(|e| AckError::Broker(Box::new(e))),
handler => {
handler.ack();
Ok(())
}
}
}
fn supports_nack_after(&self) -> bool {
true
}
fn nack_after(self, delay: Duration) -> impl Future<Output = Result<(), AckError>> + Send {
if delay > self.limits.max_lease {
return ready(Err(AckError::Broker(box_err(
PubSubError::DelayBeyondLease {
requested: delay,
lease: self.limits.max_lease,
},
))));
}
#[cfg(feature = "testing")]
let (this, slot) = match self {
Self {
handler: Settle::InProcess(settlement),
..
} => {
settlement.reject_after(delay);
return ready(Ok(()));
}
this @ Self {
handler: Settle::Pull(_, slot),
..
} => (this, slot),
};
#[cfg(not(feature = "testing"))]
let (this, slot) = {
let Settle::Pull(_, slot) = self.handler;
(self, slot)
};
let Some(runtime) = slot.runtime() else {
drop(this);
return ready(Ok(()));
};
runtime.spawn(async move {
sleep(delay).await;
let _ = this.reject().await;
});
ready(Ok(()))
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
if requeue || self.at_delivery_cap() {
return self.reject().await;
}
self.ack().await
}
fn partition_key(&self) -> Option<&[u8]> {
Partitioned::partition_key(self)
}
}
fn headers_of(
attributes: HashMap<String, String>,
ordering_key: String,
spare: usize,
) -> HeaderMap {
let mut headers = HeaderMap::with_capacity(attributes.len() + 1 + spare);
for (name, value) in attributes {
headers.insert(name, value);
}
if !ordering_key.is_empty() {
headers.insert(Str::from_static(PARTITION_KEY_HEADER), ordering_key);
}
headers
}
pub(crate) fn header_text<'a>(name: &str, value: &'a [u8]) -> Result<&'a str, String> {
str::from_utf8(value).map_err(|_| {
format!(
"header {name:?} is not UTF-8, and a Pub/Sub attribute or ordering key carries text \
only"
)
})
}
pub(crate) fn to_gcp_message(
payload: BytesMut,
headers: &HeaderMap,
ordering_key: Option<&str>,
) -> Result<GcpMessage, String> {
let mut attributes: Vec<(String, String)> = Vec::with_capacity(headers.len());
for (name, value) in headers.iter() {
if name == PARTITION_KEY_HEADER {
continue;
}
attributes.push((name.to_owned(), header_text(name, value)?.to_owned()));
}
let mut message = GcpMessage::new().set_data(payload);
if !attributes.is_empty() {
message = message.set_attributes(attributes);
}
if let Some(key) = ordering_key {
message = message.set_ordering_key(key);
}
Ok(message)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_resolved_key_becomes_the_messages_ordering_key() {
let mut headers = HeaderMap::new();
headers.insert(PARTITION_KEY_HEADER, "user-42");
headers.insert("x-tenant", "acme");
let message = to_gcp_message(BytesMut::from(&b"{}"[..]), &headers, Some("user-42"))
.expect("text headers are attributes");
assert_eq!(message.ordering_key, "user-42");
assert_eq!(
message.attributes.get("x-tenant").map(String::as_str),
Some("acme")
);
assert!(!message.attributes.contains_key(PARTITION_KEY_HEADER));
}
#[test]
fn the_messages_data_is_the_buffer_the_framework_wrote() {
let payload = BytesMut::from(&br#"{"id":1}"#[..]);
let written_at = payload.as_ptr();
let message =
to_gcp_message(payload, &HeaderMap::new(), None).expect("no headers to refuse");
assert_eq!(
message.data.as_ptr(),
written_at,
"Pub/Sub keeps the payload, so the buffer travels into the message rather than being \
copied into a second one",
);
}
#[test]
fn plain_messages_carry_no_ordering_key() {
let message = to_gcp_message(BytesMut::from(&b"{}"[..]), &HeaderMap::new(), None)
.expect("no headers to refuse");
assert_eq!(message.ordering_key, "");
}
#[test]
fn a_header_that_is_not_text_is_refused() {
let mut headers = HeaderMap::new();
headers.insert("x-binary", Bytes::from_static(&[0x00, 0x80, 0xff]));
let refused = to_gcp_message(BytesMut::from(&b"{}"[..]), &headers, None)
.expect_err("a non-UTF-8 value has no attribute form");
assert!(refused.contains("x-binary"), "{refused}");
}
}