#![cfg(feature = "avro")]
use std::convert::Infallible;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use ruststream::runtime::{App, AppInfo, HandlerResult, RustStream, State, TypedPublisher};
use ruststream::{Broker, FromRef, OutgoingMessage, Publisher, subscriber};
use ruststream_rdkafka::{
KafkaBroker, KafkaTopic, SchemaFrame, SchemaRegistry, SchemaType, StartOffset,
};
use serde::{Deserialize, Serialize};
use tokio::sync::Notify;
const FRAMED_TOPIC: &str = "avro-mw-frames-placeholder";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Order {
id: i64,
item: String,
}
#[derive(Clone)]
struct AvroProbe {
base: i64,
expected: usize,
seen: Arc<Mutex<Vec<Order>>>,
done: Arc<Notify>,
}
#[derive(FromRef)]
struct AvroApp {
probe: AvroProbe,
}
#[subscriber(
KafkaTopic::new(std::env::var("AVRO_MW_TRIGGER").expect("trigger env"))
.group(std::env::var("AVRO_MW_TRIGGER_GROUP").expect("trigger group env"))
.start(StartOffset::Earliest),
publish("avro-mw-frames-placeholder")
)]
async fn avro_relay(order: &Order) -> Order {
order.clone()
}
#[subscriber(
KafkaTopic::new("avro-mw-frames-placeholder")
.group(std::env::var("AVRO_MW_GROUP").expect("group env"))
.start(StartOffset::Earliest)
)]
async fn avro_mw(order: &Order, State(probe): State<AvroProbe>) -> HandlerResult {
let marker_range = probe.base..probe.base + i64::try_from(probe.expected).expect("small");
if !marker_range.contains(&order.id) {
return HandlerResult::Ack; }
{
let mut seen = probe.seen.lock().expect("seen mutex poisoned");
seen.push(order.clone());
if seen.len() < probe.expected {
return HandlerResult::Ack;
}
}
probe.done.notify_waiters();
HandlerResult::Ack
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn live_avro_middleware_end_to_end() {
const COUNT: usize = 3;
static NEXT: AtomicU64 = AtomicU64::new(0);
let Some(registry) = std::env::var("SCHEMA_REGISTRY_TEST_URL").ok() else {
return;
};
let Some(kafka) = std::env::var("KAFKA_TEST_URL").ok() else {
return;
};
let run = format!(
"{}x{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
);
let base = i64::from(std::process::id()) * 100_000;
let trigger = format!("avro-mw-trigger-{run}");
unsafe {
std::env::set_var("AVRO_MW_TRIGGER", &trigger);
std::env::set_var("AVRO_MW_TRIGGER_GROUP", format!("trigger-group-{run}"));
std::env::set_var("AVRO_MW_GROUP", format!("group-{run}"));
}
let subject = format!("avro-mw-{run}");
let schema = format!(
r#"{{"type":"record","name":"Order{base}","fields":[
{{"name":"id","type":"long"}},{{"name":"item","type":"string"}}]}}"#,
);
let sr = SchemaRegistry::new(®istry);
let id = sr
.register(&subject, SchemaType::Avro, schema)
.await
.expect("register");
let seed_broker = KafkaBroker::new([kafka.clone()]);
Broker::connect(&seed_broker).await.expect("connect seed");
let count = i64::try_from(COUNT).expect("small count");
for n in 0..count {
let json = format!(r#"{{"id":{},"item":"item-{n}"}}"#, base + n);
seed_broker
.publisher()
.publish(OutgoingMessage::new(&trigger, json.as_bytes()))
.await
.expect("seed trigger");
}
Broker::shutdown(&seed_broker).await.expect("seed shutdown");
let consumer_sr = SchemaRegistry::new(®istry);
let probe = AvroProbe {
base,
expected: COUNT,
seen: Arc::new(Mutex::new(Vec::new())),
done: Arc::new(Notify::new()),
};
let app_probe = probe.clone();
let broker = KafkaBroker::new([kafka]).schema_registry(consumer_sr.clone());
let replies = TypedPublisher::new(broker.publisher());
let app = RustStream::new(AppInfo::new("avro-mw", "0.0.0"))
.publish_layer(
SchemaFrame::new(SchemaRegistry::new(®istry)).subject(FRAMED_TOPIC, &subject),
)
.on_startup(async move |()| Ok::<_, Infallible>(AvroApp { probe: app_probe }))
.with_broker(broker, |b| {
b.include(avro_mw);
b.include_publishing(avro_relay, replies);
});
let done = Arc::clone(&probe.done);
let wait = async move {
tokio::time::timeout(std::time::Duration::from_secs(20), done.notified())
.await
.expect("all deliveries within timeout");
};
App::run_until(app, wait).await.expect("run");
let mut seen = probe.seen.lock().expect("seen mutex poisoned").clone();
seen.sort_by_key(|order| order.id);
let expected: Vec<Order> = (0..count)
.map(|n| Order {
id: base + n,
item: format!("item-{n}"),
})
.collect();
assert_eq!(
seen, expected,
"Avro on the wire must arrive as JSON in plain handlers",
);
assert!(
consumer_sr.cached_schema(id).is_some(),
"the consumer transcoded through this run's schema id, so the layer really framed",
);
}