Registry crate implementation

This commit is contained in:
mdecimus
2026-01-23 18:44:11 +01:00
parent 2266634f36
commit acecd32c8e
43 changed files with 2258 additions and 912 deletions

View File

@@ -0,0 +1,105 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use rdkafka::{
ClientConfig, ClientContext, TopicPartitionList,
consumer::{BaseConsumer, ConsumerContext, Rebalance, StreamConsumer},
error::KafkaResult,
producer::FutureProducer,
};
use std::{fmt::Debug, time::Duration};
use utils::config::{Config, utils::AsKey};
pub mod pubsub;
pub(super) type LoggingConsumer = StreamConsumer<CustomContext>;
pub struct KafkaPubSub {
consumer_builder: ClientConfig,
producer: FutureProducer,
}
impl KafkaPubSub {
pub async fn open(config: &mut Config, prefix: impl AsKey) -> Option<Self> {
let prefix = prefix.as_key();
let brokers = config
.values((&prefix, "brokers"))
.map(|(_, v)| v.to_string())
.collect::<Vec<_>>();
if brokers.is_empty() {
config.new_build_error((&prefix, "brokers"), "No Kafka brokers specified");
return None;
}
let mut consumer_builder = ClientConfig::new();
consumer_builder
.set(
"group.id",
config.value_require_non_empty((&prefix, "group-id"))?,
)
.set(
"bootstrap.servers",
config.value_require_non_empty((&prefix, "brokers"))?,
)
.set("enable.partition.eof", "false")
.set(
"session.timeout.ms",
config
.property_or_default((&prefix, "timeout.session"), "5s")
.unwrap_or(Duration::from_secs(5))
.as_millis()
.to_string(),
)
.set("enable.auto.commit", "true");
let producer = ClientConfig::new()
.set(
"bootstrap.servers",
config.value_require_non_empty((&prefix, "brokers"))?,
)
.set(
"message.timeout.ms",
config
.property_or_default((&prefix, "timeout.message"), "5s")
.unwrap_or(Duration::from_secs(5))
.as_millis()
.to_string(),
)
.create()
.map_err(|err| {
config.new_build_error(
(&prefix, "config"),
format!("Failed to create Kafka producer: {}", err),
);
})
.ok()?;
KafkaPubSub {
consumer_builder,
producer,
}
.into()
}
}
impl Debug for KafkaPubSub {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KafkaPubSub").finish()
}
}
pub(super) struct CustomContext;
impl ClientContext for CustomContext {}
impl ConsumerContext for CustomContext {
fn pre_rebalance(&self, _: &BaseConsumer<Self>, _: &Rebalance) {}
fn post_rebalance(&self, _: &BaseConsumer<Self>, _: &Rebalance) {}
fn commit_callback(&self, _: KafkaResult<()>, _: &TopicPartitionList) {}
}

View File

@@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::time::Duration;
use super::{CustomContext, KafkaPubSub, LoggingConsumer};
use crate::dispatch::pubsub::{Msg, PubSubStream};
use rdkafka::{
Message,
consumer::{CommitMode, Consumer, StreamConsumer},
producer::FutureRecord,
};
use trc::{ClusterEvent, Error, EventType};
pub struct KafkaPubSubStream {
subs: LoggingConsumer,
}
impl KafkaPubSub {
pub async fn publish(&self, topic: &'static str, message: Vec<u8>) -> trc::Result<()> {
self.producer
.send(
FutureRecord::<(), [u8]>::to(topic).payload(message.as_slice()),
Duration::from_secs(0),
)
.await
.map(|_| ())
.map_err(|(err, _)| {
Error::new(EventType::Cluster(ClusterEvent::PublisherError)).reason(err)
})
}
pub async fn subscribe(&self, topic: &'static str) -> trc::Result<PubSubStream> {
let subs: StreamConsumer<CustomContext> = self
.consumer_builder
.create_with_context(CustomContext)
.map_err(|err| {
Error::new(EventType::Cluster(ClusterEvent::SubscriberError)).reason(err)
})?;
subs.subscribe(&[topic]).map_err(|err| {
Error::new(EventType::Cluster(ClusterEvent::SubscriberError)).reason(err)
})?;
Ok(PubSubStream::Kafka(KafkaPubSubStream { subs }))
}
}
impl KafkaPubSubStream {
pub async fn next(&mut self) -> Option<Msg> {
let msg = self.subs.recv().await.ok()?;
let _ = self.subs.commit_message(&msg, CommitMode::Async);
Msg::Kafka(msg.payload().unwrap_or_default().to_vec()).into()
}
}

View File

