use crate::job::InsertOpts;
use crate::queue_storage::ordering_key_hash64;
use std::collections::HashSet;
const DEFAULT_PHYSICAL_QUEUE_SUFFIX: &str = "__p";
const PARTITION_HASH_DOMAIN: u64 = 0x9d4d_1b2f_53a7_0c91;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PartitionedQueueError {
#[error("partitioned queue logical queue must not be empty")]
EmptyLogicalQueue,
#[error("partitioned queue partitions must be > 0")]
ZeroPartitions,
#[error("partitioned queue supports at most {max} physical queues; got {got}")]
TooManyPhysicalQueues { got: usize, max: usize },
#[error("partitioned queue physical queue must not be empty")]
EmptyPhysicalQueue,
#[error("partitioned queue physical queue '{queue}' is duplicated")]
DuplicatePhysicalQueue { queue: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionedQueue {
logical_queue: String,
physical_queues: Vec<String>,
}
impl PartitionedQueue {
pub fn new(
logical_queue: impl Into<String>,
partitions: usize,
) -> Result<Self, PartitionedQueueError> {
let logical_queue = logical_queue.into();
validate_logical_queue(&logical_queue)?;
validate_physical_count(partitions)?;
let mut physical_queues = Vec::with_capacity(partitions);
physical_queues.push(logical_queue.clone());
physical_queues.extend(
(1..partitions)
.map(|idx| format!("{logical_queue}{DEFAULT_PHYSICAL_QUEUE_SUFFIX}{idx}")),
);
Ok(Self {
logical_queue,
physical_queues,
})
}
pub fn from_physical_queues<I, S>(
logical_queue: impl Into<String>,
physical_queues: I,
) -> Result<Self, PartitionedQueueError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let logical_queue = logical_queue.into();
validate_logical_queue(&logical_queue)?;
let physical_queues: Vec<String> = physical_queues.into_iter().map(Into::into).collect();
validate_physical_count(physical_queues.len())?;
validate_physical_queues(&physical_queues)?;
Ok(Self {
logical_queue,
physical_queues,
})
}
pub fn logical_queue(&self) -> &str {
&self.logical_queue
}
pub fn physical_queues(&self) -> &[String] {
&self.physical_queues
}
pub fn partitions(&self) -> usize {
self.physical_queues.len()
}
pub fn queue_for_key(&self, key: impl AsRef<[u8]>) -> &str {
let partition = partition_for_ordering_key(key.as_ref(), self.partitions());
&self.physical_queues[partition]
}
pub fn queue_for_index(&self, index: usize) -> &str {
&self.physical_queues[index % self.partitions()]
}
pub fn route_opts_by_key(&self, mut opts: InsertOpts, key: impl AsRef<[u8]>) -> InsertOpts {
let key = key.as_ref();
opts.queue = self.queue_for_key(key).to_string();
opts.ordering_key = Some(key.to_vec());
opts
}
pub fn route_opts_by_index(&self, mut opts: InsertOpts, index: usize) -> InsertOpts {
opts.queue = self.queue_for_index(index).to_string();
opts
}
}
impl AsRef<[String]> for PartitionedQueue {
fn as_ref(&self) -> &[String] {
self.physical_queues()
}
}
impl<'a> IntoIterator for &'a PartitionedQueue {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.physical_queues.iter()
}
}
fn validate_logical_queue(logical_queue: &str) -> Result<(), PartitionedQueueError> {
if logical_queue.is_empty() {
return Err(PartitionedQueueError::EmptyLogicalQueue);
}
Ok(())
}
fn validate_physical_count(count: usize) -> Result<(), PartitionedQueueError> {
if count == 0 {
return Err(PartitionedQueueError::ZeroPartitions);
}
let max = i16::MAX as usize;
if count > max {
return Err(PartitionedQueueError::TooManyPhysicalQueues { got: count, max });
}
Ok(())
}
pub fn partition_for_ordering_key(ordering_key: &[u8], partitions: usize) -> usize {
if partitions <= 1 {
return 0;
}
(partition_hash64(ordering_key) % partitions as u64) as usize
}
pub fn partition_hash64(ordering_key: &[u8]) -> u64 {
let mut value = ordering_key_hash64(ordering_key) ^ PARTITION_HASH_DOMAIN;
value ^= value >> 30;
value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
value ^= value >> 27;
value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
value ^ (value >> 31)
}
fn validate_physical_queues(queues: &[String]) -> Result<(), PartitionedQueueError> {
let mut seen = HashSet::with_capacity(queues.len());
for queue in queues {
if queue.is_empty() {
return Err(PartitionedQueueError::EmptyPhysicalQueue);
}
if !seen.insert(queue.as_str()) {
return Err(PartitionedQueueError::DuplicatePhysicalQueue {
queue: queue.clone(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_names_preserve_single_queue_shape() {
let queue = PartitionedQueue::new("email", 1).expect("partitioned queue should build");
assert_eq!(queue.logical_queue(), "email");
assert_eq!(queue.physical_queues(), &["email".to_string()]);
assert_eq!(queue.partitions(), 1);
assert_eq!(queue.queue_for_index(42), "email");
}
#[test]
fn default_names_are_stable_for_multiple_queues() {
let queue = PartitionedQueue::new("email", 4).expect("partitioned queue should build");
assert_eq!(
queue.physical_queues(),
&[
"email".to_string(),
"email__p1".to_string(),
"email__p2".to_string(),
"email__p3".to_string(),
]
);
assert_eq!(queue.partitions(), 4);
assert_eq!(queue.queue_for_index(0), "email");
assert_eq!(queue.queue_for_index(5), "email__p1");
}
#[test]
fn key_routing_sets_queue_and_ordering_key() {
let queue =
PartitionedQueue::new("customer-updates", 4).expect("partitioned queue should build");
let opts = queue.route_opts_by_key(InsertOpts::default(), b"customer-42");
assert_eq!(opts.queue, queue.queue_for_key(b"customer-42"));
assert_eq!(opts.ordering_key.as_deref(), Some(&b"customer-42"[..]));
}
#[test]
fn partition_hash_is_domain_separated_from_enqueue_shard_hash() {
use crate::queue_storage::shard_for_ordering_key;
let partitions = 4;
let shards = 4;
let mut partition_shards = vec![HashSet::new(); partitions];
for idx in 0..20_000 {
let key = format!("customer-{idx}");
let partition = partition_for_ordering_key(key.as_bytes(), partitions);
let shard = shard_for_ordering_key(key.as_bytes(), shards);
partition_shards[partition].insert(shard);
}
for hits in partition_shards {
assert_eq!(hits.len(), shards as usize);
}
}
#[test]
fn explicit_queues_reject_empty_and_duplicate_names() {
let empty = PartitionedQueue::from_physical_queues("email", ["email-a", ""]);
assert!(matches!(
empty,
Err(PartitionedQueueError::EmptyPhysicalQueue)
));
let duplicate = PartitionedQueue::from_physical_queues("email", ["email-a", "email-a"]);
assert!(matches!(
duplicate,
Err(PartitionedQueueError::DuplicatePhysicalQueue { queue }) if queue == "email-a"
));
}
#[test]
fn explicit_queues_preserve_caller_order() {
let queue = PartitionedQueue::from_physical_queues(
"email",
["email-fast", "email-bulk", "email-slow"],
)
.expect("partitioned queue should build");
assert_eq!(
queue.physical_queues(),
&[
"email-fast".to_string(),
"email-bulk".to_string(),
"email-slow".to_string(),
]
);
assert_eq!(queue.queue_for_index(0), "email-fast");
assert_eq!(queue.queue_for_index(4), "email-bulk");
}
#[test]
fn partition_count_is_bounded_by_queue_storage_shard_type() {
let too_wide = PartitionedQueue::from_physical_queues(
"email",
(0..=(i16::MAX as usize)).map(|idx| format!("email-{idx}")),
);
assert!(matches!(
too_wide,
Err(PartitionedQueueError::TooManyPhysicalQueues { got, max })
if got == i16::MAX as usize + 1 && max == i16::MAX as usize
));
}
}