use std::{collections::HashMap, sync::Arc};
use sqlx::PgPool;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::{
error::{BusError, BusResult},
store::{
backend::{Envelope, Locator, MessagingBackend, PostgresBackend, Published},
jetstream::{self, JetStreamBackend},
},
};
#[derive(Clone)]
pub enum AnyBackend {
Postgres(PostgresBackend),
JetStream(Box<JetStreamBackend>),
}
impl MessagingBackend for AnyBackend {
fn name(&self) -> &'static str {
match self {
Self::Postgres(b) => b.name(),
Self::JetStream(b) => b.name(),
}
}
async fn publish(&self, envelope: Envelope) -> Published {
match self {
Self::Postgres(b) => b.publish(envelope).await,
Self::JetStream(b) => b.publish(envelope).await,
}
}
async fn fetch(&self, locator: &Locator, message_id: Uuid) -> BusResult<Option<String>> {
match self {
Self::Postgres(b) => b.fetch(locator, message_id).await,
Self::JetStream(b) => b.fetch(locator, message_id).await,
}
}
async fn retain(&self, before: chrono::DateTime<chrono::Utc>) -> BusResult<u64> {
match self {
Self::Postgres(b) => b.retain(before).await,
Self::JetStream(b) => b.retain(before).await,
}
}
async fn reconcile(&self, envelope: &Envelope) -> BusResult<Option<Locator>> {
match self {
Self::Postgres(b) => b.reconcile(envelope).await,
Self::JetStream(b) => b.reconcile(envelope).await,
}
}
}
#[derive(Clone)]
pub struct Backends {
postgres: PostgresBackend,
nats: Option<jetstream::Config>,
connected: Arc<RwLock<HashMap<Uuid, JetStreamBackend>>>,
}
impl Backends {
pub fn postgres_only(pool: PgPool) -> Self {
Self {
postgres: PostgresBackend::new(pool),
nats: None,
connected: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn with_jetstream(pool: PgPool, config: jetstream::Config) -> Self {
Self {
postgres: PostgresBackend::new(pool),
nats: Some(config),
connected: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn jetstream_configured(&self) -> bool {
self.nats.is_some()
}
pub async fn named(&self, name: &str, team_id: Uuid) -> BusResult<AnyBackend> {
match name {
PostgresBackend::NAME => Ok(AnyBackend::Postgres(self.postgres.clone())),
JetStreamBackend::NAME => {
if let Some(existing) = self.connected.read().await.get(&team_id) {
return Ok(AnyBackend::JetStream(Box::new(existing.clone())));
}
let Some(config) = &self.nats else {
return Err(BusError::conflict(
"this team's conversations are routed to JetStream, but this server \
was started without a broker (`--nats-url`). Its bodies are not \
lost; they are on the broker, and this process cannot reach it. \
Start the server with the broker configured, or route the team \
back to Postgres for *new* threads.",
));
};
let backend = JetStreamBackend::connect(config, team_id).await?;
self.connected
.write()
.await
.insert(team_id, backend.clone());
Ok(AnyBackend::JetStream(Box::new(backend)))
}
other => Err(BusError::conflict(format!(
"this conversation records backend '{other}', which this build does not \
know how to read. It is a newer server's data; upgrade rather than \
downgrade."
))),
}
}
pub async fn for_conversation(
&self,
pool: &PgPool,
conversation: Uuid,
) -> BusResult<AnyBackend> {
let row: Option<(String, Uuid)> =
sqlx::query_as("SELECT backend, team_id FROM conversations WHERE id = $1")
.bind(conversation)
.fetch_optional(pool)
.await?;
let Some((name, team_id)) = row else {
return Err(BusError::not_found("no such conversation"));
};
self.named(&name, team_id).await
}
pub async fn for_message(&self, backend: &str, team_id: Uuid) -> BusResult<AnyBackend> {
self.named(backend, team_id).await
}
pub async fn for_team(&self, pool: &PgPool, team_id: Uuid) -> BusResult<AnyBackend> {
let (name,): (String,) = sqlx::query_as("SELECT default_backend FROM teams WHERE id = $1")
.bind(team_id)
.fetch_one(pool)
.await?;
self.named(&name, team_id).await
}
pub async fn broker_reachable(&self) -> Option<bool> {
let config = self.nats.as_ref()?;
Some(JetStreamBackend::reachable(config).await)
}
pub async fn routed_teams(&self, pool: &PgPool) -> BusResult<Vec<Uuid>> {
let rows: Vec<(Uuid,)> = sqlx::query_as(
"SELECT id FROM teams t
WHERE t.default_backend <> 'postgres'
OR EXISTS (SELECT 1 FROM conversations c
WHERE c.team_id = t.id AND c.backend <> 'postgres')",
)
.fetch_all(pool)
.await?;
Ok(rows.into_iter().map(|r| r.0).collect())
}
}