Skip to content

feat(rq): Add cron monitoring for rq-scheduler jobs - #8053

Draft
wedamija wants to merge 3 commits into
masterfrom
danf/rq-scheduler-crons
Draft

wedamija wants to merge 3 commits into
masterfrom
danf/rq-scheduler-crons

Conversation

@wedamija

@wedamija wedamija commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Description

Part of a series adding automatic Crons monitoring for the most-used job schedulers in each SDK. Check-ins for a monitor that doesn't exist yet are dropped, so today each monitor has to be created by hand with its schedule copied from the code. Sending the job's schedule as the monitor config lets Sentry create the monitor and keep it in sync.

This adds opt-in check-ins for recurring jobs scheduled with rq-scheduler, the most used way to run recurring RQ jobs, modelled on CeleryIntegration(monitor_beat_tasks=True, exclude_beat_tasks=[...]):

sentry_sdk.init(
    integrations=[RqIntegration(monitor_cron_jobs=True, exclude_cron_jobs=["cleanup"])],
)
  • Only the worker needs the option. It sends in_progress when the job starts and ok or error when perform_job finishes. rq-scheduler stores the schedule in the job's meta, so nothing runs in the rqscheduler process.
  • Jobs from scheduler.cron(...) and scheduler.schedule(..., interval=...) are monitored. One-off jobs (enqueue_at, enqueue_in, schedule without interval) are not.
  • The slug is the job id passed to cron()/schedule(). Without one, the job gets a random id on every deploy, so the function's import path is used instead.
  • Cron strings are sent as crontabs in UTC. rq-scheduler evaluates them in the system's local time, so the config is only sent when the worker's local timezone is UTC. Intervals are sent when they are whole minutes.
  • Check-ins are sent without a config for cron strings Sentry may read differently (6-field crontabs, @daily, names such as MON, L, or both day of month and day of week set), sub-minute or uneven intervals, and jobs with repeat, since they stop after a number of runs.

RQ's built-in rq cron scheduler isn't covered here. It doesn't mark the jobs it enqueues, so it needs a patch in the scheduler process (see the closed #7841).

The rq test envs now install rq-scheduler for RQ 2.x.

Issues

Docs: getsentry/sentry-docs#20058

Send check-ins around each run of a job that rq-scheduler re-enqueues
(scheduler.cron() and scheduler.schedule(interval=...)) when
RqIntegration(monitor_cron_jobs=True) is set. The schedule is sent as the
monitor config when Sentry reads it the same way, so Sentry creates the
monitor and keeps it in sync. One-off jobs (enqueue_at, enqueue_in) are
not monitored.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Results 📊

✅ 133762 passed | ⏭️ 7299 skipped | Total: 141061 | Pass Rate: 94.83% | Execution Time: 427m 47s

📊 Comparison with Base Branch

Metric Change
Total Tests 📈 +119
Passed Tests 📈 +96
Failed Tests —
Skipped Tests 📈 +23

All tests are passing successfully.

✅ Patch coverage is 100.00% (60 of 60 changed executable lines covered; target 80%).
Project statement coverage is 90.28% (up 0.03 percentage points from base (0f7acd5) to head (6f68bc3)).

Changed files with executable lines (1)
File Patch coverage Changed executable lines
sentry_sdk/integrations/rq.py 100.00% 60/60 covered; partial branches: 92, 302, 330
Coverage diff
@@            Coverage Diff             @@
##        master     #8053       +/-##
==========================================
+ Coverage    90.25%    90.28%    +0.03%
==========================================
  Files          202       202         —
  Tracked lines     26660     26719       +59
  Branches      9926      9956       +30
==========================================
+ Hits         24062     24122       +60
+ Misses        2598      2597        -1
- Partials      1510      1511        +1

Generated by Coverage Action

Comment on lines +95 to +97
check_in = None
with capture_internal_exceptions():
check_in = _start_check_in(integration, job)

@sentry-warden sentry-warden Bot Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated-ID cron jobs use dotted func_name as monitor_slug

_start_check_in falls back to job.func_name (e.g. module.task) when the job ID is a UUID, but Sentry monitor slugs only allow [a-zA-Z0-9_-]; sanitize or replace dots so check-ins are not rejected.

