use std::io::BufRead;
use felix_client::AckMode;
use crate::cli::{AckArg, PubArgs};
use crate::connect::Broker;
use crate::context::Settings;
use crate::error::{Exit, MarkExit};
use crate::output::Output;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Source {
Literal(Vec<u8>),
File(std::path::PathBuf),
StdinLines,
StdinWhole,
}
impl Source {
pub(crate) fn of(args: &PubArgs) -> Self {
if let Some(data) = &args.data {
Source::Literal(data.as_bytes().to_vec())
} else if let Some(file) = &args.file {
Source::File(file.clone())
} else if args.whole {
Source::StdinWhole
} else {
Source::StdinLines
}
}
}
pub(crate) async fn run(args: &PubArgs, settings: &Settings, out: &Output) -> anyhow::Result<()> {
let broker = Broker::connect(settings).await?;
let mut sender = Sender::new(&broker, args).await?;
match Source::of(args) {
Source::Literal(bytes) => sender.send_repeated(bytes).await?,
Source::File(path) => {
let bytes = tokio::fs::read(&path)
.await
.mark(Exit::Usage, format!("read {}", path.display()))?;
sender.send_repeated(bytes).await?;
}
Source::StdinWhole => {
let mut bytes = Vec::new();
std::io::Read::read_to_end(&mut std::io::stdin().lock(), &mut bytes)
.mark(Exit::Usage, "read stdin")?;
sender.send_repeated(bytes).await?;
}
Source::StdinLines => {
let (tx, mut rx) = tokio::sync::mpsc::channel::<std::io::Result<Vec<u8>>>(64);
std::thread::spawn(move || {
for line in std::io::stdin().lock().split(b'\n') {
let line = line.map(|mut bytes| {
if bytes.last() == Some(&b'\r') {
bytes.pop();
}
bytes
});
if tx.blocking_send(line).is_err() {
break;
}
}
});
while let Some(line) = rx.recv().await {
sender
.send_repeated(line.mark(Exit::Usage, "read stdin")?)
.await?;
}
}
}
sender.finish().await?;
let stream = &args.stream;
let text = summary(
stream,
&sender.offsets,
matches!(sender.mode, Mode::Plain(AckMode::None)),
);
out.done(
&text,
serde_json::json!({
"stream": stream,
"published": sender.offsets.len(),
"offsets": sender.offsets,
}),
)
}
fn summary(stream: &str, offsets: &[Option<u64>], unacked: bool) -> String {
let published = offsets.len();
let first = offsets.iter().flatten().min().copied();
let last = offsets.iter().flatten().max().copied();
let mut text = match (first, last) {
(Some(first), Some(last)) if first == last => {
format!("published {published} to {stream} at offset {first}")
}
(Some(first), Some(last)) => {
format!("published {published} to {stream}, offsets {first}..={last}")
}
_ => format!("published {published} to {stream}"),
};
let missing = offsets.iter().filter(|offset| offset.is_none()).count();
if !unacked && missing > 0 {
text.push_str(&format!(
"; {missing} without an offset (acknowledged before the write, or the stream has no log)"
));
}
text
}
struct Sender<'a> {
broker: &'a Broker,
stream: String,
key: Option<bytes::Bytes>,
count: u64,
mode: Mode,
offsets: Vec<Option<u64>>,
}
enum Mode {
Plain(AckMode),
Idempotent(Box<felix_client::IdempotentProducer>),
}
impl<'a> Sender<'a> {
async fn new(broker: &'a Broker, args: &PubArgs) -> anyhow::Result<Self> {
let mode = if args.idempotent {
Mode::Idempotent(Box::new(broker.cluster.idempotent_producer().await?))
} else if args.ack == AckArg::None {
Mode::Plain(AckMode::None)
} else {
Mode::Plain(AckMode::PerMessage)
};
Ok(Self {
broker,
stream: args.stream.clone(),
key: args.key.clone().map(bytes::Bytes::from),
count: args.count,
mode,
offsets: Vec::new(),
})
}
async fn send_repeated(&mut self, payload: Vec<u8>) -> anyhow::Result<()> {
for _ in 0..self.count {
let offset = self.send(payload.clone()).await?;
self.offsets.push(offset);
}
Ok(())
}
async fn send(&self, payload: Vec<u8>) -> anyhow::Result<Option<u64>> {
let (tenant, namespace, stream) = (
self.broker.tenant.as_str(),
self.broker.namespace.as_str(),
self.stream.as_str(),
);
let cluster = &self.broker.cluster;
match (&self.mode, &self.key) {
(Mode::Plain(ack), None) => {
cluster
.publish(tenant, namespace, stream, payload, *ack)
.await
}
(Mode::Plain(ack), Some(key)) => {
cluster
.publish_keyed(tenant, namespace, stream, payload, key.clone(), *ack)
.await
}
(Mode::Idempotent(producer), None) => {
producer.publish(tenant, namespace, stream, payload).await
}
(Mode::Idempotent(producer), Some(key)) => {
producer
.publish_keyed(tenant, namespace, stream, key.clone(), payload)
.await
}
}
}
async fn finish(&self) -> anyhow::Result<()> {
if let Mode::Plain(AckMode::None) = self.mode {
self.broker.cluster.finish().await?;
}
Ok(())
}
}
#[cfg(test)]
mod tests;