use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use aion_core::{ActivityId, WorkflowId};
use super::census::PoolCensus;
use super::policy::QueueServicePolicy;
use super::taxonomy::{QueueServiceReason, ServiceAddress};
use crate::error::ServerError;
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct UnservedKey {
pub namespace: String,
pub task_queue: String,
pub activity_type: String,
}
impl UnservedKey {
#[must_use]
pub fn of(address: &ServiceAddress) -> Self {
Self {
namespace: address.namespace.clone(),
task_queue: address.task_queue.clone(),
activity_type: address.activity_type.clone(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UnservedDispatch {
pub workflow_id: WorkflowId,
pub activity_id: ActivityId,
pub node: Option<String>,
pub waiting_for: Duration,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UnservedQueue {
pub key: UnservedKey,
pub reason: QueueServiceReason,
pub policy: QueueServicePolicy,
pub census: PoolCensus,
pub unserved_for: Duration,
pub waiting: Vec<UnservedDispatch>,
}
#[derive(Clone, Debug)]
struct Entry {
reason: QueueServiceReason,
policy: QueueServicePolicy,
census: PoolCensus,
since: Instant,
waiting: HashMap<(WorkflowId, ActivityId), Waiting>,
}
#[derive(Clone, Debug)]
struct Waiting {
node: Option<String>,
since: Instant,
}
#[derive(Clone, Debug, Default)]
pub struct QueueServiceState {
inner: Arc<Mutex<BTreeMap<UnservedKey, Entry>>>,
}
impl QueueServiceState {
pub fn mark(&self, parked: Parked<'_>) -> Result<(), ServerError> {
let key = UnservedKey::of(parked.address);
let now = Instant::now();
let mut state = self.state()?;
let entry = state.entry(key).or_insert_with(|| Entry {
reason: parked.reason,
policy: parked.policy,
census: parked.census,
since: now,
waiting: HashMap::new(),
});
entry.reason = parked.reason;
entry.policy = parked.policy;
entry.census = parked.census;
entry
.waiting
.entry((parked.workflow_id.clone(), parked.activity_id.clone()))
.or_insert_with(|| Waiting {
node: parked.address.node.clone(),
since: now,
});
Ok(())
}
pub fn clear(
&self,
address: &ServiceAddress,
workflow_id: &WorkflowId,
activity_id: &ActivityId,
) -> Result<(), ServerError> {
let key = UnservedKey::of(address);
let mut state = self.state()?;
let Some(entry) = state.get_mut(&key) else {
return Ok(());
};
entry
.waiting
.remove(&(workflow_id.clone(), activity_id.clone()));
if entry.waiting.is_empty() {
state.remove(&key);
}
Ok(())
}
pub fn unserved(&self) -> Result<Vec<UnservedQueue>, ServerError> {
let now = Instant::now();
let state = self.state()?;
Ok(state
.iter()
.map(|(key, entry)| {
let mut waiting: Vec<UnservedDispatch> = entry
.waiting
.iter()
.map(|((workflow_id, activity_id), held)| UnservedDispatch {
workflow_id: workflow_id.clone(),
activity_id: activity_id.clone(),
node: held.node.clone(),
waiting_for: now.saturating_duration_since(held.since),
})
.collect();
waiting.sort_by(|left, right| {
right.waiting_for.cmp(&left.waiting_for).then_with(|| {
left.workflow_id
.to_string()
.cmp(&right.workflow_id.to_string())
})
});
UnservedQueue {
key: key.clone(),
reason: entry.reason,
policy: entry.policy,
census: entry.census,
unserved_for: now.saturating_duration_since(entry.since),
waiting,
}
})
.collect())
}
pub fn parked_on_queue(&self, task_queue: &str) -> Result<usize, ServerError> {
Ok(self
.state()?
.iter()
.filter(|(key, _)| key.task_queue == task_queue)
.map(|(_, entry)| entry.waiting.len())
.sum())
}
pub fn is_unserved(
&self,
workflow_id: &WorkflowId,
activity_id: &ActivityId,
) -> Result<bool, ServerError> {
let held = (workflow_id.clone(), activity_id.clone());
Ok(self
.state()?
.values()
.any(|entry| entry.waiting.contains_key(&held)))
}
fn state(
&self,
) -> Result<std::sync::MutexGuard<'_, BTreeMap<UnservedKey, Entry>>, ServerError> {
self.inner
.lock()
.map_err(|_| ServerError::lock_poisoned("queue service state"))
}
}
#[derive(Clone, Copy, Debug)]
pub struct Parked<'a> {
pub address: &'a ServiceAddress,
pub reason: QueueServiceReason,
pub policy: QueueServicePolicy,
pub census: PoolCensus,
pub workflow_id: &'a WorkflowId,
pub activity_id: &'a ActivityId,
}
#[cfg(test)]
mod tests {
use super::*;
fn address() -> ServiceAddress {
ServiceAddress {
namespace: "default".to_owned(),
task_queue: "general".to_owned(),
activity_type: "greet".to_owned(),
node: None,
}
}
fn parked<'a>(
address: &'a ServiceAddress,
workflow_id: &'a WorkflowId,
activity_id: &'a ActivityId,
) -> Parked<'a> {
Parked {
address,
reason: QueueServiceReason::NoLivePollers,
policy: QueueServicePolicy::DurablePending,
census: PoolCensus::default(),
workflow_id,
activity_id,
}
}
#[test]
fn a_parked_dispatch_is_visible_immediately() -> Result<(), ServerError> {
let state = QueueServiceState::default();
let address = address();
let workflow_id = WorkflowId::new_v4();
let activity_id = ActivityId::from_sequence_position(3);
state.mark(parked(&address, &workflow_id, &activity_id))?;
let unserved = state.unserved()?;
assert_eq!(unserved.len(), 1);
assert_eq!(unserved[0].reason, QueueServiceReason::NoLivePollers);
assert_eq!(unserved[0].policy, QueueServicePolicy::DurablePending);
assert_eq!(unserved[0].key.task_queue, "general");
assert_eq!(unserved[0].waiting.len(), 1);
assert_eq!(unserved[0].waiting[0].workflow_id, workflow_id);
assert!(state.is_unserved(&workflow_id, &activity_id)?);
Ok(())
}
#[test]
fn clearing_the_last_waiter_removes_the_address() -> Result<(), ServerError> {
let state = QueueServiceState::default();
let address = address();
let first = WorkflowId::new_v4();
let second = WorkflowId::new_v4();
let activity_id = ActivityId::from_sequence_position(0);
state.mark(parked(&address, &first, &activity_id))?;
state.mark(parked(&address, &second, &activity_id))?;
assert_eq!(state.unserved()?.len(), 1);
assert_eq!(state.unserved()?[0].waiting.len(), 2);
state.clear(&address, &first, &activity_id)?;
assert!(!state.is_unserved(&first, &activity_id)?);
assert_eq!(state.unserved()?[0].waiting.len(), 1);
state.clear(&address, &second, &activity_id)?;
assert!(state.unserved()?.is_empty(), "the address must disappear");
Ok(())
}
#[test]
fn clearing_an_unknown_dispatch_is_a_no_op() -> Result<(), ServerError> {
let state = QueueServiceState::default();
state.clear(
&address(),
&WorkflowId::new_v4(),
&ActivityId::from_sequence_position(0),
)?;
assert!(state.unserved()?.is_empty());
Ok(())
}
#[test]
fn re_marking_refreshes_the_verdict_without_duplicating_the_waiter() -> Result<(), ServerError>
{
let state = QueueServiceState::default();
let address = address();
let workflow_id = WorkflowId::new_v4();
let activity_id = ActivityId::from_sequence_position(0);
state.mark(parked(&address, &workflow_id, &activity_id))?;
state.mark(Parked {
reason: QueueServiceReason::PollersIncompatible,
census: PoolCensus {
workers_in_pool: 2,
..PoolCensus::default()
},
..parked(&address, &workflow_id, &activity_id)
})?;
let unserved = state.unserved()?;
assert_eq!(unserved.len(), 1);
assert_eq!(unserved[0].waiting.len(), 1);
assert_eq!(unserved[0].reason, QueueServiceReason::PollersIncompatible);
assert_eq!(unserved[0].census.workers_in_pool, 2);
Ok(())
}
}