Evidence
  • sentry_patched_perform_job calls _start_check_in(integration, job) for every job when the integration is enabled.
  • In _start_check_in, monitor_slug = job.func_name if _GENERATED_JOB_ID.fullmatch(job.id) else job.id.
  • RQ job.func_name is a dotted import path (confirmed by test_cron_job_generated_id expecting tests.integrations.rq.test_rq_crons.ok_job).
  • Sentry monitor slugs are validated as ^[a-zA-Z0-9_-]+- sentry_patched_perform_jobcalls_start_check_in(integration, job)` for every job when the integration is enabled.
  • In _start_check_in, monitor_slug = job.func_name if _GENERATED_JOB_ID.fullmatch(job.id) else job.id.
  • RQ job.func_name is a dotted import path (confirmed by test_cron_job_generated_id expecting tests.integrations.rq.test_rq_crons.ok_job).
  • Sentry monitor slugs are validated as , so these check-ins fail for the common scheduler.cron(..., func=task) path without an explicit id.

Identified by Warden · code-review, find-bugs · 8PJ-S34

Comment on lines +95 to +97
check_in = None
with capture_internal_exceptions():
check_in = _start_check_in(integration, job)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated IDs collapse distinct schedules for the same function

When scheduled jobs have generated IDs, _start_check_in uses job.func_name as the monitor slug. Two recurring schedules for the same function therefore share one monitor, and each check-in sends its own schedule config to that monitor. The monitor cannot represent both schedules independently; its config is left reflecting whichever schedule was sent last.

Evidence
  • _start_check_in substitutes job.func_name when job.id matches _GENERATED_JOB_ID; test_cron_job_generated_id confirms the fallback is used for generated IDs.
  • The fallback does not include the job’s cron_string or interval, so separate schedules for the same function produce the same slug.
  • _get_monitor_config derives config from each job’s schedule metadata, and _start_check_in passes that config to capture_checkin.
  • The tests cover a single generated-ID schedule, but do not cover distinct schedules for the same function.

Identified by Warden · find-bugs · FW4-M52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix attempt detected (commit 6f68bc3)

The change warns that generated IDs for the same function share a monitor, but still uses the function name as the slug, so distinct schedules continue to collide unless callers provide explicit IDs.

The original issue appears unresolved. Please review and try again.

Evaluated by Warden

Co-Authored-By: Claude <noreply@anthropic.com>

check_in = None
with capture_internal_exceptions():
check_in = _start_check_in(integration, job)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crontab monitor timezone taken from worker, not scheduler

_get_crontab_config (via _start_check_in) treats the worker being UTC as proof the cron runs in UTC, so a non-UTC rqscheduler with UTC workers upserts the wrong monitor timezone and causes false missed check-ins.

Evidence
  • _start_check_in() → _get_monitor_config() → _get_crontab_config() builds monitor config on the worker only.
  • _get_crontab_config checks time.timezone / time.daylight / time.altzone in the worker process, then hard-codes "timezone": "UTC".
  • Comments and PR design state rq-scheduler evaluates crontabs in the scheduler process local time, and nothing runs in rqscheduler.
  • Tests only cover worker TZ non-UTC → no config; they never cover scheduler TZ ≠ worker TZ.

Identified by Warden · find-bugs · 4XW-77D

Comment on lines +324 to +325
if "cron_string" not in job.meta and "interval" not in job.meta:
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One-off schedule() jobs are monitored when meta interval is None

Gate on recurring jobs with truthiness (not job.meta.get("cron_string") and not job.meta.get("interval")) instead of key presence—rq-scheduler always sets meta['interval'] = interval, so one-off schedule() jobs still get check-ins.

Evidence
  • _start_check_in returns early only when both "cron_string" and "interval" are absent from job.meta.
  • rq-scheduler's schedule() always assigns job.meta['interval'] = interval, including when interval is None for a one-off run.
  • With the key present, execution continues to capture_checkin, so one-off schedule() jobs emit check-ins.
  • test_one_off_jobs_not_monitored expects no check-ins for scheduler.schedule(..., ok_job) without an interval.

Identified by Warden · find-bugs · WBR-F4V

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant