sim-time
The sim-time
feature enables time simulation capabilities in es-entity, allowing you to accelerate time for testing and development purposes. This is particularly useful for testing time-dependent logic without having to wait for real time to pass.
Enabling sim-time
Add the sim-time
feature to your es-entity dependency:
[dependencies]
es-entity = { version = "0.7", features = ["sim-time"] }
Configuration
The sim-time crate is configured through the TimeConfig
struct. By default, sim-time operates in real-time mode. To enable simulation, you need to initialize it with a configuration:
extern crate es_entity; extern crate tokio; use es_entity::prelude::{sim_time, chrono}; use std::time::Duration; #[tokio::main] async fn main() { let config = sim_time::TimeConfig { realtime: false, simulation: Some(sim_time::SimulationConfig { // Start the simulation at a specific time start_at: chrono::Utc::now(), // Real milliseconds between simulation ticks tick_interval_ms: 10, // Simulated duration per tick tick_duration_secs: Duration::from_secs(86400), // 1 day per 10ms // Whether to switch to real-time when catching up to present transform_to_realtime: false, }), }; // Initialize sim-time with the configuration sim_time::init(config); }
Configuration Parameters
TimeConfig
realtime: bool
- Whentrue
, sim-time is deactivated and all time operations use real time. Whenfalse
, simulation is enabled.simulation: Option<SimulationConfig>
- The simulation configuration. Required whenrealtime
isfalse
.
SimulationConfig
start_at: DateTime<Utc>
- The starting time for the simulation. Defaults to the current time.tick_interval_ms: u64
- The real-world milliseconds between simulation ticks.tick_duration_secs: Duration
- How much simulated time passes per tick.transform_to_realtime: bool
- Iftrue
, the simulation will automatically switch to real-time mode once it catches up to the current time.
Usage
Once configured, sim-time provides several functions that work with simulated time:
extern crate es_entity; extern crate tokio; use es_entity::prelude::{sim_time, chrono}; use std::time::Duration; #[tokio::main] async fn main() { // Initialize sim-time with the example configuration let config = sim_time::TimeConfig { realtime: false, simulation: Some(sim_time::SimulationConfig { start_at: chrono::Utc::now(), tick_interval_ms: 10, tick_duration_secs: Duration::from_secs(86400), // 1 day per 10ms transform_to_realtime: false, }), }; sim_time::init(config); // Get the current simulated time let current_time = sim_time::now(); // Sleep for a simulated duration // With the example config (1 day = 10ms), this sleeps for ~0.04 real seconds sim_time::sleep(Duration::from_secs(3600)).await; // Sleep for 1 simulated hour // Set a timeout on an operation async fn async_operation() -> Result<(), std::io::Error> { Ok(()) } sim_time::timeout(Duration::from_secs(60), async_operation()).await; // Wait until simulation catches up to real time // (only relevant if transform_to_realtime is true) sim_time::wait_until_realtime().await; }
Effect on es-entity
When sim-time is enabled, it affects how es-entity handles timestamps:
-
Database Operations: The
DbOp
struct automatically caches the simulated time when the feature is enabled. This cached time is used instead of databaseNOW()
for all write operations. -
Event Timestamps: All events created during a transaction will use the same simulated timestamp, ensuring consistency.
-
Time-based Queries: Operations that depend on the current time will use the simulated time instead of real time.
Example: Testing Time-Dependent Logic
extern crate es_entity; extern crate tokio; extern crate sqlx; extern crate serde; extern crate anyhow; use es_entity::prelude::*; use std::time::Duration; use es_entity::{EsEntity, EsEvent, EsRepo, TryFromEvents, IntoEvents, EsEntityError, EntityEvents}; use chrono::Datelike; es_entity::entity_id! { SubscriptionId } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(EsEvent)] #[serde(tag = "type")] #[es_event(id = "SubscriptionId")] enum SubscriptionEvent { Initialized { id: SubscriptionId, expires_at: chrono::DateTime<chrono::Utc> }, } #[derive(Clone, EsEntity)] struct Subscription { pub id: SubscriptionId, pub expires_at: chrono::DateTime<chrono::Utc>, pub events: EntityEvents<SubscriptionEvent>, } impl Subscription { pub fn is_expired(&self, now: chrono::DateTime<chrono::Utc>) -> bool { self.expires_at <= now } pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> { // Get the timestamp from when this entity was first persisted self.events.entity_first_persisted_at().unwrap_or_else(|| sim_time::now()) } } impl TryFromEvents<SubscriptionEvent> for Subscription { fn try_from_events(events: EntityEvents<SubscriptionEvent>) -> Result<Self, es_entity::EsEntityError> { let mut expires_at = chrono::Utc::now(); for event in events.iter_all() { match event { SubscriptionEvent::Initialized { expires_at: exp, .. } => { expires_at = *exp; } } } Ok(Self { id: events.id().clone(), expires_at, events }) } } #[derive(Debug)] struct NewSubscription { id: SubscriptionId, duration_days: i64, } impl IntoEvents<SubscriptionEvent> for NewSubscription { fn into_events(self) -> EntityEvents<SubscriptionEvent> { EntityEvents::init( self.id, [SubscriptionEvent::Initialized { id: self.id, expires_at: sim_time::now() + chrono::Duration::days(self.duration_days), }]) } } #[derive(Clone, EsRepo)] #[es_repo(entity = "Subscription", event = "SubscriptionEvent")] struct SubscriptionRepo { pool: sqlx::PgPool, } #[tokio::main] async fn main() -> anyhow::Result<()> { // Setup database connection let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://localhost/test".to_string()); let pool = sqlx::PgPool::connect(&db_url).await.unwrap(); let repo = SubscriptionRepo { pool }; // Configure time to run 30 days per second let config = sim_time::TimeConfig { realtime: false, simulation: Some(sim_time::SimulationConfig { // Start simulation one year in the past start_at: chrono::Utc::now() - chrono::Duration::days(365), tick_interval_ms: 33, // ~30 ticks per second tick_duration_secs: Duration::from_secs(86400), // 1 day per tick transform_to_realtime: false, }), }; let start_time = chrono::Utc::now() - chrono::Duration::days(365); sim_time::init(config); // Create a subscription that expires in 30 days let subscription = repo.create(NewSubscription { id: SubscriptionId::new(), duration_days: 30, }).await?; // Verify that sim-time is working let created_at = subscription.created_at(); // Verify sim-time is working by checking the entity was created in the simulated year/month assert_eq!(created_at.year(), start_time.year(), "Entity should be created in the simulated year"); assert_eq!(created_at.month(), start_time.month(), "Entity should be created in the simulated month"); // Verify that we're actually in the past (compared to real time) let real_now = chrono::Utc::now(); assert!(created_at < real_now - chrono::Duration::days(300), "Entity creation time should be in the past"); // The subscription should NOT be expired yet (30 days haven't passed in sim time) assert!(!subscription.is_expired(sim_time::now())); // Sleep for 30 simulated days (which takes ~1 real second with this config) sim_time::sleep(Duration::from_secs(30 * 86400)).await; // Check that the subscription is now expired let subscription = repo.find_by_id(subscription.id).await?; assert!(subscription.is_expired(sim_time::now())); Ok(()) }
Best Practices
-
Initialize Early: Call
sim_time::init()
before any other es-entity operations to ensure consistent time handling. -
Use in Tests: The sim-time feature is primarily designed for testing. Consider using conditional compilation to only enable it in test builds:
[dev-dependencies] es-entity = { version = "0.7", features = ["sim-time"] }
-
Consistent Time: All operations within a single database transaction will use the same timestamp, ensuring consistency in your event store.
-
Real-time Transformation: Use
transform_to_realtime: true
when you want to start a simulation in the past and have it automatically switch to real-time when it catches up. This is useful for replaying historical scenarios.