use std::{
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
thread,
time::Duration,
};
use reifydb_catalog::MaterializedCatalog;
use reifydb_core::{event::EventBus, interceptor::StandardInterceptorFactory};
use reifydb_engine::StandardEngine;
use reifydb_store_transaction::TransactionStore;
use reifydb_sub_api::{ClosureTask, Priority, Scheduler, Subsystem};
use reifydb_sub_worker::{WorkerConfig, WorkerSubsystem};
use reifydb_transaction::{cdc::TransactionCdc, multi::Transaction, single::TransactionSingleVersion};
use reifydb_type::{diagnostic::internal, error};
fn create_test_engine() -> StandardEngine {
let store = TransactionStore::testing_memory();
let eventbus = EventBus::new();
let single = TransactionSingleVersion::svl(store.clone(), eventbus.clone());
let cdc = TransactionCdc::new(store.clone());
let multi = Transaction::new(store, single.clone(), eventbus.clone());
StandardEngine::new(
multi,
single,
cdc,
eventbus,
Box::new(StandardInterceptorFactory::default()),
MaterializedCatalog::new(),
)
}
#[test]
fn test_schedule_every_basic_interval_execution() {
let engine = create_test_engine();
let mut instance = WorkerSubsystem::new(WorkerConfig::default(), engine);
assert!(instance.start().is_ok());
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let task = Box::new(ClosureTask::new("interval_task", Priority::Normal, move |_ctx| {
counter_clone.fetch_add(1, Ordering::Relaxed);
Ok(())
}));
let handle = instance.every(Duration::from_millis(30), task).unwrap();
let mut attempts = 0;
let max_attempts = 20; while counter.load(Ordering::Relaxed) < 3 && attempts < max_attempts {
thread::sleep(Duration::from_millis(10));
attempts += 1;
}
let count = counter.load(Ordering::Relaxed);
assert!(count >= 3, "Expected at least 3 executions, got {} after {} attempts", count, attempts);
assert!(instance.cancel(handle).is_ok());
thread::sleep(Duration::from_millis(50));
let count_after_cancel = counter.load(Ordering::Relaxed);
thread::sleep(Duration::from_millis(100));
assert_eq!(counter.load(Ordering::Relaxed), count_after_cancel, "Task should not execute after cancellation");
assert!(instance.shutdown().is_ok());
}
#[test]
fn test_schedule_every_priority_ordering() {
let engine = create_test_engine();
let mut instance = WorkerSubsystem::new(
WorkerConfig {
num_workers: 1, max_queue_size: 100,
scheduler_interval: Duration::from_millis(10),
task_timeout_warning: Duration::from_secs(1),
},
engine,
);
assert!(instance.start().is_ok());
let execution_order = Arc::new(Mutex::new(Vec::new()));
let high_order = Arc::clone(&execution_order);
let high_task = Box::new(ClosureTask::new("high_priority_interval", Priority::High, move |_ctx| {
high_order.lock().unwrap().push("high");
Ok(())
}));
let low_order = Arc::clone(&execution_order);
let low_task = Box::new(ClosureTask::new("low_priority_interval", Priority::Low, move |_ctx| {
low_order.lock().unwrap().push("low");
Ok(())
}));
let _high_handle = instance.every(Duration::from_millis(50), high_task).unwrap();
let _low_handle = instance.every(Duration::from_millis(50), low_task).unwrap();
thread::sleep(Duration::from_millis(200));
let order = execution_order.lock().unwrap();
assert!(!order.is_empty(), "Tasks should have executed");
if order.len() >= 2 {
let first_high = order.iter().position(|s| *s == "high");
let first_low = order.iter().position(|s| *s == "low");
if let (Some(high_pos), Some(low_pos)) = (first_high, first_low) {
if (high_pos as isize - low_pos as isize).abs() == 1 {
assert!(
high_pos < low_pos,
"High priority should execute before low when both are ready"
);
}
}
}
assert!(instance.shutdown().is_ok());
}
#[test]
fn test_schedule_every_cancellation() {
let engine = create_test_engine();
let mut instance = WorkerSubsystem::new(WorkerConfig::default(), engine);
assert!(instance.start().is_ok());
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let task = Box::new(ClosureTask::new("test_task", Priority::Normal, move |_ctx| {
counter_clone.fetch_add(1, Ordering::Relaxed);
Ok(())
}));
let handle = instance.every(Duration::from_millis(20), task).unwrap();
thread::sleep(Duration::from_millis(100));
let count_before_cancel = counter.load(Ordering::Relaxed);
assert!(count_before_cancel > 0, "Task should have executed at least once");
assert!(instance.cancel(handle).is_ok());
thread::sleep(Duration::from_millis(100));
let count_after_cancel = counter.load(Ordering::Relaxed);
assert!(count_after_cancel <= count_before_cancel + 1, "Task should stop executing after cancellation");
assert!(instance.shutdown().is_ok());
}
#[test]
fn test_schedule_every_multiple_intervals() {
let engine = create_test_engine();
let mut instance = WorkerSubsystem::new(
WorkerConfig {
num_workers: 2,
max_queue_size: 100,
scheduler_interval: Duration::from_millis(10),
task_timeout_warning: Duration::from_secs(1),
},
engine,
);
assert!(instance.start().is_ok());
let counter1 = Arc::new(AtomicUsize::new(0));
let counter2 = Arc::new(AtomicUsize::new(0));
let counter1_clone = Arc::clone(&counter1);
let task1 = Box::new(ClosureTask::new("high_priority_interval", Priority::High, move |_ctx| {
counter1_clone.fetch_add(1, Ordering::Relaxed);
Ok(())
}));
let counter2_clone = Arc::clone(&counter2);
let task2 = Box::new(ClosureTask::new("normal_priority_interval", Priority::Normal, move |_ctx| {
counter2_clone.fetch_add(1, Ordering::Relaxed);
Ok(())
}));
let _handle1 = instance.every(Duration::from_millis(30), task1).unwrap();
let _handle2 = instance.every(Duration::from_millis(60), task2).unwrap();
thread::sleep(Duration::from_millis(200));
let count1 = counter1.load(Ordering::Relaxed);
let count2 = counter2.load(Ordering::Relaxed);
assert!(count1 > 0, "Task1 should have executed");
assert!(count2 > 0, "Task2 should have executed");
assert!(
count1 >= count2,
"Task1 (30ms) should execute at least as often as Task2 (60ms). Count1: {}, Count2: {}",
count1,
count2
);
assert!(instance.shutdown().is_ok());
}
#[test]
fn test_schedule_every_task_failure_recovery() {
let engine = create_test_engine();
let mut instance = WorkerSubsystem::new(WorkerConfig::default(), engine);
assert!(instance.start().is_ok());
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let task = Box::new(ClosureTask::new("failing_task", Priority::Normal, move |_ctx| {
let count = counter_clone.fetch_add(1, Ordering::Relaxed);
if count % 2 == 0 {
Ok(())
} else {
Err(error!(internal("test error")))
}
}));
let _handle = instance.every(Duration::from_millis(30), task).unwrap();
thread::sleep(Duration::from_millis(200));
let final_count = counter.load(Ordering::Relaxed);
assert!(final_count >= 3, "Task should continue executing despite failures. Count: {}", final_count);
assert!(instance.shutdown().is_ok());
}