⏰ RailsCron
First release 0.1.0 is empty gem. 🕒 A scheduler-agnostic, multi-node-safe cron runner for Ruby and Rails — with Redis or Postgres advisory locks.
✨ Overview
rails_cron lets you bind cron expressions to Ruby code or shell commands —
without depending on any specific job system or scheduler (like Sidekiq-Cron or Rufus).
It guarantees that:
- Each scheduled tick enqueues work exactly once across all running Ruby nodes.
- You remain agnostic to your background job system (
ActiveJob,Sidekiq,Resque, etc.). - Locks are coordinated safely via Redis or PostgreSQL advisory locks.
- Cron syntax can be validated, linted, humanized, and translated with i18n.
🧩 Why RailsCron?
| Problem | RailsCron Solution |
|---|---|
| Multiple nodes running the same cron | Distributed locks → exactly-once execution |
| Cron syntax not human-friendly | Built-in parser + to_human translations |
| Missed runs during downtime | Configurable lookback window replays missed ticks |
| Coupled to job system | Scheduler-agnostic, works with any Ruby queue backend |
⚙️ Installation
Add to your Gemfile:
gem "rails_cron"
Then run:
bundle install
bin/rails g rails_cron:install
Example initializer (config/initializers/rails_cron.rb):
RailsCron.configure do |c|
# Choose your distributed lock adapter
# Redis (recommended)
# c.lock_adapter = RailsCron::Lock::Redis.new(url: ENV["REDIS_URL"])
# or Postgres advisory locks
# c.lock_adapter = RailsCron::Lock::Postgres.new(connection: ActiveRecord::Base.connection)
c.tick_interval = 5 # seconds between scheduler ticks
c.window_lookback = 120 # recover missed runs (seconds)
c.lease_ttl = 60 # lock TTL in seconds
end
👉 See full installation guide →
🚀 Quick Start
Register a scheduled job anywhere during boot (e.g., config/initializers/rails_cron_jobs.rb):
RailsCron.register(
key: "reports:weekly_summary",
cron: "0 9 * * 1", # every Monday at 9 AM
enqueue: ->(fire_time:, idempotency_key:) {
WeeklySummaryJob.perform_later(fire_time: fire_time, key: idempotency_key)
}
)
Start the scheduler:
bundle exec rails rails_cron:start
💡 Recommended: run as a dedicated process in production (Procfile, systemd, Kubernetes).
🧰 CLI & Rake Tasks
CLI Examples
$ rails-cron explain "*/15 * * * *"
Every 15 minutes
$ rails-cron next "0 9 * * 1" --count 3
2025-11-03 09:00:00 UTC
2025-11-10 09:00:00 UTC
2025-11-17 09:00:00 UTC
Rails Tasks
bin/rails rails_cron:start # Start scheduler loop
bin/rails rails_cron:status # Show registry & configuration
bin/rails rails_cron:tick # Trigger one tick manually
bin/rails rails_cron:explain["*/5 * * * *"] # Humanize cron expression
🧠 Cron Utilities
RailsCron.valid?("0 * * * *") # => true
RailsCron.simplify("0 0 * * *") # => "@daily"
RailsCron.lint("*/61 * * * *") # => ["invalid minute step: 61"]
I18n.locale = :fr
RailsCron.to_human("0 9 * * 1") # => "À 09h00 chaque lundi"
🈶 All tokens are i18n-based — override translations under
rails_cron.*keys.
🧱 Running the Scheduler
Procfile (Heroku / Foreman):
web: bundle exec puma -C config/puma.rb
scheduler: bundle exec rails rails_cron:start
systemd unit:
[Service]
Type=simple
WorkingDirectory=/var/apps/myapp/current
ExecStart=/usr/bin/bash -lc 'bundle exec rails rails_cron:start'
Restart=always
Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: rails-cron
spec:
replicas: 1
template:
spec:
containers:
- name: scheduler
image: your-app:latest
command: ["bash", "-lc", "bundle exec rails rails_cron:start"]
🔍 Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| Jobs run multiple times | Using memory lock | Use Redis or Postgres adapter |
| Missed jobs after downtime | Short lookback window | Increase window_lookback |
| Scheduler exits early | Normal SIGTERM | Exits gracefully after tick |
| Redis timeout | Network latency | Increase Redis timeout or switch adapter |
🧪 Testing Example
RSpec.describe "multi-node safety" do
it "dispatches exactly once across two threads" do
redis = FakeRedis::Redis.new
lock = RailsCron::Lock::Redis.new(client: redis)
RailsCron.configure { |c| c.lock_adapter = lock }
threads = 2.times.map { Thread.new { RailsCron.tick! } }
threads.each(&:join)
expect(redis.keys.grep(/dispatch/).size).to eq(1)
end
end
🧩 Roadmap
| Area | Description | Label |
|---|---|---|
| Registry & API | Cron registration and validation | feature |
| Coordinator Loop | Safe ticking and dispatch | feature |
| Lock Adapters | Redis / Postgres | build |
| CLI Tool | rails-cron executable |
build |
| i18n Humanizer | Multi-language support | lang |
| Docs & Examples | Developer onboarding | lang |
🤝 Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Run tests (
bundle exec rspec) - Open a PR using the Feature Request template
Labels: feature, build, ci, lang
📚 Documentation
📄 License
Released under the MIT License. © 2025 Codevedas Inc. — All rights reserved.