pub(crate) mod fixed;
pub(crate) mod retention;
pub(crate) mod tx;
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
pub(crate) mod var;
pub(crate) mod wait;
use std::any::Any;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::time::Duration;
use armour_core::KeyScheme;
use crate::Key;
use crate::error::{DbError, DbResult};
pub const LIVE_CLASS: u8 = 0;
pub const DEAD_CLASS: u8 = 1;
pub const ID_BLOCK: u64 = 1024;
static NEXT_QUEUE_INSTANCE_ID: AtomicU64 = AtomicU64::new(1);
pub(crate) fn next_queue_instance_id() -> u64 {
NEXT_QUEUE_INSTANCE_ID
.fetch_update(AtomicOrdering::Relaxed, AtomicOrdering::Relaxed, |id| {
id.checked_add(1)
})
.expect("queue instance id exhausted")
}
#[derive(Default)]
pub(crate) struct QueueDepthAdmissions {
pending: HashMap<u64, u64>,
}
pub(crate) struct QueueDepthAdmission {
queue_id: u64,
live_count: Arc<AtomicU64>,
max_depth: Option<u64>,
}
impl QueueDepthAdmission {
fn new(queue_id: u64, live_count: Arc<AtomicU64>, max_depth: Option<u64>) -> Self {
Self {
queue_id,
live_count,
max_depth,
}
}
pub(crate) fn reserve(self, admissions: &mut QueueDepthAdmissions) -> DbResult<()> {
admissions.reserve(self.queue_id, &self.live_count, self.max_depth)
}
}
impl QueueDepthAdmissions {
pub(crate) fn reserve(
&mut self,
queue_id: u64,
live_count: &AtomicU64,
max_depth: Option<u64>,
) -> DbResult<()> {
let Some(limit) = max_depth else {
return Ok(());
};
let pending = self.pending.entry(queue_id).or_default();
let admitted = live_count
.load(AtomicOrdering::Acquire)
.saturating_add(*pending);
if admitted >= limit {
return Err(DbError::QueueFull);
}
*pending = pending.saturating_add(1);
Ok(())
}
}
pub(crate) use fixed::FixedQueueInner;
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
pub(crate) use var::VarQueueInner;
pub use wait::QueueDue;
use wait::{QueueScan, QueueWait};
#[doc(hidden)]
pub trait QueueHandle<N: QueueSpec> {
fn push(
queue: &Queue<N>,
payload: N::Payload,
not_before: Option<std::time::SystemTime>,
) -> DbResult<u64>;
fn len(queue: &Queue<N>) -> u64;
fn dead_len(queue: &Queue<N>) -> u64;
fn lease(
queue: &Queue<N>,
n: usize,
timeout: Option<Duration>,
) -> DbResult<Vec<LeasedEntry<N::Payload>>>;
fn ack(queue: &Queue<N>, token: LeaseToken) -> DbResult<()>;
fn nack(queue: &Queue<N>, token: LeaseToken) -> DbResult<()>;
fn scan_due(queue: &Queue<N>) -> DbResult<QueueScan>;
fn wait(queue: &Queue<N>) -> &QueueWait;
fn stats(queue: &Queue<N>) -> QueueStats;
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct QueueStats {
pub compaction_bytes: u64,
pub payload_writes: u64,
}
#[repr(C)]
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
Hash,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
zerocopy::KnownLayout,
)]
pub struct QueueKey {
sort_be: u64,
id_be: u64,
}
impl QueueKey {
pub const fn live(visible_at_ms: u64, id: u64) -> Self {
Self::new(LIVE_CLASS, visible_at_ms, id)
}
pub const fn dead(visible_at_ms: u64, id: u64) -> Self {
Self::new(DEAD_CLASS, visible_at_ms, id)
}
pub const fn class(self) -> u8 {
(u64::from_be(self.sort_be) >> 63) as u8
}
pub const fn visible_at_ms(self) -> u64 {
u64::from_be(self.sort_be) & ((1 << 63) - 1)
}
pub const fn id(self) -> u64 {
u64::from_be(self.id_be)
}
const fn new(class: u8, visible_at_ms: u64, id: u64) -> Self {
assert!(
visible_at_ms < (1 << 63),
"queue visibility exceeds 63-bit domain"
);
Self {
sort_be: (((class as u64) << 63) | visible_at_ms).to_be(),
id_be: id.to_be(),
}
}
pub fn as_bytes(&self) -> &[u8] {
zerocopy::IntoBytes::as_bytes(self)
}
}
impl Ord for QueueKey {
fn cmp(&self, other: &Self) -> Ordering {
(self.class(), self.visible_at_ms(), self.id()).cmp(&(
other.class(),
other.visible_at_ms(),
other.id(),
))
}
}
impl PartialOrd for QueueKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Key for QueueKey {
const KEY_SCHEME: KeyScheme = KeyScheme::Bytes;
fn as_bytes(&self) -> &[u8] {
self.as_bytes()
}
fn as_bytes_mut(&mut self) -> &mut [u8] {
zerocopy::IntoBytes::as_mut_bytes(self)
}
fn from_bytes(bytes: &[u8]) -> Self {
zerocopy::FromBytes::read_from_bytes(bytes).expect("16-byte queue key")
}
fn zeroed() -> Self {
Self::new(0, 0, 0)
}
}
pub(crate) const fn next_lease_epoch(epoch: u32) -> u32 {
epoch.wrapping_add(1)
}
pub(crate) fn partial_lease_batch<P>(
leased: Vec<LeasedEntry<P>>,
queue: &'static str,
key: QueueKey,
error: DbError,
) -> DbResult<Vec<LeasedEntry<P>>> {
if leased.is_empty() {
return Err(error);
}
tracing::warn!(
queue,
id = key.id(),
claimed = leased.len(),
%error,
"queue lease returning a partial batch after a row-level failure"
);
Ok(leased)
}
pub trait QueueMode<N: QueueSpec>: Send + Sync + 'static {
type Inner;
const STREAM_ROWS: usize;
const HAS_PAYLOAD_ROW: bool;
}
#[doc(hidden)]
pub trait QueueOpen<N: QueueSpec>: QueueMode<N> {
fn open(db: &crate::armour::Db, config: QueueConfig) -> DbResult<Arc<Queue<N>>>;
}
pub struct FixedQueueMode;
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
pub struct VarQueueMode;
impl<N> QueueMode<N> for FixedQueueMode
where
N: QueueSpec,
N::Payload: armour_core::GetType,
{
type Inner = FixedQueueInner<N>;
const STREAM_ROWS: usize = 1;
const HAS_PAYLOAD_ROW: bool = false;
}
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
impl<N> QueueMode<N> for VarQueueMode
where
N: QueueSpec,
N::Payload: armour_core::GetType,
{
type Inner = VarQueueInner<N>;
const STREAM_ROWS: usize = 2;
const HAS_PAYLOAD_ROW: bool = true;
}
pub(crate) trait QueueTarget: Send + Sync {
fn physical_rows(&self) -> usize;
fn owns_physical_name(&self, name: &str) -> bool;
fn name(&self) -> &'static str;
fn check_managed(&self, db: &crate::armour::Db) -> DbResult<()>;
fn stream_bytes(&self, payload: &dyn Any) -> DbResult<usize>;
fn tx_participants(&self) -> Vec<Arc<dyn crate::txn::participant::TxParticipant>>;
fn prepare_stream(&self) -> DbResult<QueueStreamPreparation>;
fn tx_stream_writes(
&self,
payload: &dyn Any,
visible_at: std::time::SystemTime,
) -> DbResult<crate::txn::participant::TxStreamWrites>;
}
#[doc(hidden)]
pub struct QueueStreamParticipant {
inner: Arc<dyn crate::txn::participant::TxParticipant>,
}
#[doc(hidden)]
pub struct QueueStreamPlan {
inner: crate::txn::participant::TxStreamWrites,
admission: QueueDepthAdmission,
}
#[doc(hidden)]
pub struct QueueStreamPreparation {
permits: Vec<crate::queue::retention::ProducerPermit>,
}
impl QueueStreamPreparation {
fn empty() -> Self {
Self {
permits: Vec::new(),
}
}
pub(crate) fn into_permits(self) -> Vec<crate::queue::retention::ProducerPermit> {
self.permits
}
}
#[doc(hidden)]
pub trait QueueStreamPayload<N: QueueSpec>: QueueMode<N> {
fn stream_bytes(payload: &N::Payload) -> usize;
fn check_managed(inner: &Self::Inner, db: &crate::armour::Db) -> DbResult<()>;
#[doc(hidden)]
fn stream_participants(inner: &Self::Inner) -> Vec<QueueStreamParticipant>;
fn prepare_stream(inner: &Self::Inner) -> DbResult<QueueStreamPreparation>;
#[doc(hidden)]
fn lower_stream(
inner: &Self::Inner,
payload: &dyn Any,
visible_at: std::time::SystemTime,
) -> DbResult<QueueStreamPlan>;
}
impl<N> QueueStreamPayload<N> for FixedQueueMode
where
N: QueueSpec<Mode = FixedQueueMode>,
N::Payload: Copy
+ zerocopy::IntoBytes
+ zerocopy::FromBytes
+ zerocopy::Immutable
+ armour_core::GetType,
{
fn stream_bytes(_: &N::Payload) -> usize {
16 + 32 + 4096
}
fn check_managed(inner: &Self::Inner, db: &crate::armour::Db) -> DbResult<()> {
db.tx_collection_id(Arc::clone(&inner.headers).as_ref())
.map(|_| ())
}
fn stream_participants(inner: &Self::Inner) -> Vec<QueueStreamParticipant> {
vec![QueueStreamParticipant {
inner: Arc::clone(&inner.headers) as Arc<dyn crate::txn::participant::TxParticipant>,
}]
}
fn prepare_stream(_: &Self::Inner) -> DbResult<QueueStreamPreparation> {
Ok(QueueStreamPreparation::empty())
}
fn lower_stream(
inner: &Self::Inner,
payload: &dyn Any,
visible_at: std::time::SystemTime,
) -> DbResult<QueueStreamPlan> {
let payload = payload
.downcast_ref::<N::Payload>()
.ok_or(DbError::Internal("queue stream payload type mismatch"))?;
let now = std::time::SystemTime::now();
let visible_at = fixed::FixedQueueInner::<N>::now_ms(visible_at)?;
if visible_at >= (1 << 63) {
return Err(DbError::Client("queue visibility exceeds 63-bit domain"));
}
let id = inner.next_id_in_tx()?;
let key = QueueKey::live(visible_at, id);
let row =
fixed::QueueFixedRow::<N>::new(*payload, fixed::FixedQueueInner::<N>::now_ms(now)?)?;
let count = Arc::clone(&inner.live_count);
let wait = Arc::clone(&inner.wait);
Ok(QueueStreamPlan {
inner: crate::txn::participant::TxStreamWrites {
writes: vec![crate::txn::participant::TxDerivedWrite {
participant: Arc::clone(&inner.headers)
as Arc<dyn crate::txn::participant::TxParticipant>,
key: crate::Key::as_bytes(&key).to_vec(),
value: Some(zerocopy::IntoBytes::as_bytes(&row).to_vec()),
}],
after_success: vec![Box::new(move || {
count.fetch_add(1, std::sync::atomic::Ordering::Release);
wait.notify_visible_at(visible_at);
})],
queue_depth_admissions: Vec::new(),
},
admission: QueueDepthAdmission::new(
inner.instance_id,
Arc::clone(&inner.live_count),
inner.config.max_depth,
),
})
}
}
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
impl<N> QueueStreamPayload<N> for VarQueueMode
where
N: QueueSpec<Mode = VarQueueMode>,
N::Payload: rapira::Rapira + armour_core::GetType + Send + Sync,
{
fn stream_bytes(payload: &N::Payload) -> usize {
let payload = var::QueueVarPayload::<N> {
bytes: rapira::serialize(payload),
_queue: std::marker::PhantomData,
};
16 + 32 + 8 + rapira::serialize(&payload).len()
}
fn check_managed(inner: &Self::Inner, db: &crate::armour::Db) -> DbResult<()> {
db.tx_collection_id(Arc::clone(&inner.headers).as_ref())?;
db.tx_collection_id(Arc::clone(&inner.payloads).as_ref())
.map(|_| ())
}
fn stream_participants(inner: &Self::Inner) -> Vec<QueueStreamParticipant> {
vec![
QueueStreamParticipant {
inner: Arc::clone(&inner.headers)
as Arc<dyn crate::txn::participant::TxParticipant>,
},
QueueStreamParticipant {
inner: Arc::clone(&inner.payloads)
as Arc<dyn crate::txn::participant::TxParticipant>,
},
]
}
fn prepare_stream(inner: &Self::Inner) -> DbResult<QueueStreamPreparation> {
inner.retention.process_before_queue_operation();
Ok(QueueStreamPreparation {
permits: vec![inner.retention.producer_permit()],
})
}
fn lower_stream(
inner: &Self::Inner,
payload: &dyn Any,
visible_at: std::time::SystemTime,
) -> DbResult<QueueStreamPlan> {
let payload = payload
.downcast_ref::<N::Payload>()
.ok_or(DbError::Internal("queue stream payload type mismatch"))?;
let now = std::time::SystemTime::now();
let visible_at = fixed::FixedQueueInner::<N>::now_ms(visible_at)?;
if visible_at >= (1 << 63) {
return Err(DbError::Client("queue visibility exceeds 63-bit domain"));
}
let id = inner.next_id_in_tx()?;
let key = QueueKey::live(visible_at, id);
let payload_key = rend::u64_be::from_native(id);
let payload = var::QueueVarPayload::<N> {
bytes: rapira::serialize(payload),
_queue: std::marker::PhantomData,
};
let mut payload_bytes = Vec::new();
crate::Codec::encode_to(&crate::RapiraCodec, &payload, &mut payload_bytes)?;
let header = var::QueueVarHeaderRow::<N>::new(fixed::FixedQueueInner::<N>::now_ms(now)?);
let count = Arc::clone(&inner.live_count);
let wait = Arc::clone(&inner.wait);
let payload_writes = Arc::clone(&inner.payload_writes);
Ok(QueueStreamPlan {
inner: crate::txn::participant::TxStreamWrites {
writes: vec![
crate::txn::participant::TxDerivedWrite {
participant: Arc::clone(&inner.payloads)
as Arc<dyn crate::txn::participant::TxParticipant>,
key: crate::Key::as_bytes(&payload_key).to_vec(),
value: Some(payload_bytes),
},
crate::txn::participant::TxDerivedWrite {
participant: Arc::clone(&inner.headers)
as Arc<dyn crate::txn::participant::TxParticipant>,
key: crate::Key::as_bytes(&key).to_vec(),
value: Some(zerocopy::IntoBytes::as_bytes(&header).to_vec()),
},
],
after_success: vec![Box::new(move || {
count.fetch_add(1, std::sync::atomic::Ordering::Release);
payload_writes.fetch_add(1, std::sync::atomic::Ordering::Release);
wait.notify_visible_at(visible_at);
})],
queue_depth_admissions: Vec::new(),
},
admission: QueueDepthAdmission::new(
inner.instance_id,
Arc::clone(&inner.live_count),
inner.config.max_depth,
),
})
}
}
impl<N> QueueTarget for Queue<N>
where
N: QueueSpec,
<N::Mode as QueueMode<N>>::Inner: Send + Sync,
N::Mode: QueueStreamPayload<N>,
{
fn physical_rows(&self) -> usize {
N::Mode::STREAM_ROWS
}
fn owns_physical_name(&self, name: &str) -> bool {
N::NAME == name || (N::Mode::HAS_PAYLOAD_ROW && N::PAYLOAD_NAME == name)
}
fn name(&self) -> &'static str {
N::NAME
}
fn check_managed(&self, db: &crate::armour::Db) -> DbResult<()> {
N::Mode::check_managed(&self.inner, db)
}
fn stream_bytes(&self, payload: &dyn Any) -> DbResult<usize> {
payload
.downcast_ref::<N::Payload>()
.map(N::Mode::stream_bytes)
.ok_or(DbError::Internal("queue stream payload type mismatch"))
}
fn tx_participants(&self) -> Vec<Arc<dyn crate::txn::participant::TxParticipant>> {
N::Mode::stream_participants(&self.inner)
.into_iter()
.map(|token| token.inner)
.collect()
}
fn prepare_stream(&self) -> DbResult<QueueStreamPreparation> {
N::Mode::prepare_stream(&self.inner)
}
fn tx_stream_writes(
&self,
payload: &dyn Any,
visible_at: std::time::SystemTime,
) -> DbResult<crate::txn::participant::TxStreamWrites> {
let QueueStreamPlan {
mut inner,
admission,
} = N::Mode::lower_stream(&self.inner, payload, visible_at)?;
inner.queue_depth_admissions.push(admission);
Ok(inner)
}
}
#[derive(Default, Clone)]
pub struct QueueTargets {
handles: Vec<Arc<dyn QueueTarget>>,
}
impl QueueTargets {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with<N>(self, queue: &Arc<Queue<N>>) -> Self
where
N: QueueSpec,
<N::Mode as QueueMode<N>>::Inner: Send + Sync,
N::Mode: QueueStreamPayload<N>,
{
assert!(
!self
.handles
.iter()
.any(|target| target.owns_physical_name(N::NAME)
|| (N::Mode::HAS_PAYLOAD_ROW && target.owns_physical_name(N::PAYLOAD_NAME))),
"QueueTargets: duplicate physical queue name"
);
let mut handles = self.handles;
handles.push(Arc::clone(queue) as Arc<dyn QueueTarget>);
Self { handles }
}
pub fn is_empty(&self) -> bool {
self.handles.is_empty()
}
pub fn len(&self) -> usize {
self.handles.len()
}
pub(crate) fn first_name(&self) -> &'static str {
self.handles.first().map_or("", |target| target.name())
}
pub(crate) fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
self.handles.iter().map(|target| target.name())
}
pub(crate) fn check_managed(&self, db: &crate::armour::Db) -> DbResult<()> {
for target in &self.handles {
target.check_managed(db)?;
}
Ok(())
}
pub(crate) fn tx_participants(&self) -> Vec<Arc<dyn crate::txn::participant::TxParticipant>> {
self.handles
.iter()
.flat_map(|target| target.tx_participants())
.collect()
}
pub(crate) fn prepare_streams(&self) -> DbResult<Vec<crate::queue::retention::ProducerPermit>> {
let mut permits = Vec::new();
for target in &self.handles {
permits.extend(target.prepare_stream()?.into_permits());
}
Ok(permits)
}
pub(crate) fn lower(
&self,
sink: &EmitSink<'_>,
) -> DbResult<crate::txn::participant::TxStreamWrites> {
let mut out = crate::txn::participant::TxStreamWrites::empty();
for emission in sink.emissions() {
let mut plan = emission
.target
.tx_stream_writes(emission.payload.as_ref(), emission.visible_at)?;
out.writes.append(&mut plan.writes);
out.after_success.append(&mut plan.after_success);
out.queue_depth_admissions
.append(&mut plan.queue_depth_admissions);
}
Ok(out)
}
pub(crate) fn contains<N>(&self, queue: &Arc<Queue<N>>) -> bool
where
N: QueueSpec,
<N::Mode as QueueMode<N>>::Inner: Send + Sync,
N::Mode: QueueStreamPayload<N>,
{
let queue = Arc::clone(queue) as Arc<dyn QueueTarget>;
self.handles
.iter()
.any(|target| Arc::ptr_eq(target, &queue))
}
}
pub(crate) struct BufferedEmission {
#[allow(dead_code)] pub(crate) target: Arc<dyn QueueTarget>,
#[allow(dead_code)] pub(crate) payload: Box<dyn Any + Send>,
#[allow(dead_code)] pub(crate) visible_at: std::time::SystemTime,
}
pub struct EmitSink<'a> {
targets: &'a QueueTargets,
max_rows: usize,
max_bytes: usize,
row_count: usize,
byte_count: usize,
emissions: Vec<BufferedEmission>,
}
impl<'a> EmitSink<'a> {
#[cfg(feature = "queue-test-support")]
#[doc(hidden)]
pub fn debug_new(targets: &'a QueueTargets, max_rows: usize, max_bytes: usize) -> Self {
Self::new(targets, max_rows, max_bytes)
}
pub(crate) fn new(targets: &'a QueueTargets, max_rows: usize, max_bytes: usize) -> Self {
Self {
targets,
max_rows,
max_bytes,
row_count: 0,
byte_count: 0,
emissions: Vec::new(),
}
}
pub fn emit<N>(&mut self, queue: &Arc<Queue<N>>, payload: N::Payload) -> DbResult<()>
where
N: QueueSpec,
N::Payload: Send + 'static,
<N::Mode as QueueMode<N>>::Inner: Send + Sync,
N::Mode: QueueStreamPayload<N>,
{
self.emit_at(queue, payload, std::time::SystemTime::now())
}
pub fn emit_at<N>(
&mut self,
queue: &Arc<Queue<N>>,
payload: N::Payload,
visible_at: std::time::SystemTime,
) -> DbResult<()>
where
N: QueueSpec,
N::Payload: Send + 'static,
<N::Mode as QueueMode<N>>::Inner: Send + Sync,
N::Mode: QueueStreamPayload<N>,
{
if !self.targets.contains(queue) {
return Err(DbError::Client("undeclared queue target"));
}
let target = Arc::clone(queue) as Arc<dyn QueueTarget>;
let added_rows = target.physical_rows();
let added_bytes = target.stream_bytes(&payload)?;
let next_rows = self.row_count + added_rows;
let next_bytes = self.byte_count + added_bytes;
if next_rows > self.max_rows || next_bytes > self.max_bytes {
return Err(DbError::ProjectionBudgetExceeded {
rows: next_rows,
bytes: next_bytes,
});
}
self.row_count = next_rows;
self.byte_count = next_bytes;
self.emissions.push(BufferedEmission {
target,
payload: Box::new(payload),
visible_at,
});
Ok(())
}
pub fn len(&self) -> usize {
self.emissions.len()
}
pub fn is_empty(&self) -> bool {
self.emissions.is_empty()
}
#[allow(dead_code)] pub(crate) fn emissions(&self) -> &[BufferedEmission] {
&self.emissions
}
}
pub trait QueueSpec: Sized + Send + Sync + 'static {
type Payload;
type Mode: QueueMode<Self>;
const NAME: &'static str;
const PAYLOAD_NAME: &'static str;
}
#[allow(dead_code)]
pub struct Queue<N: QueueSpec> {
pub(crate) inner: <N::Mode as QueueMode<N>>::Inner,
}
impl<N> Queue<N>
where
N: QueueSpec,
N::Mode: QueueHandle<N>,
{
pub fn push(
&self,
payload: N::Payload,
not_before: Option<std::time::SystemTime>,
) -> DbResult<u64> {
<N::Mode as QueueHandle<N>>::push(self, payload, not_before)
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> u64 {
<N::Mode as QueueHandle<N>>::len(self)
}
pub fn dead_len(&self) -> u64 {
<N::Mode as QueueHandle<N>>::dead_len(self)
}
pub fn lease(
&self,
n: usize,
timeout: Option<Duration>,
) -> DbResult<Vec<LeasedEntry<N::Payload>>> {
<N::Mode as QueueHandle<N>>::lease(self, n, timeout)
}
pub fn ack(&self, token: LeaseToken) -> DbResult<()> {
<N::Mode as QueueHandle<N>>::ack(self, token)
}
pub fn nack(&self, token: LeaseToken) -> DbResult<()> {
<N::Mode as QueueHandle<N>>::nack(self, token)
}
pub fn next_delay(&self) -> DbResult<Option<Duration>> {
Ok(<N::Mode as QueueHandle<N>>::scan_due(self)?.delay)
}
pub fn wait_due_timeout(&self, timeout: Duration) -> DbResult<bool> {
wait::wait_due_timeout(self, timeout)
}
pub fn due(&self) -> QueueDue<'_, N> {
QueueDue::new(self)
}
pub fn stats(&self) -> QueueStats {
<N::Mode as QueueHandle<N>>::stats(self)
}
#[cfg(all(debug_assertions, feature = "queue-test-support"))]
#[doc(hidden)]
pub fn debug_wait_registration_count(&self) -> usize {
<N::Mode as QueueHandle<N>>::wait(self).registration_count()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LeaseToken {
queue_instance_id: u64,
key: QueueKey,
epoch: u32,
}
impl LeaseToken {
#[allow(dead_code)] pub(crate) const fn new(queue_instance_id: u64, key: QueueKey, epoch: u32) -> Self {
Self {
queue_instance_id,
key,
epoch,
}
}
pub(crate) const fn belongs_to(self, queue_instance_id: u64) -> bool {
self.queue_instance_id == queue_instance_id
}
pub const fn key(self) -> QueueKey {
self.key
}
pub const fn epoch(self) -> u32 {
self.epoch
}
}
#[allow(dead_code)]
pub struct LeasedEntry<P> {
key: QueueKey,
id: u64,
attempts: u16,
payload: P,
token: LeaseToken,
}
impl<P> LeasedEntry<P> {
pub const fn key(&self) -> QueueKey {
self.key
}
pub const fn id(&self) -> u64 {
self.id
}
pub const fn attempts(&self) -> u16 {
self.attempts
}
pub fn payload(&self) -> &P {
&self.payload
}
pub const fn token(&self) -> LeaseToken {
self.token
}
}
#[cfg(test)]
mod lease_value_tests {
use super::*;
#[test]
fn leased_entry_accessors_expose_the_claimed_value() {
let key = QueueKey::live(2, 9);
let token = LeaseToken::new(1, key, 5);
let entry = LeasedEntry {
key,
id: 9,
attempts: 4,
payload: 1u64,
token,
};
assert_eq!(entry.key(), key);
assert_eq!(entry.id(), 9);
assert_eq!(entry.attempts(), 4);
assert_eq!(*entry.payload(), 1);
assert_eq!(entry.token().key(), key);
assert_eq!(entry.token().epoch(), 5);
}
}
pub struct QueueConfig {
pub(crate) max_attempts: u16,
pub(crate) backoff_secs: Vec<u64>,
pub(crate) lease_timeout: Duration,
pub(crate) dead_retention: Option<Duration>,
pub(crate) max_depth: Option<u64>,
pub(crate) wakeup_recheck: Duration,
header_config: Option<crate::FixedConfig>,
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
payload_config: Option<crate::Config>,
}
impl QueueConfig {
pub fn balanced() -> Self {
Self {
max_attempts: 3,
backoff_secs: vec![60],
lease_timeout: Duration::from_secs(60),
dead_retention: None,
max_depth: None,
wakeup_recheck: Duration::from_secs(5),
header_config: None,
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
payload_config: None,
}
}
pub fn max_attempts(mut self, max_attempts: u16) -> Self {
self.max_attempts = max_attempts;
self
}
pub fn backoff_secs(mut self, backoff_secs: &[u64]) -> Self {
self.backoff_secs = backoff_secs.to_vec();
self
}
pub fn lease_timeout(mut self, lease_timeout: Duration) -> Self {
self.lease_timeout = lease_timeout;
self
}
pub fn dead_retention(mut self, dead_retention: Option<Duration>) -> Self {
self.dead_retention = dead_retention;
self
}
pub fn max_depth(mut self, max_depth: Option<u64>) -> Self {
self.max_depth = max_depth;
self
}
pub fn wakeup_recheck(mut self, wakeup_recheck: Duration) -> Self {
self.wakeup_recheck = wakeup_recheck;
self
}
pub fn header_config(mut self, header_config: crate::FixedConfig) -> Self {
self.header_config = Some(header_config);
self
}
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
pub fn payload_config(mut self, payload_config: crate::Config) -> Self {
self.payload_config = Some(payload_config);
self
}
pub fn build(self) -> DbResult<Self> {
if self.max_attempts == 0 {
return Err(DbError::Config(
"queue max_attempts must be greater than zero",
));
}
if self.backoff_secs.is_empty() {
return Err(DbError::Config("queue backoff_secs must not be empty"));
}
if self.lease_timeout.as_millis() == 0 {
return Err(DbError::Config(
"queue lease_timeout must be at least one millisecond",
));
}
if self.wakeup_recheck.as_millis() == 0 {
return Err(DbError::Config(
"queue wakeup_recheck must be at least one millisecond",
));
}
if let Some(config) = &self.header_config {
config.validate()?;
}
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
if let Some(config) = &self.payload_config {
config.validate()?;
}
Ok(self)
}
pub fn fixed_config(&self) -> crate::FixedConfig {
let mut config = self.header_config.clone().unwrap_or_else(|| {
crate::FixedConfig::balanced()
.grow_step(100_000)
.reversed(false)
.build()
});
config.reversed = false;
config
}
#[cfg(all(feature = "rapira-codec", feature = "var-collections"))]
pub(crate) fn var_payload_config(&self) -> crate::Config {
self.payload_config
.clone()
.unwrap_or_else(|| crate::Config::balanced().build())
}
}