use core::time::Duration;
use anyhow::Context as _;
use async_nats::jetstream;
use crate::partition::PARTITIONS;
pub const RAW_PREFIX: &str = "events.raw.p";
pub const MATCHED_PREFIX: &str = "events.matched.p";
pub const MATCHED_STREAM: &str = "EVENTS-MATCHED";
pub fn raw_subject(partition: u64) -> String {
format!("{RAW_PREFIX}.{partition}")
}
pub fn matched_subject(partition: u64) -> String {
format!("{MATCHED_PREFIX}.{partition}")
}
pub fn stream_index(partition: u64, streams: u64) -> u64 {
partition / PARTITIONS.div_ceil(streams)
}
pub fn stream_name(index: u64) -> String {
format!("EVENTS-RAW-{index}")
}
pub async fn raw_stream(
context: &jetstream::Context,
index: u64,
streams: u64,
) -> anyhow::Result<jetstream::stream::Stream> {
let chunk = PARTITIONS.div_ceil(streams);
let partitions = (index * chunk)..(((index + 1) * chunk).min(PARTITIONS));
context
.get_or_create_stream(jetstream::stream::Config {
name: stream_name(index),
subjects: partitions.map(raw_subject).collect(),
retention: jetstream::stream::RetentionPolicy::WorkQueue,
storage: jetstream::stream::StorageType::File,
..Default::default()
})
.await
.with_context(|| format!("could not create raw stream {index}"))
}
pub async fn matched_stream(
context: &jetstream::Context,
max_age: Duration,
) -> anyhow::Result<jetstream::stream::Stream> {
context
.get_or_create_stream(jetstream::stream::Config {
name: MATCHED_STREAM.into(),
subjects: vec![format!("{MATCHED_PREFIX}.>")],
retention: jetstream::stream::RetentionPolicy::Limits,
storage: jetstream::stream::StorageType::File,
max_age,
..Default::default()
})
.await
.context("could not create matched stream")
}
pub async fn partition_consumer(
stream: &jetstream::stream::Stream,
partition: u64,
max_ack_pending: i64,
ack_wait: Duration,
) -> anyhow::Result<jetstream::consumer::PullConsumer> {
let name = format!("orchestrator-p{partition}");
stream
.get_or_create_consumer(
&name,
jetstream::consumer::pull::Config {
durable_name: Some(name.clone()),
filter_subject: raw_subject(partition),
ack_policy: jetstream::consumer::AckPolicy::Explicit,
max_ack_pending,
ack_wait,
..Default::default()
},
)
.await
.with_context(|| format!("could not create consumer for partition {partition}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn streams_partition_the_partition_space() {
for streams in [1u64, 2, 3, 4, 8, 32, 1024] {
let mut last = 0;
for partition in 0..PARTITIONS {
let index = stream_index(partition, streams);
assert!(index < streams, "index {index} escapes {streams} streams");
assert!(index >= last, "stream blocks must be contiguous");
last = index;
}
}
}
#[test]
fn subjects_are_distinct_per_partition() {
assert_eq!(raw_subject(0), "events.raw.p.0");
assert_eq!(raw_subject(1023), "events.raw.p.1023");
assert_eq!(matched_subject(485), "events.matched.p.485");
}
}