use crate::cluster::NodeId;
const SEP: char = ';';
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DispatchRecord {
pub node: NodeId,
pub reply_to: Option<String>,
pub pinned: bool,
}
impl DispatchRecord {
pub fn new(node: NodeId) -> Self {
DispatchRecord { node, reply_to: None, pinned: false }
}
pub fn parse(v: &str) -> Option<Self> {
let v = v.trim();
if v.is_empty() {
return None;
}
let mut it = v.split(SEP);
let node: NodeId = it.next()?.trim().parse().ok()?;
if node == 0 {
return None;
}
let mut r = DispatchRecord::new(node);
for f in it {
let f = f.trim();
if let Some(q) = f.strip_prefix("reply_to=") {
let q = q.trim();
if !q.is_empty() {
r.reply_to = Some(q.to_string());
}
} else if f == "pinned" {
r.pinned = true;
}
}
Some(r)
}
pub fn render(&self) -> String {
let mut s = self.node.to_string();
if let Some(q) = &self.reply_to {
s.push(SEP);
s.push_str("reply_to=");
s.push_str(q);
}
if self.pinned {
s.push(SEP);
s.push_str("pinned");
}
s
}
}
pub fn is_control(key: &str) -> bool {
let n: Vec<&str> = key.trim_end_matches('/').split('/').collect();
if n.len() < 3 {
return false;
}
if !matches!(n.get(1).map(|s| *s), Some("q") | Some("queue")) {
return false;
}
match n.len() {
3 => !n[2].is_empty(),
5 => matches!(n[3], "c" | "consumer") && !n[4].is_empty(),
_ => false,
}
}
pub fn dispatcher_key(fq_name: &str) -> String {
fq_name.to_string()
}
pub fn consumer_key(fq_name: &str, client: &uuid::Uuid) -> String {
format!("{}/c/{}", fq_name, client)
}
pub fn consumer_of(key: &str) -> Option<uuid::Uuid> {
let n: Vec<&str> = key.trim_end_matches('/').split('/').collect();
if n.len() != 5 || !matches!(n.get(1).map(|s| *s), Some("q") | Some("queue")) {
return None;
}
if !matches!(n[3], "c" | "consumer") {
return None;
}
uuid::Uuid::parse_str(n[4]).ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Claim {
Consumer,
Producer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dispatch {
Local,
Remote(NodeId),
Unknown,
}
impl Dispatch {
pub fn of(record: Option<&DispatchRecord>, me: NodeId) -> Self {
match record {
None => Dispatch::Unknown,
Some(r) if r.node == me => Dispatch::Local,
Some(r) => Dispatch::Remote(r.node),
}
}
pub fn is_local(&self) -> bool { matches!(self, Dispatch::Local) }
pub fn is_unknown(&self) -> bool { matches!(self, Dispatch::Unknown) }
}
#[cfg(test)]
pub fn leg_cost(n: usize, producer: NodeId, dispatcher: Option<NodeId>, consumer: NodeId)
-> usize
{
let Some(d) = dispatcher else { return n.saturating_sub(1) };
usize::from(producer != d) + usize::from(d != consumer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_node_id_is_a_valid_record() {
let r = DispatchRecord::parse("7").unwrap();
assert_eq!(r, DispatchRecord { node: 7, reply_to: None, pinned: false });
assert_eq!(r.render(), "7");
}
#[test]
fn the_full_record_round_trips() {
let r = DispatchRecord {
node: 42, reply_to: Some("/q/rpc-reply-a".into()), pinned: true };
assert_eq!(r.render(), "42;reply_to=/q/rpc-reply-a;pinned");
assert_eq!(DispatchRecord::parse(&r.render()).unwrap(), r);
}
#[test]
fn an_older_reader_still_finds_the_node_id() {
let raw = "42;reply_to=/q/x;pinned";
assert_eq!(raw.split(';').next().unwrap().parse::<NodeId>().unwrap(), 42);
}
#[test]
fn an_unknown_field_is_ignored_not_rejected() {
let r = DispatchRecord::parse("9;weight=3;pinned;future=yes").unwrap();
assert_eq!(r.node, 9);
assert!(r.pinned);
}
#[test]
fn junk_is_no_record_rather_than_a_wrong_one() {
for v in ["", " ", "abc", ";", "reply_to=/q/x", "-1", "1.5"] {
assert_eq!(DispatchRecord::parse(v), None, "{:?} parsed", v);
}
assert_eq!(DispatchRecord::parse("0"), None);
assert_eq!(DispatchRecord::parse("0;pinned"), None);
}
#[test]
fn the_registry_keys_are_not_queue_messages() {
assert!(is_control("/q/rpc"));
assert!(is_control("/queue/rpc"));
assert!(is_control("/q/rpc/c/2b8f0a1e-0000-4000-8000-000000000001"));
assert!(is_control("/q/rpc/consumer/2b8f0a1e-0000-4000-8000-000000000001"));
assert!(is_control("/q/rpc/"));
}
#[test]
fn queue_traffic_is_not_control() {
assert!(!is_control("/q/rpc/c/2b8f0a1e-0000-4000-8000-000000000001/7/key"));
assert!(!is_control("/q/rpc/producer/key"));
assert!(!is_control("/q/rpc/p/key"));
assert!(!is_control("/q/rpc/7/key"));
assert!(!is_control("/q/rpc/input/cid/key"));
}
#[test]
fn nothing_outside_the_queue_namespace_is_control() {
assert!(!is_control("/secret/db_pw"));
assert!(!is_control("/cluster/peers/a"));
assert!(!is_control("/qq/rpc"));
assert!(!is_control("/q"));
assert!(!is_control("/q/"));
assert!(!is_control(""));
}
#[test]
fn the_keys_compose_and_decompose() {
let c = uuid::Uuid::parse_str("2b8f0a1e-0000-4000-8000-000000000001").unwrap();
assert_eq!(dispatcher_key("/q/rpc"), "/q/rpc");
let k = consumer_key("/q/rpc", &c);
assert_eq!(k, "/q/rpc/c/2b8f0a1e-0000-4000-8000-000000000001");
assert!(is_control(&k));
assert_eq!(consumer_of(&k), Some(c));
assert_eq!(consumer_of(&format!("{}/7/key", k)), None);
assert_eq!(consumer_of("/q/rpc"), None);
}
#[test]
fn local_and_unknown_are_no_longer_the_same_answer() {
assert_eq!(Dispatch::of(None, 5), Dispatch::Unknown);
assert_eq!(Dispatch::of(Some(&DispatchRecord::new(5)), 5), Dispatch::Local);
assert_eq!(Dispatch::of(Some(&DispatchRecord::new(6)), 5), Dispatch::Remote(6));
assert!(Dispatch::Unknown.is_unknown());
assert!(Dispatch::Local.is_local());
assert!(!Dispatch::Remote(6).is_local());
}
#[test]
fn the_cost_model_holds() {
let (n, a, b, c) = (9usize, 1u64, 2u64, 3u64);
assert_eq!(leg_cost(n, a, None, b), n - 1);
assert_eq!(leg_cost(n, a, Some(c), b), 2);
assert_eq!(leg_cost(n, a, Some(b), b), 1);
assert_eq!(leg_cost(n, c, Some(b), b), 1);
assert_eq!(leg_cost(n, a, Some(a), b), 1);
assert_eq!(leg_cost(n, c, Some(a), b), 2);
assert_eq!(leg_cost(n, a, Some(a), a), 0);
assert_eq!(2 * leg_cost(9, a, None, b), 16);
assert_eq!(2 * leg_cost(9, a, Some(b), b), 2);
}
#[test]
fn a_consumer_claim_outranks_a_producer_claim() {
assert_ne!(Claim::Consumer, Claim::Producer);
}
}