12 releases (6 breaking)
Uses new Rust 2024
| 0.7.0 | Oct 7, 2025 |
|---|---|
| 0.6.1 | Jun 17, 2025 |
| 0.6.0 | Apr 25, 2025 |
| 0.5.1 | Dec 9, 2024 |
| 0.2.0 | Nov 6, 2023 |
#863 in Game dev
163 downloads per month
41KB
186 lines
bevy_cronjob
A simple, efficient, and reliable helper for running scheduled tasks (cronjobs) in Bevy applications.
โจ Features
- ๐ Flexible Scheduling: Support for both cron expressions and natural English descriptions
- ๐ฏ Dual Usage Patterns: System run conditions or component-based scheduling with observers
- ๐ Local Timezone: All schedules operate in local timezone for intuitive behavior
- ๐ Performance Optimized: Efficient batching, minimal allocations, and smart state management
- ๐ง Easy Integration: Simple plugin setup with comprehensive examples
- ๐ก๏ธ Reliable: Fixed initialization bugs ensuring schedules work from the first frame
- ๐ Well Documented: Extensive documentation with practical examples
๐ Quick Start
Add to your Cargo.toml:
[dependencies]
bevy_cronjob = "0.6"
Basic usage:
use bevy::prelude::*;
use bevy_cronjob::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(CronJobPlugin)
.add_systems(Startup, setup)
.add_systems(
Update,
my_system.run_if(schedule_passed("every 5 seconds")),
)
.run();
}
fn setup(mut commands: Commands) {
commands
.spawn(ScheduleTimer::new("every 10 seconds"))
.observe(|_: On<ScheduleArrived>| {
info!("Timer triggered!");
});
}
fn my_system() {
info!("This runs every 5 seconds");
}
๐ Usage Patterns
Pattern 1: System Run Conditions (Simple & Lightweight)
Perfect for simple, stateless scheduled tasks:
use bevy_cronjob::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(CronJobPlugin)
.add_systems(
Update,
(
save_game.run_if(schedule_passed("every 30 seconds")),
cleanup_cache.run_if(schedule_passed("every 5 minutes")),
daily_backup.run_if(schedule_passed("every day at 3 am")),
),
)
.run();
}
fn save_game() {
info!("Auto-saving game...");
}
fn cleanup_cache() {
info!("Cleaning up cache...");
}
fn daily_backup() {
info!("Running daily backup...");
}
Pros: Minimal setup, integrates seamlessly with Bevy's scheduling Cons: Each run condition maintains separate state
Pattern 2: Component-Based with Observers (Flexible & Powerful)
Ideal for complex scheduling needs with entity-specific logic:
use bevy_cronjob::prelude::*;
#[derive(Component)]
struct GameStats {
score: u32,
level: u32,
}
fn setup(mut commands: Commands) {
// Create entities with different schedules
commands
.spawn((
ScheduleTimer::new("every 1 minute"),
GameStats { score: 0, level: 1 },
Name::new("Score Reporter"),
))
.observe(report_score);
commands
.spawn((
ScheduleTimer::new("every 10 seconds"),
Name::new("Health Monitor"),
))
.observe(check_player_health);
}
fn report_score(
trigger: On<ScheduleArrived>,
query: Query<&GameStats>,
) {
if let Ok(stats) = query.get(trigger.event().entity) {
info!("Current score: {}, Level: {}", stats.score, stats.level);
}
}
fn check_player_health(trigger: On<ScheduleArrived>) {
info!("Checking player health for entity: {:?}", trigger.event().entity);
}
Pros: Entity-specific data, flexible event handling, better for complex logic Cons: Slightly more setup required
๐ Schedule Expression Formats
Cron Expressions
Standard cron syntax based on the cron crate:
| Field | Values | Description |
|---|---|---|
| Second | 0-59 | Seconds |
| Minute | 0-59 | Minutes |
| Hour | 0-23 | Hours (24-hour format) |
| Day of Month | 1-31 | Day of the month |
| Month | 1-12 | Month (1=January, 12=December) |
| Day of Week | 1-7 | Day of the week (1=Monday) |
| Year | 1970-2100 | Year (optional) |
Special Characters:
*- Any value?- No specific value (for day fields)/- Step values (e.g.,0/5= every 5 units),- List separator (e.g.,1,3,5)-- Range (e.g.,1-5)
Examples:
"0/5 * * * * ? *" // Every 5 seconds
"0 * * * * ? *" // Every minute at second 0
"0 0 * * * ? *" // Every hour at minute 0
"0 0 0 * * ? *" // Every day at midnight
"0 0 9 * * MON-FRI *" // Every weekday at 9 AM
"0 0 0 1 * ? *" // First day of every month
"0 30 14 * * ? *" // Every day at 2:30 PM
English Expressions
Natural language scheduling powered by english-to-cron:
| English Expression | Equivalent Cron | Description |
|---|---|---|
every 15 seconds |
0/15 * * * * ? * |
Every 15 seconds |
every minute |
0 * * * * ? * |
Every minute |
every hour |
0 0 * * * ? * |
Every hour |
every day |
0 0 0 */1 * ? * |
Every day at midnight |
every day at 4:00 pm |
0 0 16 */1 * ? * |
Every day at 4 PM |
at 10:00 am |
0 0 10 * * ? * |
Every day at 10 AM |
run at midnight on the 1st and 15th of month |
0 0 0 1,15 * ? * |
1st and 15th of each month |
On Sunday at 12:00 |
0 0 12 ? * SUN * |
Every Sunday at noon |
7pm every Thursday |
0 0 19 ? * THU * |
Every Thursday at 7 PM |
midnight on Tuesdays |
0 0 0 ? * TUE * |
Every Tuesday at midnight |
๐ง Predefined Constants
Common schedules are available as constants for convenience:
use bevy_cronjob::*;
// Frequent intervals
EVERY_5_SEC // "0/5 * * * * ? *"
EVERY_10_SEC // "0/10 * * * * ? *"
EVERY_30_SEC // "0/30 * * * * ? *"
EVERY_MIN // "0 * * * * ? *"
EVERY_5_MIN // "0 0/5 * * * ? *"
EVERY_30_MIN // "0 0/30 * * * ? *"
EVERY_HOUR // "0 0 * * * ? *"
EVERY_DAY // "0 0 0 */1 * ? *"
// Specific daily times
EVERY_1_AM // "0 0 1 */1 * ? *"
EVERY_6_AM // "0 0 6 */1 * ? *"
EVERY_12_PM // "0 0 12 */1 * ? *"
EVERY_6_PM // "0 0 18 */1 * ? *"
EVERY_11_PM // "0 0 23 */1 * ? *"
// ... and many more
Usage:
.add_systems(
Update,
backup_system.run_if(schedule_passed(EVERY_DAY)),
)
๐ฏ Advanced Examples
Game-Specific Scheduling
use bevy_cronjob::prelude::*;
#[derive(Component)]
struct Enemy;
#[derive(Component)]
struct SpawnTimer;
fn setup_game_schedules(mut commands: Commands) {
// Spawn enemies every 30 seconds
commands
.spawn((
ScheduleTimer::new("every 30 seconds"),
SpawnTimer,
Name::new("Enemy Spawner"),
))
.observe(spawn_enemy_wave);
// Save game progress every 5 minutes
commands
.spawn(ScheduleTimer::new("every 5 minutes"))
.observe(|_: On<ScheduleArrived>| {
info!("Auto-saving game progress...");
// Save game logic here
});
// Daily challenges reset at midnight
commands
.spawn(ScheduleTimer::new("every day at 12 am"))
.observe(reset_daily_challenges);
// Weekend bonus events
commands
.spawn(ScheduleTimer::new("0 0 18 ? * FRI *")) // Friday 6 PM
.observe(|_: On<ScheduleArrived>| {
info!("Weekend bonus event started!");
});
}
fn spawn_enemy_wave(
trigger: On<ScheduleArrived>,
mut commands: Commands,
) {
info!("Spawning enemy wave for spawner: {:?}", trigger.event().entity);
// Spawn multiple enemies
for i in 0..5 {
commands.spawn((
Enemy,
Name::new(format!("Enemy-{}", i)),
// Add enemy components...
));
}
}
fn reset_daily_challenges(trigger: On<ScheduleArrived>) {
info!("Resetting daily challenges...");
// Reset challenge progress
}
Server Maintenance Scheduling
use bevy_cronjob::prelude::*;
fn setup_server_maintenance(mut commands: Commands) {
// Log server stats every minute
commands
.spawn(ScheduleTimer::new("every minute"))
.observe(log_server_stats);
// Clean up disconnected players every 5 minutes
commands
.spawn(ScheduleTimer::new("every 5 minutes"))
.observe(cleanup_disconnected_players);
// Database backup every day at 3 AM
commands
.spawn(ScheduleTimer::new("every day at 3 am"))
.observe(backup_database);
// Weekly server restart (Sunday 4 AM)
commands
.spawn(ScheduleTimer::new("0 0 4 ? * SUN *"))
.observe(schedule_server_restart);
}
fn log_server_stats(trigger: On<ScheduleArrived>) {
info!("Server uptime check - Entity: {:?}", trigger.event().entity);
// Log memory usage, player count, etc.
}
fn cleanup_disconnected_players(trigger: On<ScheduleArrived>) {
info!("Cleaning up disconnected players...");
// Remove inactive player entities
}
fn backup_database(trigger: On<ScheduleArrived>) {
info!("Starting database backup...");
// Backup logic
}
fn schedule_server_restart(trigger: On<ScheduleArrived>) {
info!("Scheduling server restart for maintenance...");
// Graceful restart logic
}
Dynamic Scheduling
use bevy_cronjob::prelude::*;
#[derive(Component)]
struct DifficultyLevel(u32);
fn setup_dynamic_scheduling(mut commands: Commands) {
commands.spawn((
DifficultyLevel(1),
Name::new("Game Manager"),
));
}
fn adjust_spawn_rate(
mut commands: Commands,
query: Query<(Entity, &DifficultyLevel)>,
existing_timers: Query<Entity, With<ScheduleTimer>>,
) {
for (entity, difficulty) in query.iter() {
// Remove old spawn timers
for timer_entity in existing_timers.iter() {
commands.entity(timer_entity).despawn();
}
// Create new timer based on difficulty
let spawn_interval = match difficulty.0 {
1 => "every 30 seconds",
2 => "every 20 seconds",
3 => "every 10 seconds",
4 => "every 5 seconds",
_ => "every 2 seconds",
};
commands
.spawn(ScheduleTimer::new(spawn_interval))
.observe(move |_: On<ScheduleArrived>| {
info!("Spawning enemies at difficulty level {}", difficulty.0);
});
}
}
๐ก๏ธ Error Handling and Validation
Safe Schedule Creation
use cron::Schedule;
use std::str::FromStr;
use bevy_cronjob::*;
fn safe_schedule_creation(expression: &str) -> Result<ScheduleTimer, String> {
// Parse expression first (handles English to cron conversion)
let cron_expr = if expression.chars().any(|c| c.is_ascii_alphabetic()) {
str_cron_syntax(expression)
.map_err(|e| format!("Invalid English expression '{}': {}", expression, e))?
} else {
expression.to_string()
};
// Validate cron expression
Schedule::from_str(&cron_expr)
.map_err(|e| format!("Invalid cron expression '{}': {}", cron_expr, e))?;
Ok(ScheduleTimer::new(expression))
}
fn setup_with_validation(mut commands: Commands) {
match safe_schedule_creation("every 5 seconds") {
Ok(timer) => {
commands
.spawn(timer)
.observe(|_: On<ScheduleArrived>| {
info!("Safe timer triggered!");
});
}
Err(e) => {
error!("Failed to create schedule: {}", e);
}
}
}
Runtime Schedule Management
use bevy_cronjob::prelude::*;
#[derive(Component)]
struct ScheduleConfig {
expression: String,
enabled: bool,
}
fn manage_schedules_system(
mut commands: Commands,
config_query: Query<(Entity, &ScheduleConfig), Changed<ScheduleConfig>>,
timer_query: Query<Entity, With<ScheduleTimer>>,
) {
for (entity, config) in config_query.iter() {
// Remove existing timer if any
if let Ok(timer_entity) = timer_query.get(entity) {
commands.entity(timer_entity).despawn();
}
// Add new timer if enabled
if config.enabled {
match safe_schedule_creation(&config.expression) {
Ok(timer) => {
commands.entity(entity).insert(timer);
}
Err(e) => {
warn!("Invalid schedule for entity {:?}: {}", entity, e);
}
}
}
}
}
๐ Performance Optimizations
The crate includes several performance optimizations for production use:
1. Fixed Initialization Logic
- Problem: Previous versions had a bug where schedules wouldn't trigger on the first check
- Solution: Proper state initialization handling for immediate and future triggers
2. Efficient Time Calculations
- Uses optimized schedule queries for better performance
- Smart state management to minimize unnecessary calculations
3. Batched Event Processing
- Multiple triggered schedules are processed in a single batch operation
- Reduces system call overhead for high-frequency schedules
4. Memory Efficient
- Expression parsing done once during setup, not on every check
- Minimal allocations during runtime execution
5. Scalable Architecture
- Handles many concurrent schedules efficiently
- Iterator-based processing for better CPU cache usage
๐งช Testing
The crate includes comprehensive tests. Run them with:
cargo test
For manual testing, run the examples:
# Basic functionality
cargo run --example cronjobs
# Performance demonstration
cargo run --example performance_demo
# Trigger testing
cargo run --example trigger_test
๐ Migration Guide
From Bevy 0.16 to 0.17
Bevy 0.17 introduced changes to the event system. Here's what you need to update:
Observer Function Signatures:
// Old (Bevy 0.16)
.observe(|trigger: Trigger<ScheduleArrived>| {
info!("Entity: {:?}", trigger.target());
})
// New (Bevy 0.17)
.observe(|trigger: On<ScheduleArrived>| {
info!("Entity: {:?}", trigger.event().entity);
})
Key Changes:
Trigger<T>renamed toOn<T>trigger.target()changed totrigger.event().entityScheduleArrivednow usesEntityEventderive macro
From v0.5.x to v0.6.x
No breaking changes! The API remains the same, but with important improvements:
- Bug Fix: Schedules now work correctly from the first frame
- Performance: Better batching and time calculations
- Documentation: Comprehensive English documentation
Simply update your Cargo.toml:
[dependencies]
bevy_cronjob = "0.6"
๐ Supported Bevy Versions
| Bevy Version | bevy_cronjob Version |
|---|---|
| 0.17 | 0.6 |
| 0.16 | 0.5 |
| 0.15 | 0.4 |
| 0.14 | 0.3 |
๐ค Contributing
Contributions are welcome! Here's how you can help:
- Report Bugs: Open an issue with a clear description and minimal reproduction case
- Suggest Features: Propose new features with use cases and API design
- Submit PRs: Fork, create a feature branch, and submit a pull request
- Improve Docs: Help make the documentation even better
Development Setup
git clone https://github.com/foxzool/bevy_cronjob.git
cd bevy_cronjob
cargo test
cargo run --example cronjobs
Guidelines
- Follow Rust naming conventions
- Add tests for new features
- Update documentation for API changes
- Keep performance in mind for runtime code
๐ License
This project is dual-licensed under either:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
You may choose either license when using this crate in your projects.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
๐ Additional Resources
- Cron Expression Reference - Interactive cron expression builder
- Bevy Documentation - Learn more about Bevy
- Examples Directory - More usage examples
- API Documentation - Complete API reference
Made with โค๏ธ for the Bevy community
Dependencies
~19MB
~340K SLoC