@@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
#[cfg(feature = "kafka")]
pub mod kafka;
#[cfg(feature = "nats")]
pub mod nats;
#[cfg(feature = "redis")]
pub mod redis;
#[cfg(feature = "zenoh")]
pub mod zenoh;

View File

@@ -0,0 +1,108 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::time::Duration;
use async_nats::Client;
use utils::config::{Config, utils::AsKey};
pub mod pubsub;
#[derive(Debug)]
pub struct NatsPubSub {
client: Client,
}
impl NatsPubSub {
pub async fn open(config: &mut Config, prefix: impl AsKey) -> Option<Self> {
let prefix = prefix.as_key();
let urls = config
.values((&prefix, "address"))
.map(|(_, v)| v.to_string())
.collect::<Vec<_>>();
if urls.is_empty() {
config.new_build_error((&prefix, "address"), "No Nats addresses specified");
return None;
}
let mut opts = async_nats::ConnectOptions::new()
.max_reconnects(
config
.property_or_default::<Option<usize>>((&prefix, "max-reconnects"), "false")
.unwrap_or_default(),
)
.connection_timeout(
config
.property_or_default((&prefix, "timeout.connection"), "5s")
.unwrap_or_else(|| Duration::from_secs(5)),
)
.request_timeout(
config
.property_or_default::<Option<Duration>>((&prefix, "timeout.request"), "10s")
.unwrap_or_else(|| Some(Duration::from_secs(10))),
)
.ping_interval(
config
.property_or_default((&prefix, "ping-interval"), "60s")
.unwrap_or_else(|| Duration::from_secs(5)),
)
.client_capacity(
config
.property_or_default((&prefix, "capacity.client"), "2048")
.unwrap_or(2048),
)
.subscription_capacity(
config
.property_or_default((&prefix, "capacity.subscription"), "65536")
.unwrap_or(65536),
)
.read_buffer_capacity(
config
.property_or_default((&prefix, "capacity.read-buffer"), "65535")
.unwrap_or(65535),
)
.require_tls(
config
.property_or_default((&prefix, "tls.enable"), "false")
.unwrap_or_default(),
);
if config
.property_or_default((&prefix, "no-echo"), "true")
.unwrap_or(true)
{
opts = opts.no_echo();
}
if let (Some(user), Some(pass)) = (
config.value((&prefix, "user")),
config.value((&prefix, "password")),
) {
opts = opts.user_and_password(user.to_string(), pass.to_string());
} else if let Some(credentials) = config.value((&prefix, "credentials")) {
opts = opts
.credentials(credentials)
.map_err(|err| {
config.new_build_error(
(&prefix, "credentials"),
format!("Failed to parse Nats credentials: {}", err),
);
})
.ok()?;
}
async_nats::connect_with_options(urls, opts)
.await
.map_err(|err| {
config.new_build_error(
(&prefix, "urls"),
format!("Failed to connect to Nats: {}", err),
);
})
.map(|client| NatsPubSub { client })
.ok()
}
}

View File

@@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::NatsPubSub;
use crate::dispatch::pubsub::{Msg, PubSubStream};
use futures::StreamExt;
use trc::{ClusterEvent, Error, EventType};
pub struct NatsPubSubStream {
subs: async_nats::Subscriber,
}
impl NatsPubSub {
pub async fn publish(&self, topic: &'static str, message: Vec<u8>) -> trc::Result<()> {
self.client
.publish(topic, message.into())
.await
.map_err(|err| Error::new(EventType::Cluster(ClusterEvent::PublisherError)).reason(err))
}
pub async fn subscribe(&self, topic: &'static str) -> trc::Result<PubSubStream> {
self.client
.subscribe(topic)
.await
.map(|subs| PubSubStream::Nats(NatsPubSubStream { subs }))
.map_err(|err| {
Error::new(EventType::Cluster(ClusterEvent::SubscriberError)).reason(err)
})
}
}
impl NatsPubSubStream {
pub async fn next(&mut self) -> Option<Msg> {
self.subs.next().await.map(Msg::Nats)
}
}

View File

