Skip to content

Commit 49a669d

Browse files
authored
Cron functionality (#2251)
* Added barebones Cron and CronJob classes * WIP cron registration * Added config loading functionality * More work on finding out next run time * Got a working implementation of interval based cron * Simplify tests * Minor cleanup * Move worker and worker_pool CLI commands to a separate file * Formatting fixes * 1 more formatting fix * Added `rq cron` CLI command * More cron test cases * Slightly simplified cron.load_config_from_file() * Minor cleanups * Added cron.md * Fix typing * Fix typing and flaky test. * Change test runner to Ubuntu 22.04 * Fix SpawnWorker on redis-py>6 * Fix flaky test_working_worker_cold_shutdown * Fix Github Actions Dockerfile warning * Updated docs on cron.md * Renamed `Cron` to `CronScheduler` * Updated docs * Fix typing issues * Fix test_cli.py * Update cron.md
1 parent 7a918f1 commit 49a669d

File tree

9 files changed

+1174
-7
lines changed

9 files changed

+1174
-7
lines changed

.github/workflows/workflow.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ jobs:
7676
steps:
7777
- uses: actions/checkout@v4
7878

79-
- name: Set up Python 3.9
79+
- name: Set up Python 3.13
8080
uses: actions/[email protected]
8181
with:
82-
python-version: "3.9"
82+
python-version: "3.13"
8383

8484
- name: Install dependencies
8585
run: |

docs/docs/cron.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
---
2+
title: "RQ: Cron Scheduler"
3+
layout: docs
4+
---
5+
6+
RQ's `CronScheduler` is a simple scheduler that allows you to enqueue functions at regular intervals.
7+
8+
_New in version 2.4.0._
9+
10+
<div class="warning">
11+
<img style="float: right; margin-right: -60px; margin-top: -38px" src="/img/warning.png" />
12+
<strong>Note:</strong>
13+
<p>`CronScheduler` is still in beta, use at your own risk!</p>
14+
</div>
15+
16+
## Overview
17+
18+
`CronScheduler` provides a lightweight way to enqueue recurring jobs without the complexity of traditional cron systems. It's perfect for:
19+
20+
- Health Checks and Monitoring
21+
- Data Pipeline and ETL Tasks
22+
- Running Maintenance Tasks
23+
24+
Advantages over traditional cron:
25+
26+
1. **Sub-Minute Precision**: enqueue jobs every few seconds (e.g. every 5 seconds), traditional cron is limited to one minute intervals
27+
2. **RQ Integration**: plugs into your existing RQ infrastructure
28+
3. **Fault Tolerance**: jobs benefit from RQ's retry mechanisms and failure handling (soon)
29+
4. **Scalability**: route functions to run in different queues, allowing for better resource management and scaling
30+
5. **Dynamic Configuration**: easily configure functions and intervals without modifying system cron
31+
6. **Job Control**: jobs can have timeouts, TTLs and custom failure/success handling
32+
33+
## Quick Start
34+
35+
### 1. Create a Cron Configuration
36+
37+
Create a cron configuration file (`cron_config.py`):
38+
39+
```python
40+
from rq import cron
41+
from myapp import cleanup_database, send_notifications, send_daily_report
42+
43+
# Run database cleanup every 5 minutes
44+
cron.register(
45+
cleanup_database,
46+
queue_name='repeating_tasks',
47+
interval=300 # 5 minutes in seconds
48+
)
49+
50+
# Send notifications every hour
51+
cron.register(
52+
send_notifications,
53+
queue_name='repeating_tasks',
54+
interval=5 # Every 5 seconds
55+
)
56+
57+
# Send daily reports every 24 hours
58+
cron.register(
59+
send_daily_report,
60+
queue_name='repeating_tasks',
61+
args=('daily',),
62+
kwargs={'format': 'pdf'},
63+
interval=86400 # 24 hours in seconds
64+
)
65+
```
66+
67+
### 2. Start the Scheduler
68+
69+
```sh
70+
rq cron cron_config.py
71+
```
72+
73+
That's it! Your jobs will now be automatically enqueued at the specified intervals.
74+
75+
## Understanding RQ Cron
76+
77+
### How It Works
78+
79+
Key concepts:
80+
- **Interval-based**: jobs run every X seconds (e.g. every 5 seconds). Cron string based job enqueueing is planned for future releases.
81+
- **Separation of concerns**: `CronScheduler` only enqueues jobs; RQ workers handle execution
82+
83+
`CronScheduler` is not a job executor - it's a scheduler to enqueue functions periodically. Here's how the system works:
84+
85+
1. **Registration**: functions are registered along with their intervals and target queues
86+
2. **First run**: registered functions are immediately enqueued when `CronScheduler` starts
87+
3. **Worker execution**: RQ workers pick up and execute the jobs from their queues
88+
4. **Sleep**: `CronScheduler` sleeps until the next job is due
89+
90+
## Configuration Files
91+
92+
### Basic Configuration
93+
94+
Configuration files use the global `register` function to define scheduled jobs:
95+
96+
```python
97+
# my_cron_config.py
98+
from rq.cron import register
99+
from myapp.tasks import cleanup_database, generate_reports, backup_files
100+
101+
# Simple job - runs every 60 seconds
102+
register(cleanup_database, queue_name='maintenance', interval=60)
103+
104+
# Job with arguments
105+
register(
106+
generate_reports,
107+
queue_name='reports',
108+
args=('daily',),
109+
kwargs={'format': 'pdf', 'email': True},
110+
interval=3600
111+
)
112+
113+
# Job with custom timeout and TTL settings
114+
register(
115+
backup_files,
116+
queue_name='backup',
117+
interval=86400, # Once per day
118+
timeout=1800, # 30 minutes max execution time
119+
result_ttl=3600, # Keep results for 1 hour
120+
failure_ttl=86400 # Keep failed jobs for 1 day
121+
)
122+
```
123+
124+
Since configuration files are standard Python modules, you can include conditional logic. For example:
125+
126+
```python
127+
import os
128+
from rq.cron import register
129+
from myapp.tasks import cleanup
130+
131+
if os.getenv("ENABLE_CLEANUP") == "true":
132+
register(cleanup, queue_name="maintenance", interval=600)
133+
```
134+
135+
## Command Line Usage
136+
137+
```console
138+
$ rq cron my_cron_config.py
139+
```
140+
141+
You can specify a dotted module path instead of a file path:
142+
143+
```console
144+
$ rq cron myapp.cron_config
145+
```
146+
147+
The `rq cron` CLI command accepts the following options:
148+
149+
- `--url`, `-u`: Redis connection URL (env: RQ_REDIS_URL)
150+
- `--config`, `-c`: Python module with RQ settings (env: RQ_CONFIG)
151+
- `--path`, `-P`: Additional Python import paths (can be specified multiple times)
152+
- `--logging-level`, `-l`: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL; default: INFO)
153+
- `--worker-class`, `-w`: Dotted path to RQ Worker class
154+
- `--job-class`, `-j`: Dotted path to RQ Job class
155+
- `--queue-class`: Dotted path to RQ Queue class
156+
- `--connection-class`: Dotted path to Redis client class
157+
- `--serializer`, `-S`: Dotted path to serializer class
158+
159+
Positional argument:
160+
161+
- `config_path`: File path or module path to your cron configuration
162+
163+
Example:
164+
165+
```console
166+
$ rq cron myapp.cron_config --url redis://localhost:6379/1 --path src
167+
```
168+
169+
## Programmatic API
170+
171+
### Basic Usage
172+
173+
```python
174+
from redis import Redis
175+
from rq.cron import CronScheduler
176+
from myapp.tasks import cleanup_database, send_reports
177+
178+
# Create a cron scheduler
179+
redis_conn = Redis(host='localhost', port=6379, db=0)
180+
cron = CronScheduler(connection=redis_conn, logging_level='INFO')
181+
182+
# Register jobs
183+
cron.register(
184+
cleanup_database,
185+
queue_name='maintenance',
186+
interval=3600
187+
)
188+
189+
cron.register(
190+
send_reports,
191+
queue_name='reports',
192+
args=('daily',),
193+
interval=86400
194+
)
195+
196+
# Start the scheduler (this will block until interrupted)
197+
try:
198+
cron.start()
199+
except KeyboardInterrupt:
200+
print("Shutting down cron scheduler...")
201+
```

