Fix tracking locked queue ids (closes #1066)
This commit is contained in:
@@ -128,16 +128,21 @@ pub struct EncryptionKeys {
|
||||
pub enum QueueEvent {
|
||||
Refresh(Option<u64>),
|
||||
WorkerDone(u64),
|
||||
OnHold(OnHold<QueuedMessage>),
|
||||
OnHold { queue_id: u64, status: OnHold },
|
||||
Paused(bool),
|
||||
Stop,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OnHold<T> {
|
||||
pub next_due: Option<u64>,
|
||||
pub limiters: Vec<ConcurrencyLimiter>,
|
||||
pub message: T,
|
||||
pub enum OnHold {
|
||||
InFlight,
|
||||
Locked {
|
||||
until: u64,
|
||||
},
|
||||
ConcurrencyLimited {
|
||||
limiters: Vec<ConcurrencyLimiter>,
|
||||
next_due: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
||||
@@ -34,8 +34,8 @@ tokio = { version = "1.23", features = ["full"] }
|
||||
jemallocator = "0.5.0"
|
||||
|
||||
[features]
|
||||
default = ["sqlite", "postgres", "mysql", "rocks", "elastic", "s3", "redis", "azure", "enterprise"]
|
||||
#default = ["rocks", "enterprise"]
|
||||
#default = ["sqlite", "postgres", "mysql", "rocks", "elastic", "s3", "redis", "azure", "enterprise"]
|
||||
default = ["rocks", "enterprise"]
|
||||
sqlite = ["store/sqlite"]
|
||||
foundationdb = ["store/foundation", "common/foundation"]
|
||||
postgres = ["store/postgres"]
|
||||
|
||||
@@ -18,7 +18,7 @@ use common::config::{
|
||||
server::ServerProtocol,
|
||||
smtp::{queue::RequireOptional, report::AggregateFrequency},
|
||||
};
|
||||
use common::ipc::{OnHold, PolicyType, QueueEvent, QueuedMessage, TlsEvent};
|
||||
use common::ipc::{OnHold, PolicyType, QueueEvent, TlsEvent};
|
||||
use common::Server;
|
||||
use mail_auth::{
|
||||
mta_sts::TlsRpt,
|
||||
@@ -41,16 +41,7 @@ use super::{lookup::ToNextHop, mta_sts, session::SessionParams, NextHop, TlsStra
|
||||
use crate::queue::{throttle, DeliveryAttempt, Domain, Error, QueueEnvelope, Status};
|
||||
|
||||
impl DeliveryAttempt {
|
||||
pub fn try_deliver(mut self, server: Server) -> Option<OnHold<QueuedMessage>> {
|
||||
// Global concurrency limiter
|
||||
if let Err(limiter) = server.is_outbound_allowed(&mut self.in_flight) {
|
||||
return Some(OnHold {
|
||||
next_due: None,
|
||||
limiters: vec![limiter],
|
||||
message: self.event,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn try_deliver(self, server: Server) {
|
||||
tokio::spawn(async move {
|
||||
// Lock queue event
|
||||
let queue_id = self.event.queue_id;
|
||||
@@ -123,11 +114,12 @@ impl DeliveryAttempt {
|
||||
QueueEvent::WorkerDone(queue_id)
|
||||
}
|
||||
} else {
|
||||
QueueEvent::OnHold(OnHold {
|
||||
next_due: Some(LOCK_EXPIRY + 1),
|
||||
limiters: vec![],
|
||||
message: self.event,
|
||||
})
|
||||
QueueEvent::OnHold {
|
||||
queue_id: self.event.queue_id,
|
||||
status: OnHold::Locked {
|
||||
until: now() + LOCK_EXPIRY + 1,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Notify queue manager
|
||||
@@ -139,8 +131,6 @@ impl DeliveryAttempt {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn deliver_task(mut self, server: Server, mut message: Message) -> QueueEvent {
|
||||
@@ -193,11 +183,13 @@ impl DeliveryAttempt {
|
||||
SpanId = span_id,
|
||||
);
|
||||
|
||||
QueueEvent::OnHold(OnHold {
|
||||
next_due,
|
||||
limiters: vec![limiter],
|
||||
message: self.event,
|
||||
})
|
||||
QueueEvent::OnHold {
|
||||
queue_id: self.event.queue_id,
|
||||
status: OnHold::ConcurrencyLimited {
|
||||
next_due,
|
||||
limiters: vec![limiter],
|
||||
},
|
||||
}
|
||||
}
|
||||
throttle::Error::Rate { retry_at } => {
|
||||
// Save changes to disk
|
||||
@@ -1339,11 +1331,13 @@ impl DeliveryAttempt {
|
||||
SpanId = span_id,
|
||||
);
|
||||
|
||||
QueueEvent::OnHold(OnHold {
|
||||
next_due,
|
||||
limiters: on_hold,
|
||||
message: self.event,
|
||||
})
|
||||
QueueEvent::OnHold {
|
||||
queue_id: self.event.queue_id,
|
||||
status: OnHold::ConcurrencyLimited {
|
||||
next_due,
|
||||
limiters: on_hold,
|
||||
},
|
||||
}
|
||||
} else if let Some(due) = message.next_event() {
|
||||
trc::event!(
|
||||
Queue(trc::QueueEvent::Rescheduled),
|
||||
|
||||
@@ -9,24 +9,25 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ahash::AHashSet;
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use common::{
|
||||
core::BuildServer,
|
||||
ipc::{OnHold, QueueEvent, QueuedMessage},
|
||||
ipc::{OnHold, QueueEvent},
|
||||
Inner,
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use store::write::now;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use super::{
|
||||
spool::{SmtpSpool, QUEUE_REFRESH},
|
||||
throttle::IsAllowed,
|
||||
DeliveryAttempt, Message, QueueId, Status,
|
||||
};
|
||||
|
||||
pub struct Queue {
|
||||
pub core: Arc<Inner>,
|
||||
pub on_hold: Vec<OnHold<QueuedMessage>>,
|
||||
pub in_flight: AHashSet<QueueId>,
|
||||
pub on_hold: AHashMap<QueueId, OnHold>,
|
||||
pub next_wake_up: Instant,
|
||||
pub rx: mpsc::Receiver<QueueEvent>,
|
||||
}
|
||||
@@ -39,12 +40,13 @@ impl SpawnQueue for mpsc::Receiver<QueueEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
const CLEANUP_INTERVAL: Duration = Duration::from_secs(10 * 60);
|
||||
|
||||
impl Queue {
|
||||
pub fn new(core: Arc<Inner>, rx: mpsc::Receiver<QueueEvent>) -> Self {
|
||||
Queue {
|
||||
core,
|
||||
on_hold: Vec::with_capacity(128),
|
||||
in_flight: AHashSet::with_capacity(128),
|
||||
on_hold: AHashMap::with_capacity(128),
|
||||
next_wake_up: Instant::now(),
|
||||
rx,
|
||||
}
|
||||
@@ -52,9 +54,10 @@ impl Queue {
|
||||
|
||||
pub async fn start(&mut self) {
|
||||
let mut is_paused = false;
|
||||
let mut next_cleanup = Instant::now() + CLEANUP_INTERVAL;
|
||||
|
||||
loop {
|
||||
let (on_hold, refresh_queue) = match tokio::time::timeout(
|
||||
let refresh_queue = match tokio::time::timeout(
|
||||
self.next_wake_up.duration_since(Instant::now()),
|
||||
self.rx.recv(),
|
||||
)
|
||||
@@ -62,20 +65,24 @@ impl Queue {
|
||||
{
|
||||
Ok(Some(QueueEvent::Refresh(queue_id))) => {
|
||||
if let Some(queue_id) = queue_id {
|
||||
self.in_flight.remove(&queue_id);
|
||||
self.on_hold.remove(&queue_id);
|
||||
}
|
||||
|
||||
(None, true)
|
||||
true
|
||||
}
|
||||
Ok(Some(QueueEvent::WorkerDone(queue_id))) => {
|
||||
self.in_flight.remove(&queue_id);
|
||||
|
||||
(None, false)
|
||||
self.on_hold.remove(&queue_id);
|
||||
!self.on_hold.is_empty()
|
||||
}
|
||||
Ok(Some(QueueEvent::OnHold(on_hold))) => {
|
||||
self.in_flight.remove(&on_hold.message.queue_id);
|
||||
Ok(Some(QueueEvent::OnHold { queue_id, status })) => {
|
||||
if let OnHold::Locked { until } = &status {
|
||||
let due_in = Instant::now() + Duration::from_secs(*until - now());
|
||||
if due_in < self.next_wake_up {
|
||||
self.next_wake_up = due_in;
|
||||
}
|
||||
}
|
||||
|
||||
(on_hold.into(), false)
|
||||
self.on_hold.insert(queue_id, status);
|
||||
self.on_hold.len() > 1
|
||||
}
|
||||
Ok(Some(QueueEvent::Paused(paused))) => {
|
||||
self.core
|
||||
@@ -83,98 +90,115 @@ impl Queue {
|
||||
.queue_status
|
||||
.store(!paused, Ordering::Relaxed);
|
||||
is_paused = paused;
|
||||
(None, false)
|
||||
false
|
||||
}
|
||||
Err(_) => (None, true),
|
||||
Err(_) => true,
|
||||
Ok(Some(QueueEvent::Stop)) | Ok(None) => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if !is_paused {
|
||||
// Deliver any concurrency limited messages
|
||||
let server = self.core.build_server();
|
||||
while let Some(queue_event) = self.next_on_hold() {
|
||||
if let Some(message) =
|
||||
DeliveryAttempt::new(queue_event).try_deliver(server.clone())
|
||||
{
|
||||
self.on_hold(message);
|
||||
} else {
|
||||
self.in_flight.insert(queue_event.queue_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Deliver scheduled messages
|
||||
if refresh_queue || self.next_wake_up <= Instant::now() {
|
||||
let now = now();
|
||||
let mut next_wake_up = QUEUE_REFRESH;
|
||||
for queue_event in server.next_event().await {
|
||||
match self.is_on_hold(queue_event.queue_id) {
|
||||
None => {
|
||||
if queue_event.due <= now {
|
||||
if !self.in_flight.contains(&queue_event.queue_id) {
|
||||
if let Some(message) = DeliveryAttempt::new(queue_event)
|
||||
.try_deliver(server.clone())
|
||||
{
|
||||
self.on_hold(message);
|
||||
} else {
|
||||
self.in_flight.insert(queue_event.queue_id);
|
||||
let server = self.core.build_server();
|
||||
|
||||
// Process queue events
|
||||
let mut queue_events = server.next_event().await;
|
||||
|
||||
if queue_events.len() > 5 {
|
||||
queue_events.shuffle(&mut rand::thread_rng());
|
||||
}
|
||||
|
||||
for queue_event in &queue_events {
|
||||
if queue_event.due <= now {
|
||||
// Check if the message is still on hold
|
||||
if let Some(on_hold) = self.on_hold.get(&queue_event.queue_id) {
|
||||
match on_hold {
|
||||
OnHold::Locked { until } => {
|
||||
if *until > now {
|
||||
let due_in = *until - now;
|
||||
if due_in < next_wake_up {
|
||||
next_wake_up = due_in;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let due_in = queue_event.due - now;
|
||||
if due_in < next_wake_up {
|
||||
next_wake_up = due_in;
|
||||
OnHold::ConcurrencyLimited { limiters, next_due } => {
|
||||
if !(limiters.iter().any(|l| {
|
||||
l.concurrent.load(Ordering::Relaxed) < l.max_concurrent
|
||||
}) || next_due.map_or(false, |due| due <= now))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
OnHold::InFlight => continue,
|
||||
}
|
||||
|
||||
self.on_hold.remove(&queue_event.queue_id);
|
||||
}
|
||||
|
||||
// Enforce global concurrency limits
|
||||
let mut in_flight = Vec::new();
|
||||
match server.is_outbound_allowed(&mut in_flight) {
|
||||
Ok(_) => {
|
||||
self.on_hold.insert(queue_event.queue_id, OnHold::InFlight);
|
||||
DeliveryAttempt {
|
||||
in_flight,
|
||||
event: *queue_event,
|
||||
}
|
||||
.try_deliver(server.clone());
|
||||
}
|
||||
|
||||
Err(limiter) => {
|
||||
self.on_hold.insert(
|
||||
queue_event.queue_id,
|
||||
OnHold::ConcurrencyLimited {
|
||||
limiters: vec![limiter],
|
||||
next_due: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(on_hold)
|
||||
if on_hold.limiters.is_empty()
|
||||
&& on_hold.next_due.map_or(false, |due| due < next_wake_up) =>
|
||||
{
|
||||
next_wake_up = on_hold.next_due.unwrap();
|
||||
} else {
|
||||
let due_in = queue_event.due - now;
|
||||
if due_in < next_wake_up {
|
||||
next_wake_up = due_in;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
self.next_wake_up = Instant::now() + Duration::from_secs(next_wake_up);
|
||||
|
||||
// Remove expired locks
|
||||
let now = Instant::now();
|
||||
if next_cleanup <= now {
|
||||
next_cleanup = now + CLEANUP_INTERVAL;
|
||||
|
||||
if !self.on_hold.is_empty() {
|
||||
let active_queue_ids = queue_events
|
||||
.into_iter()
|
||||
.map(|e| e.queue_id)
|
||||
.collect::<AHashSet<_>>();
|
||||
let now = store::write::now();
|
||||
self.on_hold.retain(|queue_id, status| match status {
|
||||
OnHold::InFlight => true,
|
||||
OnHold::Locked { until } => *until > now,
|
||||
OnHold::ConcurrencyLimited { .. } => {
|
||||
active_queue_ids.contains(queue_id)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.next_wake_up = now + Duration::from_secs(next_wake_up);
|
||||
}
|
||||
} else {
|
||||
// Queue is paused
|
||||
self.next_wake_up = Instant::now() + Duration::from_secs(86400);
|
||||
}
|
||||
|
||||
// Add message on hold
|
||||
if let Some(on_hold) = on_hold {
|
||||
self.on_hold(on_hold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_on_hold(&self, queue_id: QueueId) -> Option<&OnHold<QueuedMessage>> {
|
||||
self.on_hold.iter().find(|o| o.message.queue_id == queue_id)
|
||||
}
|
||||
|
||||
pub fn on_hold(&mut self, message: OnHold<QueuedMessage>) {
|
||||
self.on_hold.push(OnHold {
|
||||
next_due: message.next_due,
|
||||
limiters: message.limiters,
|
||||
message: message.message,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn next_on_hold(&mut self) -> Option<QueuedMessage> {
|
||||
let now = now();
|
||||
self.on_hold
|
||||
.iter()
|
||||
.position(|o| {
|
||||
o.limiters
|
||||
.iter()
|
||||
.any(|l| l.concurrent.load(Ordering::Relaxed) < l.max_concurrent)
|
||||
|| o.next_due.map_or(false, |due| due <= now)
|
||||
})
|
||||
.map(|pos| self.on_hold.remove(pos).message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
|
||||
Reference in New Issue
Block a user