use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use google_cloud_auth::credentials::{Credentials, anonymous};
use google_cloud_pubsub::client::{
BasePublisher, Publisher, Subscriber, SubscriptionAdmin, TopicAdmin,
};
use ruststream::{Broker, ConnectedBroker, DefaultPublish, DescribeServer, ServerSpec, Subscribe};
use tokio::sync::OnceCell;
use crate::error::{PubSubError, box_err};
use crate::publisher::{PubSubPublish, PubSubPublisher};
use crate::subscriber::PubSubSubscriber;
use crate::subscription::PubSubSubscription;
pub(crate) struct Core {
pub(crate) subscriber: Subscriber,
pub(crate) base_publisher: BasePublisher,
pub(crate) topic_admin: TopicAdmin,
pub(crate) subscription_admin: SubscriptionAdmin,
pub(crate) project: String,
pub(crate) closed: AtomicBool,
pub(crate) publishers: tokio::sync::Mutex<std::collections::HashMap<String, Publisher>>,
}
impl Core {
pub(crate) fn ensure_open(&self) -> Result<(), PubSubError> {
if self.closed.load(Ordering::Acquire) {
return Err(PubSubError::NotConnected);
}
Ok(())
}
pub(crate) fn topic_name(&self, topic: &str) -> String {
if topic.starts_with("projects/") {
topic.to_owned()
} else {
format!("projects/{}/topics/{topic}", self.project)
}
}
pub(crate) fn subscription_name(&self, subscription: &str) -> String {
if subscription.starts_with("projects/") {
subscription.to_owned()
} else {
format!("projects/{}/subscriptions/{subscription}", self.project)
}
}
}
impl std::fmt::Debug for Core {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Core")
.field("project", &self.project)
.field("closed", &self.closed.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
pub(crate) type CoreCell = Arc<OnceCell<Arc<Core>>>;
#[derive(Clone)]
#[must_use]
pub struct PubSubBroker {
project: String,
credentials: Option<Credentials>,
endpoint: Option<String>,
emulator: Option<String>,
cell: CoreCell,
}
impl std::fmt::Debug for PubSubBroker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PubSubBroker")
.field("project", &self.project)
.field("emulator", &self.emulator)
.finish_non_exhaustive()
}
}
impl PubSubBroker {
pub fn new(project: impl Into<String>) -> Self {
Self {
project: project.into(),
credentials: None,
endpoint: None,
emulator: None,
cell: Arc::new(OnceCell::new()),
}
}
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.credentials = Some(credentials);
self
}
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
pub fn emulator(mut self, host: impl Into<String>) -> Self {
self.emulator = Some(host.into());
self
}
#[must_use]
pub fn publisher(&self) -> PubSubPublisher {
PubSubPublisher::new(Arc::clone(&self.cell))
}
}
macro_rules! build_client {
($builder:expr, $endpoint:expr, $credentials:expr) => {{
let mut b = $builder;
if let Some(endpoint) = $endpoint {
b = b.with_endpoint(endpoint.clone());
}
if let Some(credentials) = $credentials {
b = b.with_credentials(credentials.clone());
}
b.build()
.await
.map_err(|e| PubSubError::Connect(box_err(e)))
}};
}
impl Broker for PubSubBroker {
type Error = PubSubError;
type Connected = ConnectedPubSubBroker;
async fn connect(self) -> Result<Self::Connected, Self::Error> {
let core = self
.cell
.get_or_try_init(async || {
let (endpoint, credentials) = if let Some(host) = &self.emulator {
(
Some(format!("http://{host}")),
Some(anonymous::Builder::new().build()),
)
} else {
(self.endpoint.clone(), self.credentials.clone())
};
let subscriber: Subscriber =
build_client!(Subscriber::builder(), &endpoint, &credentials)?;
let base_publisher: BasePublisher =
build_client!(BasePublisher::builder(), &endpoint, &credentials)?;
let topic_admin: TopicAdmin =
build_client!(TopicAdmin::builder(), &endpoint, &credentials)?;
let subscription_admin: SubscriptionAdmin =
build_client!(SubscriptionAdmin::builder(), &endpoint, &credentials)?;
Ok::<_, PubSubError>(Arc::new(Core {
subscriber,
base_publisher,
topic_admin,
subscription_admin,
project: self.project.clone(),
closed: AtomicBool::new(false),
publishers: tokio::sync::Mutex::new(std::collections::HashMap::new()),
}))
})
.await?
.clone();
Ok(ConnectedPubSubBroker {
core,
cell: self.cell,
})
}
}
impl DescribeServer for PubSubBroker {
fn describe_server(&self) -> ServerSpec {
let host = self
.emulator
.clone()
.or_else(|| self.endpoint.clone())
.unwrap_or_else(|| "pubsub.googleapis.com".to_owned());
ServerSpec::new(host, "googlepubsub")
}
}
#[derive(Debug)]
pub struct ConnectedPubSubBroker {
pub(crate) core: Arc<Core>,
cell: CoreCell,
}
impl ConnectedPubSubBroker {
#[must_use]
pub fn publisher(&self) -> PubSubPublisher {
PubSubPublisher::new(Arc::clone(&self.cell))
}
pub async fn subscribe_descriptor(
&self,
descriptor: PubSubSubscription,
) -> Result<PubSubSubscriber, PubSubError> {
descriptor.validate()?;
self.core.ensure_open()?;
if let Some(topic) = descriptor.create_topic_ref() {
self.ensure_topic(topic).await?;
self.ensure_subscription(descriptor.subscription(), topic)
.await?;
}
Ok(PubSubSubscriber::open(&self.core, &descriptor))
}
async fn ensure_topic(&self, topic: &str) -> Result<(), PubSubError> {
let name = self.core.topic_name(topic);
let admin = &self.core.topic_admin;
if admin
.get_topic()
.set_topic(name.clone())
.send()
.await
.is_ok()
{
return Ok(());
}
match admin.create_topic().set_name(name.clone()).send().await {
Ok(_) => Ok(()),
Err(create_err) => {
if admin
.get_topic()
.set_topic(name.clone())
.send()
.await
.is_ok()
{
Ok(())
} else {
Err(PubSubError::Admin {
name,
source: box_err(create_err),
})
}
}
}
}
async fn ensure_subscription(
&self,
subscription: &str,
topic: &str,
) -> Result<(), PubSubError> {
let name = self.core.subscription_name(subscription);
let topic_name = self.core.topic_name(topic);
let admin = &self.core.subscription_admin;
if admin
.get_subscription()
.set_subscription(name.clone())
.send()
.await
.is_ok()
{
return Ok(());
}
match admin
.create_subscription()
.set_name(name.clone())
.set_topic(topic_name)
.send()
.await
{
Ok(_) => Ok(()),
Err(create_err) => {
if admin
.get_subscription()
.set_subscription(name.clone())
.send()
.await
.is_ok()
{
Ok(())
} else {
Err(PubSubError::Admin {
name,
source: box_err(create_err),
})
}
}
}
}
}
impl ConnectedBroker for ConnectedPubSubBroker {
type Error = PubSubError;
type Closed = ();
async fn shutdown(self) -> Result<(), Self::Error> {
self.core.closed.store(true, Ordering::Release);
let publishers: Vec<_> = self
.core
.publishers
.lock()
.await
.values()
.cloned()
.collect();
for publisher in publishers {
publisher.flush().await;
}
Ok(())
}
}
impl Subscribe for ConnectedPubSubBroker {
type Subscriber = PubSubSubscriber;
async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
self.subscribe_descriptor(PubSubSubscription::new(name))
.await
}
}
impl DefaultPublish for ConnectedPubSubBroker {
type Policy = PubSubPublish;
}