#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::collections::HashMap;
use std::fmt;
#[cfg(feature = "testing")]
use std::future::{Future, ready};
use std::panic::resume_unwind;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex as StdMutex, PoisonError};
use fe2o3_amqp::connection::{Connection, ConnectionHandle, Error as ConnectionError};
use fe2o3_amqp::sasl_profile::SaslProfile;
use fe2o3_amqp::session::{Session, SessionHandle};
use fe2o3_amqp::{Receiver, Sender};
use fe2o3_amqp_types::messaging::Source;
use fe2o3_amqp_types::primitives::{Array, Symbol};
#[cfg(feature = "testing")]
use ruststream::testing::{Coordinator, InProcess, TestableBroker};
use ruststream::{
AddressedCopies, Broker, ConnectedBroker, DefaultPublish, DescribeServer, ServerSpec, Subscribe,
};
#[cfg(feature = "testing")]
use ruststream::{OutgoingMessage, RawMessage};
use tokio::net::TcpStream;
use tokio::runtime::Handle;
use tokio::sync::{Mutex, OnceCell, mpsc, watch};
use url::Url;
#[cfg(feature = "testing")]
use crate::address::Routing;
use crate::address::{AmqpAddress, Settle};
#[cfg(feature = "asyncapi")]
use crate::bindings;
use crate::config::Sasl;
use crate::error::{AmqpError, box_err};
#[cfg(feature = "testing")]
use crate::in_process::{self, Bus};
use crate::publisher::{AmqpPublish, AmqpPublisher};
use crate::subscriber::AmqpSubscriber;
const AMQP_PORT: u16 = 5672;
const AMQPS_PORT: u16 = 5671;
async fn open_socket(url: &Url) -> Result<TcpStream, AmqpError> {
let addresses = url
.socket_addrs(|| match url.scheme() {
"amqps" => Some(AMQPS_PORT),
_ => Some(AMQP_PORT),
})
.map_err(|e| AmqpError::Connect(box_err(e)))?;
let stream = TcpStream::connect(&*addresses)
.await
.map_err(|e| AmqpError::Connect(box_err(e)))?;
stream
.set_nodelay(true)
.map_err(|e| AmqpError::Connect(box_err(e)))?;
Ok(stream)
}
pub(crate) struct AmqpCore {
pub(crate) conn: Mutex<ConnectionHandle<()>>,
pub(crate) session: Mutex<SessionHandle<()>>,
senders: Mutex<HashMap<String, Arc<SenderLink>>>,
pub(crate) pumps: Pumps,
pub(crate) closed: AtomicBool,
pub(crate) container_id: String,
link_seq: AtomicU64,
pub(crate) runtime: Handle,
}
impl AmqpCore {
pub(crate) fn ensure_open(&self) -> Result<(), AmqpError> {
if self.closed.load(Ordering::Acquire) {
return Err(AmqpError::NotConnected);
}
Ok(())
}
#[allow(clippy::significant_drop_tightening)]
pub(crate) async fn sender_for(&self, address: &str) -> Result<Arc<SenderLink>, AmqpError> {
let mut senders = self.senders.lock().await;
if let Some(link) = senders.get(address) {
return Ok(Arc::clone(link));
}
let sender = ConnectedAmqpBroker::attach_sender(self, address).await?;
let link = Arc::new(SenderLink(Mutex::new(Some(sender))));
senders.insert(address.to_owned(), Arc::clone(&link));
Ok(link)
}
async fn close_senders(&self) -> Result<(), AmqpError> {
let links: Vec<(String, Arc<SenderLink>)> =
self.senders.lock().await.drain().collect::<Vec<_>>();
let mut first_error = None;
for (address, link) in links {
if let Err(err) = link.close(&address).await {
first_error.get_or_insert(err);
}
}
first_error.map_or(Ok(()), Err)
}
async fn close_connection(
&self,
_sessions_ended: SessionsEnded,
) -> Result<(), ConnectionError> {
self.conn.lock().await.close().await
}
pub(crate) fn link_name(&self, role: &str) -> String {
let seq = self.link_seq.fetch_add(1, Ordering::Relaxed);
format!("{}-{role}-{seq}", self.container_id)
}
pub(crate) fn correlation_id(&self) -> String {
let seq = self.link_seq.fetch_add(1, Ordering::Relaxed);
format!("{}-corr-{seq}", self.container_id)
}
async fn open_subscription(
self: Arc<Self>,
address: AmqpAddress,
) -> Result<AmqpSubscriber, AmqpError> {
let guard = self.pumps.guard()?;
let session = {
let mut conn = self.conn.lock().await;
Session::begin(&mut conn)
.await
.map_err(|e| AmqpError::Session(box_err(e)))?
};
AmqpSubscriber::attach(&self, session, address, guard).await
}
}
pub(crate) struct Pumps {
stop: watch::Sender<bool>,
running: StdMutex<Option<mpsc::Sender<()>>>,
ended: Mutex<mpsc::Receiver<()>>,
}
pub(crate) struct PumpGuard {
pub(crate) stop: watch::Receiver<bool>,
_running: mpsc::Sender<()>,
}
pub(crate) struct SessionsEnded(());
impl Pumps {
fn new() -> Self {
let (stop, _) = watch::channel(false);
let (running, ended) = mpsc::channel(1);
Self {
stop,
running: StdMutex::new(Some(running)),
ended: Mutex::new(ended),
}
}
pub(crate) fn guard(&self) -> Result<PumpGuard, AmqpError> {
let running = self
.running
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
.ok_or(AmqpError::NotConnected)?;
Ok(PumpGuard {
stop: self.stop.subscribe(),
_running: running,
})
}
async fn stop_all(&self) -> SessionsEnded {
let running = self
.running
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
drop(running);
self.stop.send_replace(true);
let _ = self.ended.lock().await.recv().await;
SessionsEnded(())
}
}
pub(crate) struct SenderLink(Mutex<Option<Sender>>);
impl SenderLink {
#[allow(clippy::significant_drop_tightening)]
pub(crate) async fn with<F, T>(&self, f: F) -> Result<T, AmqpError>
where
F: AsyncFnOnce(&mut Sender) -> Result<T, AmqpError>,
{
let mut slot = self.0.lock().await;
let sender = slot.as_mut().ok_or(AmqpError::NotConnected)?;
f(sender).await
}
async fn close(&self, address: &str) -> Result<(), AmqpError> {
let taken = {
let mut slot = self.0.lock().await;
slot.take()
};
let Some(sender) = taken else {
return Ok(());
};
sender.close().await.map_err(|e| AmqpError::Detach {
address: address.to_owned(),
source: box_err(e),
})
}
}
impl fmt::Debug for SenderLink {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SenderLink").finish_non_exhaustive()
}
}
impl fmt::Debug for AmqpCore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AmqpCore")
.field("container_id", &self.container_id)
.field("closed", &self.closed.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub(crate) enum Link {
Amqp(Arc<AmqpCore>),
#[cfg(feature = "testing")]
InProcess(Arc<Bus>),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<Link>() == size_of::<Arc<AmqpCore>>());
pub(crate) type CoreCell = Arc<OnceCell<Link>>;
const DEFAULT_CONTAINER_ID: &str = "ruststream";
#[derive(Debug, Clone)]
#[must_use]
pub struct AmqpBroker {
url: String,
container_id: Option<String>,
sasl: Option<Sasl>,
cell: CoreCell,
}
impl AmqpBroker {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
container_id: None,
sasl: None,
cell: Arc::new(OnceCell::new()),
}
}
pub fn sasl(mut self, sasl: Sasl) -> Self {
self.sasl = Some(sasl);
self
}
pub fn container_id(mut self, id: impl Into<String>) -> Self {
self.container_id = Some(id.into());
self
}
#[must_use]
pub fn publisher(&self) -> AmqpPublisher {
AmqpPublisher::new(Arc::clone(&self.cell))
}
fn container(&self) -> &str {
self.container_id.as_deref().unwrap_or(DEFAULT_CONTAINER_ID)
}
fn coordinate(&self) -> ServerSpec {
ServerSpec::from_url(&self.url, "amqp1").protocol_version("1.0")
}
}
impl Broker for AmqpBroker {
type Error = AmqpError;
type Connected = ConnectedAmqpBroker;
async fn connect(self) -> Result<Self::Connected, Self::Error> {
let container_id = self.container().to_owned();
let link = self
.cell
.get_or_try_init(async || {
let url =
Url::parse(self.url.as_str()).map_err(|e| AmqpError::Connect(box_err(e)))?;
let stream = open_socket(&url).await?;
let mut builder = Connection::builder()
.container_id(container_id.clone())
.scheme(url.scheme());
if let Some(hostname) = url.host_str() {
builder = builder.hostname(hostname).sasl_hostname(hostname);
}
if let Some(domain) = url.domain() {
builder = builder.domain(domain);
}
if let Some(sasl) = &self.sasl {
builder = builder.sasl_profile(sasl.profile.clone());
}
if let Ok(profile) = SaslProfile::try_from(&url) {
builder = builder.sasl_profile(profile);
}
let mut conn = builder
.open_with_stream(stream)
.await
.map_err(|e| AmqpError::Connect(box_err(e)))?;
let session = Session::begin(&mut conn)
.await
.map_err(|e| AmqpError::Session(box_err(e)))?;
Ok::<_, AmqpError>(Link::Amqp(Arc::new(AmqpCore {
conn: Mutex::new(conn),
session: Mutex::new(session),
senders: Mutex::new(HashMap::new()),
pumps: Pumps::new(),
closed: AtomicBool::new(false),
container_id,
link_seq: AtomicU64::new(0),
runtime: Handle::current(),
})))
})
.await?
.clone();
#[cfg(feature = "testing")]
if matches!(link, Link::InProcess(_)) {
return Err(AmqpError::Connect(Box::from(
"a clone of this broker is connected in process already, so it cannot connect to \
a server",
)));
}
Ok(ConnectedAmqpBroker::new(link, self.cell))
}
}
#[cfg(feature = "testing")]
impl InProcess for AmqpBroker {
fn connect_in_process(
self,
) -> impl Future<Output = Result<Self::Connected, Self::Error>> + Send {
let connected = in_process::check_url(&self.url).and_then(|()| {
let fresh = Link::InProcess(Arc::new(Bus::default()));
let link = match self.cell.set(fresh.clone()) {
Ok(()) => fresh,
Err(_) => match self.cell.get().cloned() {
Some(Link::Amqp(_)) => {
return Err(AmqpError::Connect(Box::from(
"a clone of this broker is connected to a server already, so it \
cannot connect in process",
)));
}
Some(link) => link,
None => fresh,
},
};
Ok(ConnectedAmqpBroker::new(link, self.cell))
});
ready(connected)
}
}
#[cfg(feature = "testing")]
ruststream::register_testable_broker!(AmqpBroker);
impl DescribeServer for AmqpBroker {
#[cfg(not(feature = "asyncapi"))]
fn describe_server(&self) -> ServerSpec {
self.coordinate()
}
#[cfg(feature = "asyncapi")]
fn describe_server(&self) -> ServerSpec {
self.coordinate()
.bindings(bindings::server(self.container()))
}
}
#[derive(Debug)]
pub struct ConnectedAmqpBroker {
pub(crate) link: Link,
cell: CoreCell,
#[cfg(feature = "testing")]
termini: StdMutex<HashMap<String, Termini>>,
}
#[cfg(feature = "testing")]
#[derive(Debug, Default, Clone, Copy)]
struct Termini {
anycast: usize,
multicast: usize,
}
impl ConnectedAmqpBroker {
fn new(link: Link, cell: CoreCell) -> Self {
Self {
link,
cell,
#[cfg(feature = "testing")]
termini: StdMutex::new(HashMap::new()),
}
}
#[must_use]
pub fn publisher(&self) -> AmqpPublisher {
AmqpPublisher::new(Arc::clone(&self.cell))
}
pub async fn subscribe_address(
&self,
address: AmqpAddress,
) -> Result<AmqpSubscriber, AmqpError> {
#[cfg(feature = "testing")]
let (at, routing) = (address.address().to_owned(), address.routing());
let subscriber = self.open(address).await?;
#[cfg(feature = "testing")]
self.opened(at, routing);
Ok(subscriber)
}
async fn open(&self, address: AmqpAddress) -> Result<AmqpSubscriber, AmqpError> {
address.validate()?;
let core = match &self.link {
Link::Amqp(core) => core,
#[cfg(feature = "testing")]
Link::InProcess(bus) => {
let deliveries = in_process::subscribe(bus, &address)?;
return Ok(AmqpSubscriber::in_process(deliveries, &address));
}
};
core.ensure_open()?;
let opening = Arc::clone(core);
match core.runtime.spawn(opening.open_subscription(address)).await {
Ok(opened) => opened,
Err(err) if err.is_panic() => resume_unwind(err.into_panic()),
Err(err) => Err(AmqpError::Session(box_err(err))),
}
}
pub(crate) async fn attach_sender(core: &AmqpCore, address: &str) -> Result<Sender, AmqpError> {
let mut session = core.session.lock().await;
Sender::attach(&mut session, core.link_name("sender"), address)
.await
.map_err(|e| AmqpError::Attach {
address: address.to_owned(),
source: box_err(e),
})
}
pub(crate) async fn attach_dynamic_receiver(core: &AmqpCore) -> Result<Receiver, AmqpError> {
let mut session = core.session.lock().await;
Receiver::builder()
.name(core.link_name("reply"))
.source(Source::builder().dynamic(true).build())
.auto_accept(true)
.attach(&mut session)
.await
.map_err(|e| AmqpError::Attach {
address: "(dynamic)".to_owned(),
source: box_err(e),
})
}
}
pub(crate) fn source_for(address: &AmqpAddress) -> Source {
let mut source = Source::builder().address(address.address()).build();
if let Some(capability) = address.capability() {
source.capabilities = Some(Array::from(vec![Symbol::from(capability)]));
}
source
}
impl ConnectedBroker for ConnectedAmqpBroker {
type Error = AmqpError;
type Closed = ();
async fn shutdown(self) -> Result<(), Self::Error> {
let core = match self.link {
Link::Amqp(core) => core,
#[cfg(feature = "testing")]
Link::InProcess(bus) => {
bus.close();
return Ok(());
}
};
core.closed.store(true, Ordering::Release);
let sessions_ended = core.pumps.stop_all().await;
let senders_result = core.close_senders().await;
let session_result = {
let mut session = core.session.lock().await;
session.end().await
};
let conn_result = core.close_connection(sessions_ended).await;
senders_result?;
session_result.map_err(|e| AmqpError::Session(box_err(e)))?;
conn_result.map_err(|e| AmqpError::Connect(box_err(e)))?;
Ok(())
}
}
impl Subscribe for ConnectedAmqpBroker {
type Subscriber = AmqpSubscriber;
type Copies = AddressedCopies;
async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
self.subscribe_address(AmqpAddress::raw(name)).await
}
}
impl DefaultPublish for ConnectedAmqpBroker {
type Policy = AmqpPublish;
}
#[cfg(feature = "testing")]
impl TestableBroker for ConnectedAmqpBroker {
fn install_coordinator(&self, coordinator: Coordinator) {
if let Link::InProcess(bus) = &self.link {
bus.install(coordinator);
}
}
fn inject(&self, message: OutgoingMessage<'_>) {
if let Err(err) = in_process::inject(self.bus("inject"), &message) {
panic!(
"the injected message to {:?} is not one the broker takes: {err}",
message.name()
);
}
}
fn published(&self, name: &str) -> Vec<RawMessage> {
self.bus("published").published(name)
}
fn routes(&self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
let same_name = subscriptions
.iter()
.enumerate()
.filter(|(_, name)| **name == destination)
.map(|(position, _)| position);
let termini = match &self.link {
Link::InProcess(bus) => {
let recipients = bus.recipients(destination);
if recipients.len() == same_name.clone().count() {
return same_name
.zip(recipients)
.filter_map(|(position, reached)| reached.then_some(position))
.collect();
}
let (anycast, multicast) = bus.termini(destination);
Some(Termini { anycast, multicast })
}
Link::Amqp(_) => self
.termini
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(destination)
.copied(),
};
match termini {
Some(Termini { anycast, multicast }) => {
same_name.take(multicast + anycast.min(1)).collect()
}
None => same_name.collect(),
}
}
}
#[cfg(feature = "testing")]
impl ConnectedAmqpBroker {
fn opened(&self, address: String, routing: Routing) {
let mut termini = self.termini.lock().unwrap_or_else(PoisonError::into_inner);
let entry = termini.entry(address).or_default();
match routing {
Routing::Anycast => entry.anycast += 1,
Routing::Multicast => entry.multicast += 1,
}
drop(termini);
}
fn bus(&self, what: &str) -> &Bus {
match &self.link {
Link::InProcess(bus) => bus,
Link::Amqp(_) => panic!(
"TestableBroker::{what} reached a broker connected with `connect`; the harness \
drives the transport `connect_in_process` produces"
),
}
}
}
pub(crate) fn is_at_most_once(settle: Settle) -> bool {
matches!(settle, Settle::AtMostOnce)
}
#[cfg(test)]
mod tests {
use super::{AmqpBroker, DescribeServer};
#[test]
fn a_url_carrying_credentials_describes_a_server_without_them() {
let spec = AmqpBroker::new("amqp://artemis:artemis@broker.example.com:5672/prod")
.describe_server();
assert_eq!(spec.host.as_deref(), Some("broker.example.com:5672"));
assert_eq!(spec.protocol, "amqp1");
assert_eq!(spec.protocol_version.as_deref(), Some("1.0"));
let host = spec.host.expect("a networked broker describes a host");
assert!(!host.contains("artemis"), "the description leaked {host:?}");
assert!(!host.contains('@'), "the description leaked {host:?}");
}
}