#![cfg(feature = "testing")]
use std::pin::pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use futures::{Stream, StreamExt};
use ruststream::testing::InProcess;
use ruststream::{
Broker, ConnectedBroker, IncomingMessage, OutgoingMessage, Publisher, Subscriber,
};
use ruststream_fred::{
ConnectedRedisBroker, RedisBroker, RedisError, RedisPubSub, RedisPubSubPublish, RedisStream,
};
use tokio::runtime::Builder;
use tokio::sync::oneshot;
use tokio::time::timeout;
mod live;
const URL: &str = "redis://localhost:6379";
const WAIT: Duration = Duration::from_secs(5);
const MIN_IDLE: Duration = Duration::from_millis(200);
fn unique(base: &str) -> String {
static RUN: OnceLock<u128> = OnceLock::new();
static N: AtomicU64 = AtomicU64::new(0);
let run = RUN.get_or_init(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("a clock after the epoch")
.as_nanos()
});
format!(
"ruststream-rt.{base}.{run}.{}",
N.fetch_add(1, Ordering::Relaxed)
)
}
async fn on_foreign_runtime<Work, Fut, Output>(work: Work) -> Output
where
Work: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Output>,
Output: Send + 'static,
{
let (done, result) = oneshot::channel();
thread::spawn(move || {
let runtime = Builder::new_current_thread()
.enable_all()
.build()
.expect("a current-thread runtime builds");
let output = runtime.block_on(work());
drop(runtime);
let _ = done.send(output);
});
result.await.expect("the foreign runtime's work panicked")
}
async fn off_any_runtime(work: impl FnOnce() + Send + 'static) {
let (done, finished) = oneshot::channel();
thread::spawn(move || {
work();
let _ = done.send(());
});
finished
.await
.expect("the work outside any runtime panicked");
}
async fn in_process() -> ConnectedRedisBroker {
RedisBroker::standalone(URL)
.connect_in_process()
.await
.expect("connect in process")
}
async fn next<S, M>(stream: &mut S) -> M
where
S: Stream<Item = Result<M, RedisError>> + Unpin,
{
timeout(WAIT, stream.next())
.await
.expect("a delivery within the wait")
.expect("the stream is open")
.expect("a delivery, not an error")
}
async fn publish_entries(broker: &ConnectedRedisBroker, key: &str, count: usize) {
let publisher = broker.publisher();
for _ in 0..count {
publisher
.publish(OutgoingMessage::new(key, b"entry".as_slice()), None)
.await
.expect("publish");
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_claiming_subscription_opened_on_another_runtime_claims_what_comes_due() {
let broker = Arc::new(in_process().await);
let key = unique("claim");
let mut first = broker
.subscribe(
RedisStream::new(key.as_str())
.group("workers")
.consumer("first"),
)
.await
.expect("subscribe");
publish_entries(&broker, &key, 1).await;
{
let mut stream = pin!(first.stream());
drop(next(&mut stream).await);
}
let opener = Arc::clone(&broker);
let def = RedisStream::claiming(key.as_str(), MIN_IDLE)
.group("workers")
.consumer("second");
let mut second =
on_foreign_runtime(async move || opener.subscribe(def).await.expect("subscribe")).await;
let claimed = {
let mut stream = pin!(second.stream());
next(&mut stream).await
};
assert_eq!(claimed.payload(), b"entry");
claimed.ack().await.expect("ack");
drop((first, second));
let broker = Arc::into_inner(broker).expect("the opener let go of the broker");
broker.shutdown().await.expect("shutdown");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_pubsub_subscription_dropped_off_any_runtime_closes_its_connection() {
let broker = in_process().await;
let subscriber = broker
.subscribe_pubsub(RedisPubSub::new(unique("drop")))
.await
.expect("subscribe");
off_any_runtime(move || drop(subscriber)).await;
broker.shutdown().await.expect("shutdown");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_pubsub_subscription_opened_on_another_runtime_keeps_its_connection() {
let Some(url) = live::url("REDIS_TEST_URL") else {
return;
};
let broker = RedisBroker::standalone(url)
.connect()
.await
.expect("connect");
let channel = unique("open");
let broker = Arc::new(broker);
let opener = Arc::clone(&broker);
let def = RedisPubSub::new(channel.as_str());
let mut subscriber =
on_foreign_runtime(async move || opener.subscribe_pubsub(def).await.expect("subscribe"))
.await;
broker
.pubsub_publisher(RedisPubSubPublish::new())
.publish(OutgoingMessage::new(&channel, b"after".as_slice()), None)
.await
.expect("publish");
let message = {
let mut stream = pin!(subscriber.stream());
next(&mut stream).await
};
assert_eq!(message.payload(), b"after");
drop(subscriber);
let broker = Arc::into_inner(broker).expect("the opener let go of the broker");
broker.shutdown().await.expect("shutdown");
}