Skip to content

fix(sm): run an every-second schedule every second - #1070

Merged
janbjorge merged 1 commit into
janbjorge:mainfrom
ericdelorefice:fix/scheduler-second-schedules
Oct 28, 2026
Merged

janbjorge merged 1 commit into
janbjorge:mainfrom
ericdelorefice:fix/scheduler-second-schedules

Conversation

@ericdelorefice

@ericdelorefice ericdelorefice commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Agentic

Summary

SchedulerManager.run sleeps until the soonest schedule is due, plus a fixed leeway of one second. For a schedule that fires every second (* * * * * *, the 6-field form the scheduling guide documents), the next tick has already passed by the time the loop wakes. fetch_schedule then moves next_run to the first tick after now, so every other tick is skipped and the schedule runs every two seconds.

It also shows up next to other schedules. The loop wakes for the soonest one, so with an every-second schedule registered, a */3 schedule fired with gaps of 2 and 4 seconds instead of 3.

I ran real SchedulerManagers against PostgreSQL 16.2 for 12 seconds per run, on asyncpg and on psycopg, nothing mocked:

                                    main (4d687d7)        with the fix
* * * * * *     alone               6 runs, 2.0 s gaps    12 runs, 1.0 s gaps
* * * * * */2   alone               2.0 s gaps            2.0 s gaps
* * * * * */3   alone               3.0 s gaps            3.0 s gaps
* * * * * */3   next to the others  2.0 / 4.0 s gaps      3.0 s gaps

PyPI 1.6.0 gives the same as main.

Change

pgqueuer/core/sm.py, +3/-1: the leeway goes from 1 s to 100 ms, under the one-second cron resolution, with a two-line comment. Plus a RELEASE.md Fixed entry.

From reading the code, I don't think the full second was protecting against clock skew. fetch_schedule sets next_run from the database's own NOW() plus the same delay the loop then sleeps, and truncates it to the second, so the offset between the worker clock and the database clock cancels out. The leeway only has to cover the time between computing the delay and the query starting. I picked 100 ms. Anything under a second fixes this, so happy to use another value if you prefer.

Testing

  • uv sync --frozen && uv run ruff check . && uv run ruff format . --check && uv run lint-imports && uv run mypy . && uv run pytest passed (PostgreSQL 16.2 through EXTERNAL_POSTGRES_DSN, Python 3.12, pytest as CI runs it with -n 3 --dist worksteal, mypy as mypy . --no-incremental): 1567 passed, 10 skipped, 2 xfailed, rebased on current main (5c2e7c1). Before the rebase, on 4d687d7: 1566 with the change and 1562 without (the 4 new tests are the difference).
  • Additional testing steps: test_scheduler.py gets a helper that runs a SchedulerManager until a schedule has fired four times and returns the time from the first firing to the last, on InMemoryQueries and on Queries(apgdriver). test_every_second_schedule_fires_every_second (and _postgres) expects under 4.5 s for * * * * * *. On main it takes 6.0 s and fails; with the fix it takes about 3 s. test_two_second_schedule_keeps_its_cadence (and _postgres) is the control: * * * * * */2 still takes over 4.5 s, so the shorter wait does not make a schedule fire more often. It passes on main and with the fix. I checked the total time rather than each gap so a slow runner does not flip it; the four tests passed 5 runs in a row under -n 4.

Checklist

  • I have read the Contributing Guide
  • I have added or updated tests
  • I have updated documentation if necessary

@janbjorge
janbjorge force-pushed the fix/scheduler-second-schedules branch from 05859ba to 1b9f26d Compare October 28, 2026 20:24
@janbjorge
janbjorge merged commit f1cad21 into janbjorge:main Oct 28, 2026
21 checks passed
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.

2 participants