@@ -0,0 +1,105 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Msg, PubSubStream};
use futures::StreamExt;
use redis::{AsyncCommands, PushInfo, cluster::ClusterConfig, cluster_async::ClusterConnection};
use std::fmt::Display;
use store::backend::redis::{RedisPool, RedisStore};
use tokio::sync::mpsc::UnboundedReceiver;
pub struct RedisPubSubStream {
stream: redis::aio::PubSubStream,
}
pub struct RedisClusterPubSubStream {
_conn: ClusterConnection,
rx: UnboundedReceiver<PushInfo>,
}
pub(crate) async fn redis_publish(
redis: &RedisStore,
topic: &'static str,
message: Vec<u8>,
) -> trc::Result<()> {
match &redis.pool {
RedisPool::Single(pool) => pool
.get()
.await
.map_err(into_error)?
.as_mut()
.publish(topic, message)
.await
.map_err(into_error),
RedisPool::Cluster(pool) => pool
.get()
.await
.map_err(into_error)?
.as_mut()
.publish(topic, message)
.await
.map_err(into_error),
}
}
pub(crate) async fn redis_subscribe(
redis: &RedisStore,
topic: &'static str,
) -> trc::Result<PubSubStream> {
match &redis.pool {
RedisPool::Single(pool) => {
let mut pubsub = pool
.manager()
.client
.get_async_pubsub()
.await
.map_err(into_error)?;
pubsub.subscribe(topic).await.map_err(into_error)?;
Ok(PubSubStream::Redis(RedisPubSubStream {
stream: pubsub.into_on_message(),
}))
}
RedisPool::Cluster(pool) => {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut _conn = pool
.manager()
.client
.get_async_connection_with_config(ClusterConfig::default().set_push_sender(tx))
.await
.map_err(into_error)?;
_conn.subscribe(topic).await.map_err(into_error)?;
Ok(PubSubStream::RedisCluster(RedisClusterPubSubStream {
_conn,
rx,
}))
}
}
}
impl RedisPubSubStream {
pub async fn next(&mut self) -> Option<Msg> {
self.stream.next().await.map(Msg::Redis)
}
}
impl RedisClusterPubSubStream {
pub async fn next(&mut self) -> Option<Msg> {
loop {
if let Some(msg) = redis::Msg::from_push_info(self.rx.recv().await?) {
return Some(Msg::Redis(msg));
}
}
}
}
#[inline(always)]
fn into_error(err: impl Display) -> trc::Error {
trc::StoreEvent::RedisError.reason(err)
}

View File

@@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use utils::config::{Config, utils::AsKey};
pub mod pubsub;
#[derive(Debug)]
pub struct ZenohPubSub {
session: zenoh::Session,
}
impl ZenohPubSub {
pub async fn open(config: &mut Config, prefix: impl AsKey) -> Option<Self> {
let prefix = prefix.as_key();
let zenoh_config =
zenoh::Config::from_json5(config.value_require_non_empty((&prefix, "config"))?)
.map_err(|err| {
config.new_build_error(
(&prefix, "config"),
format!("Invalid zenoh config: {}", err),
);
})
.ok()?;
zenoh::open(zenoh_config)
.await
.map_err(|err| {
config.new_build_error(
(&prefix, "config"),
format!("Failed to create zenoh session: {}", err),
);
})
.map(|session| ZenohPubSub { session })
.ok()
}
}

View File

@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::ZenohPubSub;
use crate::dispatch::pubsub::{Msg, PubSubStream};
use trc::{ClusterEvent, Error, EventType};
pub struct ZenohPubSubStream {
subs: zenoh::pubsub::Subscriber<zenoh::handlers::FifoChannelHandler<zenoh::sample::Sample>>,
}
impl ZenohPubSub {
pub async fn publish(&self, topic: &'static str, message: Vec<u8>) -> trc::Result<()> {
self.session
.declare_publisher(topic)
.await
.map_err(|err| {
Error::new(EventType::Cluster(ClusterEvent::PublisherError)).reason(err)
})?
.put(message)
.await
.map_err(|err| Error::new(EventType::Cluster(ClusterEvent::PublisherError)).reason(err))
}
pub async fn subscribe(&self, topic: &'static str) -> trc::Result<PubSubStream> {
self.session
.declare_subscriber(topic)
.await
.map(|subs| PubSubStream::Zenoh(ZenohPubSubStream { subs }))
.map_err(|err| {
Error::new(EventType::Cluster(ClusterEvent::SubscriberError)).reason(err)
})
}
}
impl ZenohPubSubStream {
pub async fn next(&mut self) -> Option<Msg> {
self.subs
.recv_async()
.await
.map(|sample| Msg::Zenoh(sample.payload().to_bytes().into_owned()))
.ok()
}
}

View File

