Skip to content

Conversation

@omikader
Copy link
Contributor

@omikader omikader commented Jan 28, 2026

Introduces a new Cron dependency that extends Perpetual to support wall-clock scheduled tasks using cron expressions. Unlike Perpetual which uses relative intervals, Cron schedules tasks at exact times (e.g., "0 9 * * 1" for Mondays at 9 AM).

Key changes:

  • Add Cron class with croniter integration for expression parsing
  • Support standard 5-field cron syntax and Vixie keywords (@daily, etc.)
  • Automatic scheduling at worker startup (automatic=True by default)

Notes:

  • Added croniter as a direct dependency and types-croniter as a dev dependency
  • Many of the tests in test_cron are mocked to avoid long wait times.

Closes #288

Introduces a new Cron dependency that extends Perpetual to support
wall-clock scheduled tasks using cron expressions. Unlike Perpetual
which uses relative intervals, Cron schedules tasks at exact times
(e.g., "0 9 * * 1" for Mondays at 9 AM).

Key changes:
- Add Cron class with croniter integration for expression parsing
- Support standard 5-field cron syntax and Vixie keywords (@daily, etc.)
- Automatic scheduling at worker startup (automatic=True by default)
- Fix Perpetual.__aenter__ to preserve the automatic flag

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@codecov-commenter
Copy link

codecov-commenter commented Jan 29, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.65%. Comparing base (aecd46a) to head (1419cbc).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #311      +/-   ##
==========================================
+ Coverage   98.63%   98.65%   +0.01%     
==========================================
  Files         100      102       +2     
  Lines       10066    10149      +83     
  Branches      491      491              
==========================================
+ Hits         9929    10012      +83     
  Misses        121      121              
  Partials       16       16              
Flag Coverage Δ
python-3.10 98.65% <100.00%> (-1.35%) ⬇️
python-3.11 97.29% <100.00%> (+0.02%) ⬆️
python-3.12 98.65% <100.00%> (+0.01%) ⬆️
python-3.13 98.65% <100.00%> (-1.35%) ⬇️
python-3.14 98.64% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/docket/dependencies/__init__.py 100.00% <100.00%> (ø)
src/docket/dependencies/_cron.py 100.00% <100.00%> (ø)
src/docket/dependencies/_perpetual.py 100.00% <100.00%> (ø)
src/docket/worker.py 100.00% <100.00%> (ø)
tests/fundamentals/test_cron.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Owner

@chrisguidry chrisguidry left a comment

Choose a reason for hiding this comment

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

Everything looks good except the change to Worker, that's what my recent refactorings should have eliminated. Let's do whatever we need to eliminate the need to change Worker, then I'm stoked!

async def __aenter__(self) -> Perpetual:
execution = self.execution.get()
perpetual = Perpetual(every=self.every)
perpetual = Perpetual(every=self.every, automatic=self.automatic)
Copy link
Owner

Choose a reason for hiding this comment

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

Great catch! :doh:

Comment on lines 769 to 773
when = (
perpetual.get_next()
if isinstance(perpetual, Cron)
else None
)
Copy link
Owner

Choose a reason for hiding this comment

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

Ah okay so here's something we'd want to avoid with the new factoring, needing to look at a specific dependency subclass here in Worker. I don't think you even need this, though, because in on_complete, you're calling self.at(self.get_next()), which will set _next_when and then you call the super().on_complete(...) so you're golden, shouldn't need this at all from what I see. Let me know if that's not true and we'll fix the worker to do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @chrisguidry. Thanks for the quick review!!

I agree, I would like to implement this without having to change worker.py. Perhaps I'm missing something 🙂

When a Cron task with automatic=True (the default) starts:

  1. Worker calls _schedule_all_automatic_perpetual_tasks() at startup
  2. This calls docket.add(task_function, key=key)() with no when parameter
  3. No when = schedule for now
  4. Task runs immediately
  5. on_complete then correctly schedules the next run at _croniter.get_next()

Say you have Cron("0 9 * * 1") (Mondays at 9 AM) and start your worker on a Wednesday, the task will:

  1. Run immediately on Wednesday (unexpected)
  2. Then correctly run every Monday at 9 AM going forward

We could add a method like get_initial_when on the Perpetual class that returns None and get_next() for Cron which can be used for initially scheduling all tasks. That way we avoid the isinstance check in worker.py.

Let me know what you think!

Copy link
Owner

Choose a reason for hiding this comment

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

Ohhh gotcha gotcha, so this is about the automatic scheduling of crons. I agree that Crons should default to automatic=True, that makes perfect sense.

Yes I think what you're describing is exactly what we need: a way for Perpetuals to specify when they should initially start (defaulting to right now). I think that would be a straightforward change right here, and it would open the path for other types of Perpetuals that hit this same design constraint. Nice!

]
dependencies = [
"cloudpickle>=3.1.1",
"croniter>=6",
Copy link
Owner

Choose a reason for hiding this comment

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

Thanks for pointing me to the new pallets repo, it's unfortunate that the pypi page still has that huge warning on it. I'm good with this!

@zzstoatzz I don't know if you realized this, but croniter lives! https://github.com/pallets-eco/croniter They just haven't removed the goofy warning on PyPI, but there's a bug report about it.

Maybe time to go back in Prefect?

@chrisguidry
Copy link
Owner

Oh also don't worry about the documentation build failing, that's a bug with outside forks that I still need to fix.

Copy link
Owner

@chrisguidry chrisguidry left a comment

Choose a reason for hiding this comment

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

NICE! Thank you for this, I think it's great, will get a release out soon

return cron

@property
def initial_when(self) -> datetime:
Copy link
Owner

Choose a reason for hiding this comment

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

🤌

@chrisguidry chrisguidry merged commit f9c7d16 into chrisguidry:main Jan 30, 2026
36 of 37 checks passed
@omikader
Copy link
Contributor Author

Thanks @chrisguidry! I actually thought of an idea to add custom TZ support. Let me put up another PR and lemme know if you think we can sneak it in before the next release

@omikader
Copy link
Contributor Author

@chrisguidry Here's the follow-up PR 🙂 #312

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.

Support for Cron-style / Fixed-time Scheduling

3 participants