#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::future::{Future, ready};
use std::io;
use std::num::NonZeroUsize;
use std::pin::pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use futures::{FutureExt as _, Stream};
#[cfg(feature = "testing")]
use ruststream::RawMessage;
#[cfg(feature = "testing")]
use ruststream::testing::{Coordinator, InProcess, TestableBroker};
use ruststream::{
BatchSubscriber, Broker, BufferedSubscriber, ConnectedBroker, DefaultPublish, DescribeServer,
Lend, NamedCopies, OutgoingMessage, PairError, PublishPolicy, Publisher, ServerSpec, Subscribe,
Subscriber,
};
use sea_streamer_stdio::{
StdioConnectOptions, StdioConsumer, StdioConsumerOptions, StdioProducer, StdioProducerOptions,
StdioResult, StdioStreamer,
};
use sea_streamer_types::{
Consumer as _, ConsumerMode, ConsumerOptions as _, Producer as _, StreamKey, Streamer as _,
StreamerUri,
};
use tokio::runtime::Handle;
use tokio::sync::{OnceCell, mpsc};
use crate::batching::BATCH_MAX_WAIT;
use crate::error::{SeaFileError, box_err};
#[cfg(feature = "testing")]
use crate::in_process::{MemoryPipe, PipeQueue};
use crate::message::SeaMessage;
use crate::wire;
static READER_STARTED: AtomicBool = AtomicBool::new(false);
pub(crate) struct StdioCore {
pub(crate) link: StdioLink,
pub(crate) closed: AtomicBool,
#[cfg(feature = "testing")]
loopback: bool,
}
pub(crate) enum StdioLink {
Stdio(Pipes),
#[cfg(feature = "testing")]
InProcess(Arc<MemoryPipe>),
}
pub(crate) struct Pipes {
streamer: StdioStreamer,
runtime: Handle,
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<StdioLink>() == size_of::<Pipes>());
impl StdioCore {
fn ensure_open(&self) -> Result<(), SeaFileError> {
if self.closed.load(Ordering::Acquire) {
return Err(SeaFileError::NotConnected);
}
Ok(())
}
}
impl std::fmt::Debug for StdioCore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StdioCore")
.field("closed", &self.closed.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
type StdioCell = Arc<OnceCell<Arc<StdioCore>>>;
#[derive(Debug, Clone, Default)]
#[must_use]
pub struct StdioBroker {
loopback: bool,
cell: StdioCell,
}
impl StdioBroker {
pub fn new() -> Self {
Self::default()
}
pub fn loopback(mut self) -> Self {
self.loopback = true;
self
}
#[must_use]
pub fn publisher(&self) -> StdioPublisher {
StdioPublisher {
cell: Arc::clone(&self.cell),
producer: Arc::new(OnceCell::new()),
}
}
}
impl Broker for StdioBroker {
type Error = SeaFileError;
type Connected = ConnectedStdioBroker;
async fn connect(self) -> Result<Self::Connected, Self::Error> {
let core = self
.cell
.get_or_try_init(async || {
let mut options = StdioConnectOptions::default();
options.set_loopback(self.loopback);
let streamer = StdioStreamer::connect(StreamerUri::zero(), options)
.await
.map_err(|e| SeaFileError::Connect {
target: "stdio".to_owned(),
source: box_err(e),
})?;
Ok::<_, SeaFileError>(Arc::new(StdioCore {
link: StdioLink::Stdio(Pipes {
streamer,
runtime: Handle::current(),
}),
closed: AtomicBool::new(false),
#[cfg(feature = "testing")]
loopback: self.loopback,
}))
})
.await?
.clone();
Ok(ConnectedStdioBroker {
core,
cell: self.cell,
})
}
}
#[cfg(feature = "testing")]
impl InProcess for StdioBroker {
async fn connect_in_process(self) -> Result<Self::Connected, Self::Error> {
let core = self
.cell
.get_or_init(async || {
Arc::new(StdioCore {
link: StdioLink::InProcess(MemoryPipe::new(self.loopback)),
closed: AtomicBool::new(false),
loopback: self.loopback,
})
})
.await
.clone();
Ok(ConnectedStdioBroker {
core,
cell: self.cell,
})
}
}
#[cfg(feature = "testing")]
ruststream::register_testable_broker!(StdioBroker);
async fn open_consumer(streamer: &StdioStreamer, key: StreamKey) -> StdioResult<StdioConsumer> {
let keys = [key];
let mut created =
pin!(streamer.create_consumer(&keys, StdioConsumerOptions::new(ConsumerMode::RealTime)));
if READER_STARTED.swap(true, Ordering::AcqRel) {
return created.await;
}
let first = {
let _input = io::stdin().lock();
created.as_mut().now_or_never()
};
match first {
Some(consumer) => consumer,
None => created.await,
}
}
impl DescribeServer for StdioBroker {
fn describe_server(&self) -> ServerSpec {
ServerSpec::in_process("stdio")
}
}
#[derive(Debug)]
pub struct ConnectedStdioBroker {
core: Arc<StdioCore>,
cell: StdioCell,
}
impl ConnectedStdioBroker {
#[must_use]
pub fn publisher(&self) -> StdioPublisher {
StdioPublisher {
cell: Arc::clone(&self.cell),
producer: Arc::new(OnceCell::new()),
}
}
}
impl ConnectedBroker for ConnectedStdioBroker {
type Error = SeaFileError;
type Closed = ();
async fn shutdown(self) -> Result<(), Self::Error> {
self.core.closed.store(true, Ordering::Release);
let streamer = match &self.core.link {
StdioLink::Stdio(pipes) => &pipes.streamer,
#[cfg(feature = "testing")]
StdioLink::InProcess(pipe) => {
pipe.finish();
return Ok(());
}
};
streamer
.clone()
.disconnect()
.await
.map_err(|e| SeaFileError::Connect {
target: "stdio".to_owned(),
source: box_err(e),
})
}
}
impl Subscribe for ConnectedStdioBroker {
type Subscriber = StdioSubscriber;
type Copies = NamedCopies;
async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
self.core.ensure_open()?;
let key =
StreamKey::new(name).map_err(|e| SeaFileError::Invalid(format!("'{name}': {e}")))?;
let pipes = match &self.core.link {
StdioLink::Stdio(pipes) => pipes,
#[cfg(feature = "testing")]
StdioLink::InProcess(pipe) => {
return Ok(StdioSubscriber {
stream: name.to_owned(),
inner: BufferedSubscriber::new(StdioDeliveries::InProcess(
pipe.subscribe(key.name()),
))
.max_wait(BATCH_MAX_WAIT),
});
}
};
let consumer =
open_consumer(&pipes.streamer, key)
.await
.map_err(|e| SeaFileError::Subscribe {
stream: name.to_owned(),
source: box_err(e),
})?;
let (tx, rx) = mpsc::channel(64);
pipes.runtime.spawn(async move {
loop {
tokio::select! {
() = tx.closed() => break,
next = consumer.next() => match next {
Ok(message) => {
if tx.send(Ok(SeaMessage::new(&message))).await.is_err() {
break;
}
}
Err(_) => break,
},
}
}
});
Ok(StdioSubscriber {
stream: name.to_owned(),
inner: BufferedSubscriber::new(StdioDeliveries::Reader(rx)).max_wait(BATCH_MAX_WAIT),
})
}
}
impl DefaultPublish for ConnectedStdioBroker {
type Policy = StdioPublish;
}
#[cfg(feature = "testing")]
impl TestableBroker for ConnectedStdioBroker {
fn install_coordinator(&self, coordinator: Coordinator) {
if let StdioLink::InProcess(pipe) = &self.core.link {
pipe.install(coordinator);
}
}
fn inject(&self, message: OutgoingMessage<'_>) {
let pipe = self.memory_pipe("inject");
let key = StreamKey::new(message.name()).unwrap_or_else(|err| {
panic!(
"the injected message to {:?} is not one a pipe carries: {err}",
message.name()
)
});
assert!(
!(message.payload().is_empty() && message.headers().is_empty()),
"the injected message to {:?} is empty, and a pipe drops an empty line",
message.name(),
);
pipe.feed(
key,
wire::encode(message.headers(), message.payload(), true),
);
}
fn published(&self, name: &str) -> Vec<RawMessage> {
self.memory_pipe("published").published(name)
}
fn routes(&self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
if !self.core.loopback {
return Vec::new();
}
subscriptions
.iter()
.enumerate()
.filter(|(_, name)| **name == destination)
.map(|(position, _)| position)
.collect()
}
}
#[cfg(feature = "testing")]
impl ConnectedStdioBroker {
fn memory_pipe(&self, what: &str) -> &MemoryPipe {
match &self.core.link {
StdioLink::InProcess(pipe) => pipe,
StdioLink::Stdio(_) => panic!(
"TestableBroker::{what} reached a broker connected with `connect`; the harness \
drives the pipe `connect_in_process` produces"
),
}
}
}
pub struct StdioSubscriber {
stream: String,
inner: BufferedSubscriber<StdioDeliveries>,
}
impl std::fmt::Debug for StdioSubscriber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StdioSubscriber")
.field("stream", &self.stream)
.finish_non_exhaustive()
}
}
impl Subscriber for StdioSubscriber {
type Message = SeaMessage;
type Error = SeaFileError;
fn stream(&mut self) -> impl Stream<Item = Result<SeaMessage, SeaFileError>> + Send + '_ {
self.inner.stream()
}
}
impl BatchSubscriber for StdioSubscriber {
type Batch = Vec<SeaMessage>;
fn batches(
&mut self,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Self::Batch, SeaFileError>> + Send + '_ {
self.inner.batches(size)
}
}
enum StdioDeliveries {
Reader(mpsc::Receiver<Result<SeaMessage, SeaFileError>>),
#[cfg(feature = "testing")]
InProcess(PipeQueue),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(
size_of::<StdioDeliveries>() == size_of::<mpsc::Receiver<Result<SeaMessage, SeaFileError>>>()
);
impl Subscriber for StdioDeliveries {
type Message = SeaMessage;
type Error = SeaFileError;
fn stream(&mut self) -> impl Stream<Item = Result<SeaMessage, SeaFileError>> + Send + '_ {
futures::stream::poll_fn(move |cx| match self {
Self::Reader(rx) => rx.poll_recv(cx),
#[cfg(feature = "testing")]
Self::InProcess(queue) => queue.poll_next(cx).map(|next| next.map(Ok)),
})
}
}
#[derive(Clone)]
pub struct StdioPublisher {
cell: StdioCell,
producer: Arc<OnceCell<StdioProducer>>,
}
impl std::fmt::Debug for StdioPublisher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StdioPublisher").finish_non_exhaustive()
}
}
impl Publisher for StdioPublisher {
type Payload = Lend;
type Error = SeaFileError;
type Options = ();
async fn publish(
&self,
msg: OutgoingMessage<'_, &[u8]>,
_options: Option<&()>,
) -> Result<(), Self::Error> {
let core = self.cell.get().ok_or(SeaFileError::NotConnected)?;
core.ensure_open()?;
if msg.payload().is_empty() && msg.headers().is_empty() {
return Err(SeaFileError::Invalid(
"stdio drops empty lines; an empty message cannot be transmitted".into(),
));
}
let streamer = match &core.link {
StdioLink::Stdio(pipes) => &pipes.streamer,
#[cfg(feature = "testing")]
StdioLink::InProcess(pipe) => {
let key = StreamKey::new(msg.name())
.map_err(|e| SeaFileError::Invalid(format!("'{}': {e}", msg.name())))?;
pipe.write(key, wire::encode(msg.headers(), msg.payload(), true));
return Ok(());
}
};
let producer = self
.producer
.get_or_try_init(async || {
streamer
.create_generic_producer(StdioProducerOptions::default())
.await
.map_err(|e| SeaFileError::Publish {
stream: msg.name().to_owned(),
source: box_err(e),
})
})
.await?;
let key = StreamKey::new(msg.name())
.map_err(|e| SeaFileError::Invalid(format!("'{}': {e}", msg.name())))?;
let payload = wire::encode(msg.headers(), msg.payload(), true);
producer
.send_to(&key, payload.as_slice())
.map_err(|e| SeaFileError::Publish {
stream: msg.name().to_owned(),
source: box_err(e),
})?
.await
.map(|_| ())
.map_err(|e| SeaFileError::Publish {
stream: msg.name().to_owned(),
source: box_err(e),
})
}
}
#[derive(Debug, Clone, Copy, Default)]
#[must_use]
pub struct StdioPublish;
impl PublishPolicy<ConnectedStdioBroker> for StdioPublish {
type Live = StdioPublisher;
fn pair(
self,
connected: &ConnectedStdioBroker,
) -> impl Future<Output = Result<Self::Live, PairError>> {
ready(Ok(connected.publisher()))
}
}
pub use StdioPublish as Publish;
pub mod prelude {
pub use ruststream::prelude::*;
pub use crate::stdio::{Publish, StdioBroker};
}