Task Scheduling
Oxide’s scheduler turns a single cron invocation (oxide schedule:run,
called every minute) into a registry of timed tasks declared in Rust.
Any console command you register can be scheduled — built-ins like
auth:prune-tokens, or custom Command impls you wrote.
Declare the schedule
Implement the schedule hook on any ServiceProvider. The idiomatic
place is ConsoleServiceProvider:
use oxide::http::{Application, ServiceProvider};use oxide::Schedule;
pub struct ConsoleServiceProvider;
impl ServiceProvider for ConsoleServiceProvider { fn register(&self, app: &Application) { app.command(SyncProductCommand); }
fn schedule(&self, sched: &mut Schedule) { sched.command("auth:prune-tokens").daily(); sched.command("sync:product").hourly(); sched.command("report:weekly").cron("0 9 * * 1"); sched.command("sync:product").with_args(["--force"]).every_fifteen_minutes(); }}Wire the cron entry
Add one line to your crontab (crontab -e):
* * * * * cd /path/to/your/app && oxide schedule:run >> storage/logs/schedule.log 2>&1Cron invokes oxide schedule:run every minute. Inside the binary,
Application::schedules() collects every provider’s schedule, tests
each entry’s cron expression against the current minute, and dispatches
matches via Application::run_command.
Timing helpers
Any ScheduleEntry supports a Laravel-compatible fluent API. Every
method returns &mut Self so they chain:
| Method | Cron equivalent |
|---|---|
.every_minute() | * * * * * |
.every_five_minutes() | */5 * * * * |
.every_fifteen_minutes() | */15 * * * * |
.every_thirty_minutes() | 0,30 * * * * |
.hourly() | 0 * * * * |
.daily() | 0 0 * * * |
.daily_at("02:30") | 30 2 * * * |
.weekly() | 0 0 * * 0 |
.monthly() | 0 0 1 * * |
.cron("min hour dom month dow") | raw Laravel-style 5-field |
All times are interpreted in UTC. Convert to a specific zone inside the command if needed.
Passing arguments
with_args takes anything that iterates into String:
sched.command("sync:product") .with_args(["--force", "--limit=500"]) .every_fifteen_minutes();The args are passed through to Command::handle(&self, args: Vec<String>)
exactly as if you’d typed oxide sync:product --force --limit=500.
Inspect the registered schedule
Call application.schedules() anywhere you have an &Application, for
example inside a diagnostic command:
for entry in app.schedules().entries() { println!("{} {:<30} args={:?}", entry.cron, entry.signature, entry.args);}What schedule:run does
Every invocation:
- Collects schedules from every registered provider’s
schedule()hook. - Truncates the current UTC time to the minute.
- For each entry, parses the cron expression and checks if the next
firing after
(now - 1s)equals the current minute. - Matches run via
Application::run_command. Results are logged atinfo(success) orwarn/error(failure) viatracing. - Exit code:
0if every dispatched task returned0, otherwise1.
Operational tips
- Don’t let
schedule:runoverrun. Cron invokes it every minute; if your tasks take longer than a minute, they can pile up. Move long-running work into a background queue and have the scheduled task only enqueue. - Logging. Redirect both stdout and stderr from cron:
otherwise>> storage/logs/schedule.log 2>&1
cronemails any stderr output to the system mail spool. - Monitoring. Point a watchdog (Dead Man’s Snitch, Uptime Kuma) at
a healthcheck ping emitted from inside
schedule:runso you notice when cron itself dies.
What’s not here yet
- Overlap locks.
.without_overlapping()is on the roadmap and will use the forthcoming cache layer for atomic lock acquisition. For now, design tasks to be idempotent. - Conditional execution.
.when(|| bool)/.skip(|| bool)— coming alongside the cache layer. - Sub-minute granularity. Cron itself is minute-granularity; we
don’t yet loop internally inside
schedule:runto fire multiple times per minute. Open an issue if you need it.