use photon_backend::shard_router::shard_id;
use crate::config::NatsConfig;
pub const SEQ_STRIDE: i64 = 1_000_000_000_000;
pub const MAX_STREAM_SHARDS: u32 = 256;
pub const STREAM_SHARDS_ENV: &str = "PHOTON_NATS_STREAM_SHARDS";
#[must_use]
pub fn pick_shard(routing_key: &str, shard_count: u32) -> u32 {
shard_id(routing_key, shard_count.max(1))
}
#[must_use]
pub fn publish_routing_key(topic_key: Option<&str>, event_id: &str) -> String {
topic_key.map_or_else(|| event_id.to_string(), str::to_string)
}
#[must_use]
pub fn stream_name_for(config: &NatsConfig, shard: u32) -> String {
if config.stream_shards <= 1 {
config.stream_name.clone()
} else {
format!("{}-{}", config.stream_name, shard)
}
}
#[must_use]
pub fn stream_subject_wildcard(shard: u32, shard_count: u32) -> String {
if shard_count <= 1 {
crate::subject::STREAM_SUBJECTS.into()
} else {
format!("{}.{shard}.>", crate::subject::SHARDED_SUBJECT_PREFIX)
}
}
#[must_use]
pub fn photon_subject_for(shard: u32, shard_count: u32, topic_name: &str) -> String {
if shard_count <= 1 {
crate::subject::photon_subject(topic_name)
} else {
format!(
"{}.{shard}.{topic_name}",
crate::subject::SHARDED_SUBJECT_PREFIX
)
}
}
#[must_use]
pub fn composite_seq(shard: u32, local_seq: u64) -> i64 {
i64::from(shard)
.saturating_mul(SEQ_STRIDE)
.saturating_add(i64::try_from(local_seq).unwrap_or(i64::MAX))
}
#[must_use]
pub fn decompose_seq(composite: i64) -> (u32, i64) {
if composite <= 0 {
return (0, 0);
}
let shard = u32::try_from(composite / SEQ_STRIDE).unwrap_or(0);
let local = composite % SEQ_STRIDE;
(shard, local)
}
#[must_use]
pub fn local_after_seq_for_shard(shard: u32, after_seq: Option<i64>) -> Option<i64> {
if after_seq == Some(0) {
return Some(0);
}
let composite = after_seq.filter(|&s| s > 0)?;
let (seq_shard, local) = decompose_seq(composite);
match seq_shard.cmp(&shard) {
std::cmp::Ordering::Equal => Some(local),
std::cmp::Ordering::Greater => Some(0),
std::cmp::Ordering::Less => None,
}
}
#[must_use]
pub fn stream_shards_from_env() -> u32 {
std::env::var(STREAM_SHARDS_ENV)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1)
.clamp(1, MAX_STREAM_SHARDS)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pick_shard_is_stable() {
assert_eq!(pick_shard("user-1", 4), pick_shard("user-1", 4));
}
#[test]
fn composite_roundtrip() {
let c = composite_seq(3, 42);
assert_eq!(decompose_seq(c), (3, 42));
}
#[test]
fn local_after_zero_replays_from_start() {
assert_eq!(local_after_seq_for_shard(2, Some(0)), Some(0));
}
#[test]
fn legacy_subject_when_single_shard() {
assert_eq!(photon_subject_for(0, 1, "orders"), "photon.orders");
}
#[test]
fn sharded_subject_layout() {
assert_eq!(photon_subject_for(2, 4, "orders"), "photon-s.2.orders");
assert_eq!(stream_subject_wildcard(2, 4), "photon-s.2.>");
}
}