rq/cli/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
# TODO: the following imports can be removed when we drop the `rqinfo` and
55
# `rqworkers` commands in favor of just shipping the `rq` command.
66
from .cli import info
7+
from .cli_cron import cron
78
from .workers import worker

rq/cli/cli_cron.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import logging
2+
import logging.config
3+
import sys
4+
5+
import click
6+
7+
from rq.cli.cli import main
8+
from rq.cli.helpers import (
9+
pass_cli_config,
10+
read_config_file,
11+
# setup_loghandlers_from_args is not used when only --logging-level is present
12+
)
13+
from rq.cron import CronScheduler
14+
15+
16+
@main.command()
17+
@click.option(
18+
'--logging-level',
19+
'-l',
20+
type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], case_sensitive=False),
21+
default='INFO',
22+
show_default=True, # Explicitly show the default in help text
23+
help='Set logging level.',
24+
)
25+
@click.argument('config_path')
26+
@pass_cli_config
27+
def cron(
28+
cli_config,
29+
logging_level,
30+
# verbose and quiet parameters are removed
31+
config_path,
32+
**options,
33+
):
34+
"""Starts the RQ cron scheduler.
35+
36+
Requires a configuration file or module path defining the cron jobs.
37+
Logging level is controlled by the --logging-level option.
38+
"""
39+
settings = read_config_file(cli_config.config) if cli_config.config else {}
40+
dict_config = settings.get('DICT_CONFIG')
41+
42+
# Apply custom logging configuration if provided
43+
if dict_config:
44+
logging.config.dictConfig(dict_config)
45+
logging.getLogger('rq.cron').info('Logging configured via DICT_CONFIG setting.')
46+
47+
try:
48+
cron = CronScheduler(connection=cli_config.connection, logging_level=logging_level)
49+
cron.load_config_from_file(config_path)
50+
cron.start()
51+
except KeyboardInterrupt:
52+
click.echo('\nShutting down cron scheduler...')
53+
sys.exit(0)

0 commit comments

Comments
 (0)