use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tracing::Instrument;
use crate::error::Error;
use crate::instance::Executor;
use crate::queue::DEFAULT_POLLING_INTERVAL;
use crate::sysdb;
use crate::sysdb::INTERNAL_QUEUE;
use crate::sysdb::types::{
AppScope, DatabaseTime, QueueRecord, Timestamp, WorkflowFilter, WorkflowRecord, WorkflowStatus,
};
use crate::workflow::{MAX_RECOVERY_ATTEMPTS, Role, spawn_execution, spawn_tracked};
const SUPERVISOR_INTERVAL: Duration = Duration::from_secs(1);
const MAX_POLLING_INTERVAL: Duration = Duration::from_secs(120);
const BACKOFF_FACTOR: f64 = 2.0;
const SCALEBACK_FACTOR: f64 = 0.9;
const JITTER: (f64, f64) = (0.95, 1.05);
#[derive(Default)]
pub(crate) struct Running(Mutex<HashMap<Key, i64>>);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Key {
Queue(String),
Partition(String, String),
}
impl Running {
fn claim(self: &Arc<Self>, queue: &str, partition: Option<&str>) -> Slot {
let mut keys = vec![Key::Queue(queue.to_owned())];
if let Some(partition) = partition {
keys.push(Key::Partition(queue.to_owned(), partition.to_owned()));
}
let mut counts = self.0.lock().expect("the running tally is poisoned");
for key in &keys {
*counts.entry(key.clone()).or_default() += 1;
}
drop(counts);
Slot {
running: Arc::clone(self),
keys,
}
}
fn count(&self, queue: &str) -> i64 {
self.get(&Key::Queue(queue.to_owned()))
}
fn count_for_partition(&self, queue: &str, partition: &str) -> i64 {
self.get(&Key::Partition(queue.to_owned(), partition.to_owned()))
}
fn get(&self, key: &Key) -> i64 {
self.0
.lock()
.expect("the running tally is poisoned")
.get(key)
.copied()
.unwrap_or(0)
}
}
pub(crate) struct Slot {
running: Arc<Running>,
keys: Vec<Key>,
}
impl Drop for Slot {
fn drop(&mut self) {
let mut counts = match self.running.0.lock() {
Ok(counts) => counts,
Err(_) => return,
};
for key in &self.keys {
if let Some(count) = counts.get_mut(key) {
*count -= 1;
if *count <= 0 {
counts.remove(key);
}
}
}
}
}
type Queues = Arc<Mutex<HashMap<String, QueueRecord>>>;
pub(crate) fn spawn(executor: Arc<Executor>) {
spawn_tracked(
&executor,
Role::Producer,
"queues".to_owned(),
{
let executor = Arc::clone(&executor);
supervise(executor)
}
.instrument(tracing::info_span!("queues")),
);
}
async fn supervise(executor: Arc<Executor>) {
let queues: Queues = Arc::default();
let running: Arc<Running> = Arc::default();
let mut workers: HashMap<String, crate::rt::JoinHandle<()>> = HashMap::new();
let mut warned_internal = false;
loop {
match executor.sysdb().transition_delayed_workflows().await {
Ok(0) => {}
Ok(moved) => tracing::debug!(workflows = moved, "delayed workflows are now enqueued"),
Err(error) => {
tracing::warn!(error = %error, "could not transition delayed workflows");
}
}
let names = refresh_queue_set(&executor, &queues, &mut warned_internal).await;
workers.retain(|_, handle| !handle.is_finished());
for name in names {
if workers.contains_key(&name) {
continue;
}
tracing::debug!(queue = name, "starting a worker for the queue");
let handle = spawn_tracked(
&executor,
Role::Producer,
format!("queue {name}"),
poll_queue(
Arc::clone(&executor),
name.clone(),
Arc::clone(&queues),
Arc::clone(&running),
)
.instrument(tracing::info_span!("queue", queue = %name)),
);
workers.insert(name, handle);
}
if !executor.sleep_unless_stopping(SUPERVISOR_INTERVAL).await {
return;
}
}
}
async fn refresh_queue_set(
executor: &Arc<Executor>,
queues: &Queues,
warned_internal: &mut bool,
) -> Vec<String> {
let listed = match executor.sysdb().list_queues(&AppScope::Unset).await {
Ok(listed) => listed,
Err(error) => {
tracing::warn!(error = %error, "could not list queues; keeping the current set");
return queues
.lock()
.expect("the queue set is poisoned")
.keys()
.cloned()
.collect();
}
};
let mut current: HashMap<String, QueueRecord> =
HashMap::from([(INTERNAL_QUEUE.to_owned(), internal_queue())]);
let listened = executor.listen_queues();
for queue in listed {
if queue.name == INTERNAL_QUEUE {
if !*warned_internal {
*warned_internal = true;
tracing::warn!(
queue = INTERNAL_QUEUE,
"the queues table holds a row for the engine's internal queue; its stored \
limits are ignored. Delete the row: it can only throttle `resume` and `fork`"
);
}
continue;
}
if let Some(listened) = listened
&& !listened.contains(&queue.name)
{
continue;
}
current.insert(queue.name.clone(), queue);
}
let names = current.keys().cloned().collect();
*queues.lock().expect("the queue set is poisoned") = current;
names
}
fn internal_queue() -> QueueRecord {
QueueRecord {
name: INTERNAL_QUEUE.to_owned(),
concurrency: None,
worker_concurrency: None,
rate_limit: None,
partition_concurrency: None,
partition_worker_concurrency: None,
partition_rate_limit: None,
polling_interval: DEFAULT_POLLING_INTERVAL,
application_name: None,
}
}
async fn poll_queue(executor: Arc<Executor>, name: String, queues: Queues, running: Arc<Running>) {
let mut interval = DEFAULT_POLLING_INTERVAL;
loop {
if executor.is_stopping() {
return;
}
let Some(queue) = queues
.lock()
.expect("the queue set is poisoned")
.get(&name)
.cloned()
else {
tracing::info!("the queue is no longer registered; stopping its worker");
return;
};
let floor = queue.polling_interval;
let ceiling = floor.max(MAX_POLLING_INTERVAL);
interval = interval.clamp(floor, ceiling);
let polled = poll_once(&executor, &queue, &running).await;
tracing::trace!(claims = polled.claims, "polled the queue");
let backs_off = polled.contention.is_some_and(Contention::backs_off);
interval = if backs_off {
interval.mul_f64(BACKOFF_FACTOR).min(ceiling)
} else {
interval.mul_f64(SCALEBACK_FACTOR).max(floor)
};
if !executor.sleep_unless_stopping(jitter(interval)).await {
return;
}
}
}
fn worker_budget(queue: &QueueRecord, running: i64) -> Option<i64> {
if queue.partition_worker_concurrency.is_some_and(|w| w <= 0) {
return Some(0);
}
queue
.worker_concurrency
.map(|worker| (i64::from(worker) - running).max(0))
}
#[derive(Debug, Default)]
struct Polled {
contention: Option<Contention>,
claims: usize,
}
impl Polled {
fn new(contention: Option<Contention>, claims: usize) -> Self {
Self { contention, claims }
}
}
async fn poll_once(
executor: &Arc<Executor>,
queue: &QueueRecord,
running: &Arc<Running>,
) -> Polled {
let owner_xid = || uuid::Uuid::new_v4().to_string();
if !queue.is_partitioned() {
let owner_xid = owner_xid();
return match executor
.sysdb()
.start_queued_workflows(
queue,
executor.executor_id(),
executor.app_version(),
&owner_xid,
None,
running.count(&queue.name),
0,
)
.await
{
Ok(claimed) => {
dispatch_claimed(
executor,
queue,
None,
running,
claimed.workflow_ids,
&owner_xid,
)
.await;
Polled::new(None, 1)
}
Err(error) => Polled::new(report_dequeue_error(&error), 1),
};
}
let already_running = running.count(&queue.name);
let budget = worker_budget(queue, already_running);
if budget == Some(0) {
return Polled::default();
}
if queue.partition_concurrency == Some(1)
&& queue.concurrency.is_none()
&& queue.rate_limit.is_none()
&& queue.partition_rate_limit.is_none()
{
let owner_xid = owner_xid();
return match executor
.sysdb()
.start_queued_partitioned_workflows(
queue,
executor.executor_id(),
executor.app_version(),
&owner_xid,
budget,
)
.await
{
Ok(claimed) => {
dispatch_claimed(executor, queue, None, running, claimed, &owner_xid).await;
Polled::new(None, 1)
}
Err(error) => Polled::new(report_dequeue_error(&error), 1),
};
}
let global_room = match queue.concurrency {
None => None,
Some(concurrency) => {
match executor
.sysdb()
.count_pending_queue_workflows(&queue.name)
.await
{
Ok(pending) => Some((i64::from(concurrency) - pending).max(0)),
Err(error) => return Polled::new(report_dequeue_error(&error), 0),
}
}
};
if global_room == Some(0) {
return Polled::default();
}
let partitions = match executor.sysdb().get_queue_partitions(&queue.name).await {
Ok(partitions) => shuffled(partitions),
Err(error) => return Polled::new(report_dequeue_error(&error), 0),
};
let mut claimed_here = 0i64;
let mut claims = 0usize;
for partition in partitions {
if worker_budget(queue, already_running + claimed_here) == Some(0)
|| global_room.is_some_and(|room| claimed_here >= room)
{
break;
}
claims += 1;
let owner_xid = owner_xid();
let claimed = match executor
.sysdb()
.start_queued_workflows(
queue,
executor.executor_id(),
executor.app_version(),
&owner_xid,
Some(&partition),
already_running + claimed_here,
running.count_for_partition(&queue.name, &partition),
)
.await
{
Ok(claimed) => claimed,
Err(error) if contention(&error).is_some() => {
tracing::debug!(
partition,
error = %error,
"a peer is mid-dequeue on this partition; skipping it"
);
continue;
}
Err(error) => {
tracing::warn!(error = %error, partition, "could not dequeue from the partition");
continue;
}
};
claimed_here += i64::try_from(claimed.workflow_ids.len()).unwrap_or(i64::MAX);
dispatch_claimed(
executor,
queue,
Some(&partition),
running,
claimed.workflow_ids,
&owner_xid,
)
.await;
if let Some(stop) = claimed.stopped_by
&& stop.spans_partitions()
{
tracing::debug!(
partition,
?stop,
"a queue-wide limit has no room left; ending the partition walk"
);
break;
}
}
Polled::new(None, claims)
}
fn report_dequeue_error(error: &sysdb::Error) -> Option<Contention> {
let contention = contention(error);
match contention {
Some(_) => tracing::debug!(error = %error, "a peer is mid-dequeue; polling again later"),
None => tracing::warn!(error = %error, "could not dequeue from the queue"),
}
contention
}
fn shuffled(mut keys: Vec<String>) -> Vec<String> {
let mut state = (uuid::Uuid::new_v4().as_u128() as u64) | 1;
for i in (1..keys.len()).rev() {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
keys.swap(i, (state % (i as u64 + 1)) as usize);
}
keys
}
async fn dispatch_claimed(
executor: &Arc<Executor>,
queue: &QueueRecord,
partition: Option<&str>,
running: &Arc<Running>,
claimed: Vec<String>,
owner_xid: &str,
) {
if claimed.is_empty() {
return;
}
let ids: Vec<&str> = claimed.iter().map(String::as_str).collect();
let rows = match executor
.sysdb()
.list_workflows(
&WorkflowFilter {
workflow_ids: ids,
load_output: false,
..WorkflowFilter::default()
},
None,
)
.await
{
Ok(rows) => rows,
Err(error) => {
tracing::warn!(
error = %error,
claimed = claimed.len(),
"could not read the claimed workflows; they stay PENDING until this executor \
restarts"
);
return;
}
};
if rows.len() != claimed.len() {
tracing::warn!(
claimed = claimed.len(),
found = rows.len(),
"some claimed workflows have no row"
);
}
tracing::debug!(workflows = rows.len(), "dequeued workflows");
let clock = if rows.iter().any(|row| row.deadline.is_some()) {
timing_clock(executor.sysdb().database_time().await)
} else {
local_clock()
};
let mut by_id: HashMap<String, _> = rows
.into_iter()
.map(|row| (row.workflow_id.clone(), row))
.collect();
for id in &claimed {
let Some(row) = by_id.remove(id) else {
continue;
};
let workflow_id = row.workflow_id.clone();
let partition = partition.or(row.queue_partition_key.as_deref());
let slot = running.claim(&queue.name, partition);
if let Err(error) = dispatch(executor, row, owner_xid, clock, Some(slot)).await {
tracing::warn!(
workflow_id,
error = %error,
"could not start the dequeued workflow; it stays PENDING until this executor \
restarts"
);
}
}
}
async fn dispatch(
executor: &Arc<Executor>,
row: WorkflowRecord,
owner_xid: &str,
clock: DatabaseTime,
slot: Option<Slot>,
) -> crate::Result<()> {
let Some(key) = row.qualified_name() else {
tracing::warn!(
workflow_id = row.workflow_id,
"the row names no workflow; skipped"
);
return Ok(());
};
let workflow_id = row.workflow_id;
if !executor.workflows().contains_key(&key) {
tracing::warn!(
workflow_id,
workflow = %key,
"no workflow is registered under the row's name; it stays where it is"
);
return Ok(());
}
match admission(row.status, row.recovery_attempts) {
Admission::Run => {}
Admission::NotPending => {
tracing::warn!(
workflow_id,
status = %row.status,
"the dequeued workflow is no longer pending; it is not started"
);
return Ok(());
}
Admission::Park => {
match executor
.sysdb()
.dead_letter_workflows(&[&workflow_id], row.recovery_attempts)
.await
{
Ok(parked) if !parked.is_empty() => tracing::warn!(
workflow_id,
attempts = row.recovery_attempts,
limit = MAX_RECOVERY_ATTEMPTS,
"the workflow exceeded its recovery attempts and is parked"
),
Ok(_) => {}
Err(error) => return Err(Error::SystemDatabase(error)),
}
return Ok(());
}
}
tracing::debug!(
workflow_id,
workflow = %key,
attempt = row.recovery_attempts,
"running the workflow"
);
spawn_execution(
executor,
key,
workflow_id,
owner_xid.to_owned(),
row.input,
row.serialization,
crate::workflow::Deadline::of(row.deadline, clock),
slot,
);
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Contention {
LockNotAvailable,
SerializationFailure,
}
impl Contention {
fn backs_off(self) -> bool {
self == Contention::SerializationFailure
}
}
fn contention(error: &sysdb::Error) -> Option<Contention> {
let sysdb::Error::Backend(backend) = error else {
return None;
};
match backend.sqlstate.as_deref() {
Some("55P03") => Some(Contention::LockNotAvailable),
Some("40001") => Some(Contention::SerializationFailure),
_ => None,
}
}
fn jitter(interval: Duration) -> Duration {
let (low, high) = JITTER;
interval.mul_f64(low + (high - low) * random_unit())
}
pub(crate) fn random_unit() -> f64 {
let bits = uuid::Uuid::new_v4().as_u128() as u32;
f64::from(bits) / f64::from(u32::MAX).next_up()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Admission {
Run,
NotPending,
Park,
}
fn admission(status: WorkflowStatus, recovery_attempts: i64) -> Admission {
if status != WorkflowStatus::Pending {
Admission::NotPending
} else if recovery_attempts > MAX_RECOVERY_ATTEMPTS + 1 {
Admission::Park
} else {
Admission::Run
}
}
fn timing_clock(reading: Result<DatabaseTime, sysdb::Error>) -> DatabaseTime {
reading.unwrap_or_else(|error| {
tracing::warn!(
error = %error,
"could not read the database's clock; the claimed deadlines are timed from this host's"
);
local_clock()
})
}
fn local_clock() -> DatabaseTime {
DatabaseTime::read(Timestamp::now().as_epoch_ms())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sysdb::{BackendError, BackendErrorKind};
fn backend(sqlstate: &str, kind: BackendErrorKind) -> sysdb::Error {
sysdb::Error::Backend(BackendError {
message: "boom".to_owned(),
sqlstate: Some(sqlstate.to_owned()),
kind,
})
}
#[test]
fn only_a_serialization_failure_backs_off() {
let locked = contention(&backend("55P03", BackendErrorKind::Permanent));
assert_eq!(locked, Some(Contention::LockNotAvailable));
assert!(!locked.unwrap().backs_off());
let conflict = contention(&backend("40001", BackendErrorKind::Conflict));
assert_eq!(conflict, Some(Contention::SerializationFailure));
assert!(conflict.unwrap().backs_off());
}
#[test]
fn a_deadlock_is_not_contention() {
assert_eq!(
contention(&backend("40P01", BackendErrorKind::Conflict)),
None
);
assert_eq!(
contention(&backend("23505", BackendErrorKind::Permanent)),
None
);
assert_eq!(
contention(&sysdb::Error::Malformed("bad row".to_owned())),
None
);
}
#[tokio::test]
async fn a_row_claimed_again_since_records_nothing() {
use std::sync::atomic::{AtomicU32, Ordering};
let db = dbos_test_support::test_database().await;
let dbos = crate::DBOS::new(crate::Config {
migrate: false,
app_version: Some("1.0.0".to_owned()),
..crate::Config::new("dispatch-test", db.url())
});
let runs = Arc::new(AtomicU32::new(0));
let dbos_runs = Arc::clone(&runs);
dbos.register_workflow("counted", move |()| {
let runs = Arc::clone(&dbos_runs);
async move {
runs.fetch_add(1, Ordering::SeqCst);
Ok::<_, crate::Error>(())
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let executor = dbos.executor("test").expect("launched");
executor
.sysdb()
.init_workflow(
&crate::sysdb::types::NewWorkflow {
name: Some("counted"),
input: Some("null"),
serialization: Some("rust_serde"),
..crate::sysdb::types::NewWorkflow::new("wf-reclaimed")
},
None,
crate::sysdb::types::WorkflowIdReusePolicy::ReturnExisting,
)
.await
.unwrap();
let row = || async {
executor
.sysdb()
.get_workflow("wf-reclaimed")
.await
.unwrap()
.unwrap()
};
let owner = row().await.owner_xid.expect("a started row is owned");
dispatch(
&executor,
row().await,
"an-earlier-claim",
local_clock(),
None,
)
.await
.unwrap();
let ran = |n: u32| {
let runs = Arc::clone(&runs);
async move {
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while runs.load(Ordering::SeqCst) < n {
assert!(
std::time::Instant::now() < deadline,
"run {n} never started"
);
crate::rt::sleep(Duration::from_millis(20)).await;
}
}
};
ran(1).await;
crate::rt::sleep(Duration::from_millis(300)).await;
let held = row().await;
assert_eq!(
held.status,
WorkflowStatus::Pending,
"a stale claim settled it"
);
assert_eq!(held.owner_xid.as_deref(), Some(owner.as_str()));
dispatch(&executor, row().await, &owner, local_clock(), None)
.await
.unwrap();
ran(2).await;
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while row().await.status != WorkflowStatus::Success {
assert!(
std::time::Instant::now() < deadline,
"the owner's run never recorded its outcome"
);
crate::rt::sleep(Duration::from_millis(20)).await;
}
dbos.shutdown().await;
}
async fn partitioned_backlog(
queue: crate::sysdb::types::NewQueue<'_>,
partitions: usize,
) -> (
crate::DBOS,
Arc<Executor>,
dbos_test_support::TestDatabase,
QueueRecord,
) {
let db = dbos_test_support::test_database().await;
let dbos = crate::DBOS::new(crate::Config {
migrate: false,
app_version: Some("1.0.0".to_owned()),
listen_queues: Some(Vec::new()),
..crate::Config::new("walk-test", db.url())
});
dbos.register_workflow("noop", |()| async { Ok::<_, crate::Error>(()) })
.unwrap();
dbos.launch().await.expect("launch failed");
let executor = dbos.executor("test").expect("launched");
let sysdb = executor.sysdb();
sysdb
.upsert_queue(&queue, crate::sysdb::types::OnExistingQueue::Update)
.await
.unwrap();
for n in 0..partitions {
let id = format!("wf-waiting-{n}");
let partition = format!("p-{n}");
sysdb
.init_workflow(
&crate::sysdb::types::NewWorkflow {
name: Some("noop"),
input: Some("null"),
serialization: Some("rust_serde"),
queue_name: Some(queue.name),
queue_partition_key: Some(&partition),
application_version: Some(executor.app_version()),
..crate::sysdb::types::NewWorkflow::new(&id)
},
None,
crate::sysdb::types::WorkflowIdReusePolicy::ReturnExisting,
)
.await
.unwrap();
}
let registered = sysdb.get_queue(queue.name).await.unwrap().unwrap();
(dbos, executor, db, registered)
}
async fn started_elsewhere(
executor: &Executor,
db: &dbos_test_support::TestDatabase,
queue: &str,
partition: &str,
) {
executor
.sysdb()
.init_workflow(
&crate::sysdb::types::NewWorkflow {
name: Some("noop"),
input: Some("null"),
serialization: Some("rust_serde"),
application_version: Some(executor.app_version()),
..crate::sysdb::types::NewWorkflow::new("wf-started-elsewhere")
},
None,
crate::sysdb::types::WorkflowIdReusePolicy::ReturnExisting,
)
.await
.unwrap();
sqlx::query(
r#"UPDATE "dbos"."workflow_status"
SET queue_name = $1, queue_partition_key = $2, rate_limited = TRUE,
started_at_epoch_ms = (EXTRACT(epoch FROM now()) * 1000)::bigint
WHERE workflow_uuid = 'wf-started-elsewhere'"#,
)
.bind(queue)
.bind(partition)
.execute(&db.pool().await)
.await
.unwrap();
}
#[tokio::test]
async fn a_walk_with_the_queue_wide_concurrency_spent_visits_no_partition() {
let queue = crate::sysdb::types::NewQueue {
concurrency: Some(1),
partition_concurrency: Some(1),
..crate::sysdb::types::NewQueue::new("walked")
};
let (dbos, executor, db, registered) = partitioned_backlog(queue, 50).await;
started_elsewhere(&executor, &db, "walked", "elsewhere").await;
let polled = poll_once(&executor, ®istered, &Arc::new(Running::default())).await;
assert_eq!(polled.claims, 0, "no partition should have been visited");
assert_eq!(polled.contention, None);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_walk_with_the_queue_wide_rate_limit_spent_stops_after_one_claim() {
let queue = crate::sysdb::types::NewQueue {
rate_limit: Some(crate::sysdb::types::RateLimit {
limit: 1,
period: Duration::from_secs(3600),
}),
partition_concurrency: Some(1),
..crate::sysdb::types::NewQueue::new("walked")
};
let (dbos, executor, db, registered) = partitioned_backlog(queue, 50).await;
started_elsewhere(&executor, &db, "walked", "elsewhere").await;
let polled = poll_once(&executor, ®istered, &Arc::new(Running::default())).await;
assert_eq!(
polled.claims, 1,
"the first claim's stop should end the walk"
);
assert_eq!(polled.contention, None);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_full_partition_does_not_end_the_walk() {
let queue = crate::sysdb::types::NewQueue {
concurrency: Some(100),
partition_concurrency: Some(1),
..crate::sysdb::types::NewQueue::new("walked")
};
let (dbos, executor, db, registered) = partitioned_backlog(queue, 5).await;
if matches!(db.backend(), dbos_test_support::Backend::Cockroach) {
eprintln!("skipped on CockroachDB: a claim may under-deliver");
dbos.shutdown().await;
return;
}
started_elsewhere(&executor, &db, "walked", "p-0").await;
let polled = poll_once(&executor, ®istered, &Arc::new(Running::default())).await;
assert_eq!(
polled.claims, 5,
"every partition visited, the full one included, wherever the shuffle put it"
);
assert_eq!(polled.contention, None);
let waiting: Vec<String> = sqlx::query_scalar(
r#"SELECT workflow_uuid FROM "dbos"."workflow_status"
WHERE queue_name = 'walked' AND status = 'ENQUEUED'"#,
)
.fetch_all(&db.pool().await)
.await
.unwrap();
assert_eq!(
waiting,
["wf-waiting-0"],
"only the full partition's row should still be waiting"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_walk_stops_once_the_queue_wide_room_is_claimed() {
let queue = crate::sysdb::types::NewQueue {
concurrency: Some(2),
partition_concurrency: Some(1),
..crate::sysdb::types::NewQueue::new("walked")
};
let (dbos, executor, db, registered) = partitioned_backlog(queue, 50).await;
if matches!(db.backend(), dbos_test_support::Backend::Cockroach) {
eprintln!("skipped on CockroachDB: a claim may under-deliver");
dbos.shutdown().await;
return;
}
let polled = poll_once(&executor, ®istered, &Arc::new(Running::default())).await;
assert_eq!(
polled.claims, 2,
"one claim per unit of room, and none after it is spent"
);
assert_eq!(polled.contention, None);
let claimed: i64 = sqlx::query_scalar(
r#"SELECT count(*) FROM "dbos"."workflow_status"
WHERE queue_name = 'walked' AND status <> 'ENQUEUED'"#,
)
.fetch_one(&db.pool().await)
.await
.unwrap();
assert_eq!(claimed, 2, "exactly the room the queue had");
dbos.shutdown().await;
}
#[test]
fn a_claimed_row_runs_only_while_pending_and_within_its_attempts() {
let limit = MAX_RECOVERY_ATTEMPTS + 1;
for status in [
WorkflowStatus::Cancelled,
WorkflowStatus::Enqueued,
WorkflowStatus::Success,
] {
assert_eq!(admission(status, 1), Admission::NotPending, "{status}");
assert_eq!(
admission(status, limit + 1),
Admission::NotPending,
"{status}"
);
}
assert_eq!(admission(WorkflowStatus::Pending, 1), Admission::Run);
assert_eq!(admission(WorkflowStatus::Pending, limit), Admission::Run);
assert_eq!(
admission(WorkflowStatus::Pending, limit + 1),
Admission::Park
);
}
#[test]
fn a_failed_clock_read_times_from_this_host() {
let in_a_minute = Timestamp::from_epoch_ms(Timestamp::now().as_epoch_ms() + 60_000);
let expires =
timing_clock(Err(sysdb::Error::Malformed("no clock".to_owned()))).reaches(in_a_minute);
let left = expires.saturating_duration_since(std::time::Instant::now());
assert!(
(Duration::from_secs(58)..=Duration::from_secs(60)).contains(&left),
"{left:?}"
);
let read = DatabaseTime::read(0);
assert_eq!(timing_clock(Ok(read)), read, "a reading is used as it is");
}
}