Clock Module
The es_entity::clock module provides a time abstraction that works identically whether using real time or artificial time for testing. This enables deterministic testing of time-dependent logic without waiting for real time to pass.
Overview
The clock module provides:
ClockHandle- A cheap-to-clone handle for time operationsClock- Global clock access (likeUtc::now()but testable)ClockController- Control artificial time advancementArtificialClockConfig- Configure artificial clock behavior
Clock Types
Realtime Clock
Uses the system clock and tokio timers. This is the default behavior.
use es_entity::clock::ClockHandle;
let clock = ClockHandle::realtime();
let now = clock.now(); // Returns Utc::now()
Artificial Clock
Time only advances when explicitly controlled. Perfect for deterministic testing.
use es_entity::clock::{ClockHandle, ArtificialClockConfig};
// Create artificial clock with manual advancement
let (clock, ctrl) = ClockHandle::artificial(ArtificialClockConfig::manual());
let t0 = clock.now();
// Time doesn't advance on its own
assert_eq!(clock.now(), t0);
// Advance time by 1 hour
ctrl.advance(Duration::from_secs(3600)).await;
assert_eq!(clock.now(), t0 + chrono::Duration::hours(1));
Global Clock API
The Clock struct provides static methods for global clock access, similar to Utc::now():
use es_entity::clock::{Clock, ArtificialClockConfig};
// For testing: install artificial clock (returns controller)
let ctrl = Clock::install_artificial(ArtificialClockConfig::manual());
// Get current time (works with both artificial and real time)
let now = Clock::now();
// Check if artificial clock is installed
if Clock::is_artificial() {
// We're in test mode with controlled time
}
// Sleep and timeout also use the global clock
Clock::sleep(Duration::from_secs(60)).await;
Clock::timeout(Duration::from_secs(5), some_future).await;
Lazy Initialization
If you call Clock::now() without installing an artificial clock, it lazily initializes to realtime mode. This means production code can use Clock::now() without any setup.
ArtificialClockConfig
Configure how the artificial clock behaves:
use es_entity::clock::{ArtificialClockConfig, ArtificialMode};
use chrono::Utc;
// Manual mode - time only advances via controller.advance()
let config = ArtificialClockConfig::manual();
// Auto mode - time advances automatically at 1000x speed
let config = ArtificialClockConfig::auto(1000.0);
// Start at a specific time
let config = ArtificialClockConfig {
start_at: Utc::now() - chrono::Duration::days(30),
mode: ArtificialMode::Manual,
};
ClockController
The controller is returned when creating an artificial clock and provides:
// Advance time by duration (wakes sleeping tasks in order)
ctrl.advance(Duration::from_secs(3600)).await;
// Advance to next pending wake event
let wake_time = ctrl.advance_to_next_wake().await;
// Set time directly (doesn't process intermediate wakes)
ctrl.set_time(some_datetime);
// Get current time
let now = ctrl.now();
// Check pending sleep count
let count = ctrl.pending_wake_count();
// Transition to realtime mode
ctrl.transition_to_realtime();
Integration with DbOp
When a global artificial clock is installed, database operations automatically use it:
use es_entity::clock::{Clock, ArtificialClockConfig};
// Install artificial clock for testing
let ctrl = Clock::install_artificial(ArtificialClockConfig::manual());
// DbOp::init() now caches the artificial time
let op = DbOp::init(&pool).await?;
// with_clock_time() uses the operation's clock
let op_with_time = op.with_clock_time();
// with_db_time() uses artificial time instead of SELECT NOW()
let op_with_time = op.with_db_time().await?;
This ensures all operations within a transaction use consistent, controlled time.
Explicit Clock Injection
For more control, you can inject a specific clock into database operations without modifying global state. This is useful when you want isolated clocks per test or need different clocks for different operations:
use es_entity::clock::{ClockHandle, ArtificialClockConfig};
// Create an artificial clock (not installed globally)
let (clock, ctrl) = ClockHandle::artificial(ArtificialClockConfig::manual());
// Pass the clock explicitly to DbOp
let op = DbOp::init_with_clock(&pool, &clock).await?;
// The operation uses this clock for time operations
let op_with_time = op.with_clock_time();
Repositories generated with #[derive(EsRepo)] also support this pattern:
// Using the repo's begin_op_with_clock method
let mut op = users.begin_op_with_clock(&clock).await?;
// Create entity - recorded_at will use the artificial clock's time
let user = users.create_in_op(&mut op, new_user).await?;
op.commit().await?;
This approach avoids global state and allows each test to have its own independent clock, preventing test interference.
Clock Field in Repository
For an even cleaner API, you can add a clock field to your repository struct. The macro supports two patterns:
Optional Clock Field
Use Option<ClockHandle> when you want the same repo type to work both with and without a custom clock:
use es_entity::{clock::ClockHandle, EsRepo};
use sqlx::PgPool;
#[derive(EsRepo)]
#[es_repo(entity = "User")]
pub struct Users {
pool: PgPool,
clock: Option<ClockHandle>, // Optional: use if Some, fallback to global
}
impl Users {
// Production: no clock, uses global
pub fn new(pool: PgPool) -> Self {
Self { pool, clock: None }
}
// Testing: with artificial clock
pub fn with_clock(pool: PgPool, clock: ClockHandle) -> Self {
Self { pool, clock: Some(clock) }
}
}
Usage:
// Production code - uses global clock
let users = Users::new(pool);
let user = users.create(new_user).await?;
// Test code - uses artificial clock
let (clock, ctrl) = ClockHandle::artificial(ArtificialClockConfig::manual());
let users = Users::with_clock(pool, clock);
let user = users.create(new_user).await?; // Uses artificial clock!
Required Clock Field
Use ClockHandle (non-optional) when you always want to inject a clock:
#[derive(EsRepo)]
#[es_repo(entity = "User")]
pub struct Users {
pool: PgPool,
clock: ClockHandle, // Required: always use this clock
}
impl Users {
pub fn new(pool: PgPool, clock: ClockHandle) -> Self {
Self { pool, clock }
}
}
This is useful when you want to enforce clock injection at construction time, making the dependency explicit.
Field Detection
The macro detects a field named clock (or marked with #[es_repo(clock)]) and generates the appropriate begin_op() implementation:
Option<ClockHandle>: Uses the clock ifSome, falls back to global clock ifNoneClockHandle: Always uses the injected clock- No clock field: Always uses the global clock
Example: Testing Time-Dependent Logic
use es_entity::clock::{Clock, ArtificialClockConfig};
use std::time::Duration;
#[tokio::test]
async fn test_subscription_expiry() {
// Install artificial clock starting 30 days ago
let start = Utc::now() - chrono::Duration::days(30);
let ctrl = Clock::install_artificial(ArtificialClockConfig {
start_at: start,
mode: ArtificialMode::Manual,
});
// Create subscription that expires in 7 days
let subscription = create_subscription_expiring_in(7).await;
// Not expired yet
assert!(!subscription.is_expired(Clock::now()));
// Advance 8 days
ctrl.advance(Duration::from_secs(8 * 86400)).await;
// Now expired
assert!(subscription.is_expired(Clock::now()));
}
Best Practices
-
Use
Clock::now()instead ofUtc::now()- This makes your code testable with artificial time. -
Install artificial clock early in tests - Call
Clock::install_artificial()before any code that uses time. -
Use manual mode for deterministic tests - Auto mode is useful for simulations but manual mode gives you full control.
-
Advance time explicitly - In tests, use
ctrl.advance()to move time forward in a controlled way. -
Check
is_artificial()sparingly - Most code shouldn’t need to know if time is artificial; it should just useClock::now().