@@ -0,0 +1,97 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
#[cfg(feature = "redis")]
use crate::PubSubStream;
use crate::{Coordinator, Msg};
#[allow(unused_variables)]
impl Coordinator {
pub async fn publish(&self, topic: &'static str, message: Vec<u8>) -> trc::Result<()> {
match self {
#[cfg(feature = "redis")]
Coordinator::Redis(store) => {
crate::backend::redis::redis_publish(store, topic, message).await
}
#[cfg(feature = "nats")]
Coordinator::Nats(store) => store.publish(topic, message).await,
#[cfg(feature = "zenoh")]
Coordinator::Zenoh(store) => store.publish(topic, message).await,
#[cfg(feature = "kafka")]
Coordinator::Kafka(store) => store.publish(topic, message).await,
Coordinator::None => Err(trc::StoreEvent::NotSupported.into_err()),
}
}
pub async fn subscribe(&self, topic: &'static str) -> trc::Result<PubSubStream> {
match self {
#[cfg(feature = "redis")]
Coordinator::Redis(store) => crate::backend::redis::redis_subscribe(store, topic).await,
#[cfg(feature = "nats")]
Coordinator::Nats(store) => store.subscribe(topic).await,
#[cfg(feature = "zenoh")]
Coordinator::Zenoh(store) => store.subscribe(topic).await,
#[cfg(feature = "kafka")]
Coordinator::Kafka(store) => store.subscribe(topic).await,
Coordinator::None => Err(trc::StoreEvent::NotSupported.into_err()),
}
}
pub fn is_none(&self) -> bool {
matches!(self, Coordinator::None)
}
}
impl PubSubStream {
pub async fn next(&mut self) -> Option<Msg> {
match self {
#[cfg(feature = "redis")]
PubSubStream::Redis(stream) => stream.next().await,
#[cfg(feature = "redis")]
PubSubStream::RedisCluster(stream) => stream.next().await,
#[cfg(feature = "nats")]
PubSubStream::Nats(stream) => stream.next().await,
#[cfg(feature = "zenoh")]
PubSubStream::Zenoh(stream) => stream.next().await,
#[cfg(feature = "kafka")]
PubSubStream::Kafka(stream) => stream.next().await,
#[cfg(not(any(feature = "redis", feature = "nats")))]
PubSubStream::Unimplemented => None,
}
}
}
impl Msg {
pub fn payload(&self) -> &[u8] {
match self {
#[cfg(feature = "redis")]
Msg::Redis(msg) => msg.get_payload_bytes(),
#[cfg(feature = "nats")]
Msg::Nats(msg) => msg.payload.as_ref(),
#[cfg(feature = "zenoh")]
Msg::Zenoh(msg) => msg.as_slice(),
#[cfg(feature = "kafka")]
Msg::Kafka(msg) => msg.as_slice(),
#[cfg(not(any(feature = "redis", feature = "nats")))]
Msg::Unimplemented => &[],
}
}
pub fn topic(&self) -> &str {
match self {
#[cfg(feature = "redis")]
Msg::Redis(msg) => msg.get_channel_name(),
#[cfg(feature = "nats")]
Msg::Nats(msg) => msg.subject.as_str(),
#[cfg(feature = "zenoh")]
Msg::Zenoh(_) => "",
#[cfg(feature = "kafka")]
Msg::Kafka(_) => "",
#[cfg(not(any(feature = "redis", feature = "nats")))]
Msg::Unimplemented => "",
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
pub mod backend;
pub mod dispatch;
#[derive(Clone, Default)]
pub enum Coordinator {
#[cfg(feature = "redis")]
Redis(Arc<store::backend::redis::RedisStore>),
#[cfg(feature = "nats")]
Nats(Arc<backend::nats::NatsPubSub>),
#[cfg(feature = "zenoh")]
Zenoh(Arc<backend::zenoh::ZenohPubSub>),
#[cfg(feature = "kafka")]
Kafka(Arc<backend::kafka::KafkaPubSub>),
#[default]
None,
}
pub enum PubSubStream {
#[cfg(feature = "redis")]
Redis(crate::backend::redis::RedisPubSubStream),
#[cfg(feature = "redis")]
RedisCluster(crate::backend::redis::RedisClusterPubSubStream),
#[cfg(feature = "nats")]
Nats(crate::backend::nats::pubsub::NatsPubSubStream),
#[cfg(feature = "zenoh")]
Zenoh(crate::backend::zenoh::pubsub::ZenohPubSubStream),
#[cfg(feature = "kafka")]
Kafka(crate::backend::kafka::pubsub::KafkaPubSubStream),
#[cfg(not(any(feature = "redis", feature = "nats")))]
Unimplemented,
}
pub enum Msg {
#[cfg(feature = "redis")]
Redis(redis::Msg),
#[cfg(feature = "nats")]
Nats(async_nats::Message),
#[cfg(feature = "zenoh")]
Zenoh(Vec<u8>),
#[cfg(feature = "kafka")]
Kafka(Vec<u8>),
#[cfg(not(any(feature = "redis", feature = "nats")))]
Unimplemented,
}