use bytes::Bytes;
use pulsar::proto::MessageIdData;
use ruststream::{AckError, Headers, IncomingMessage, OutgoingMessage, Partitioned, Positioned};
use tokio::sync::{mpsc, oneshot};
use crate::error::PulsarError;
pub const PARTITION_KEY_HEADER: &str = "partition-key";
#[derive(Debug)]
pub(crate) enum SettleKind {
Ack,
Nack,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PulsarPosition {
Earliest,
Latest,
MessageId(MessageIdData),
Timestamp(u64),
}
impl PulsarPosition {
#[must_use]
pub fn earliest() -> Self {
Self::Earliest
}
#[must_use]
pub fn latest() -> Self {
Self::Latest
}
#[must_use]
pub fn timestamp(millis: u64) -> Self {
Self::Timestamp(millis)
}
}
#[derive(Debug)]
pub(crate) struct SeekCmd {
pub(crate) position: PulsarPosition,
pub(crate) done: oneshot::Sender<Result<(), PulsarError>>,
}
#[derive(Debug)]
pub(crate) struct SettleCmd {
pub(crate) topic: String,
pub(crate) id: MessageIdData,
pub(crate) kind: SettleKind,
pub(crate) done: oneshot::Sender<Result<(), AckError>>,
}
#[derive(Debug)]
pub(crate) enum DriverCmd {
Settle(SettleCmd),
Seek(SeekCmd),
}
pub(crate) type SettleSender = mpsc::UnboundedSender<DriverCmd>;
pub struct PulsarMessage {
payload: Bytes,
headers: Headers,
topic: String,
id: MessageIdData,
settle: SettleSender,
}
impl std::fmt::Debug for PulsarMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PulsarMessage")
.field("topic", &self.topic)
.field("payload_len", &self.payload.len())
.finish_non_exhaustive()
}
}
impl PulsarMessage {
pub(crate) fn new(message: &pulsar::consumer::Message<Vec<u8>>, settle: SettleSender) -> Self {
let metadata = message.metadata();
let mut headers = Headers::with_capacity(metadata.properties.len() + 1);
for kv in &metadata.properties {
headers.insert(kv.key.clone(), kv.value.clone());
}
if let Some(key) = &metadata.partition_key {
headers.insert(PARTITION_KEY_HEADER, key.clone());
}
Self {
payload: Bytes::copy_from_slice(&message.payload.data),
headers,
topic: message.topic.clone(),
id: message.message_id().clone(),
settle,
}
}
#[must_use]
pub fn topic(&self) -> &str {
&self.topic
}
async fn send_settle(self, kind: SettleKind) -> Result<(), AckError> {
let (done, wait) = oneshot::channel();
self.settle
.send(DriverCmd::Settle(SettleCmd {
topic: self.topic,
id: self.id,
kind,
done,
}))
.map_err(|_| {
AckError::Broker(Box::from("the subscription's driver task has shut down"))
})?;
wait.await.map_err(|_| {
AckError::Broker(Box::from("the subscription's driver task has shut down"))
})?
}
}
impl Positioned for PulsarMessage {
type Position = PulsarPosition;
fn position(&self) -> PulsarPosition {
PulsarPosition::MessageId(self.id.clone())
}
}
impl Partitioned for PulsarMessage {
fn partition_key(&self) -> Option<&[u8]> {
self.headers.get(PARTITION_KEY_HEADER)
}
}
impl IncomingMessage for PulsarMessage {
fn payload(&self) -> &[u8] {
&self.payload
}
fn headers(&self) -> &Headers {
&self.headers
}
async fn ack(self) -> Result<(), AckError> {
self.send_settle(SettleKind::Ack).await
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
if requeue {
self.send_settle(SettleKind::Nack).await
} else {
self.send_settle(SettleKind::Ack).await
}
}
fn partition_key(&self) -> Option<&[u8]> {
Partitioned::partition_key(self)
}
}
pub(crate) fn to_pulsar_message(msg: &OutgoingMessage<'_>) -> pulsar::producer::Message {
let headers = msg.headers();
let mut properties = std::collections::HashMap::with_capacity(headers.len());
let mut partition_key = None;
for (name, value) in headers.iter() {
let text = String::from_utf8_lossy(value).into_owned();
if name == PARTITION_KEY_HEADER {
partition_key = Some(text);
} else {
properties.insert(name.to_owned(), text);
}
}
pulsar::producer::Message {
payload: msg.payload().to_vec(),
properties,
partition_key,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partition_key_header_becomes_the_partition_key() {
let mut headers = Headers::new();
headers.insert(PARTITION_KEY_HEADER, "user-42");
headers.insert("x-tenant", "acme");
let outgoing = OutgoingMessage::new("orders", b"{}".as_slice()).with_headers(headers);
let message = to_pulsar_message(&outgoing);
assert_eq!(message.partition_key.as_deref(), Some("user-42"));
assert_eq!(
message.properties.get("x-tenant").map(String::as_str),
Some("acme")
);
assert!(!message.properties.contains_key(PARTITION_KEY_HEADER));
}
}