Skip to content

Add RFC 5545 RRULE trigger support (dependency-free implementation) - #1499

Open
sideeffffect wants to merge 3 commits into
quartz-scheduler:mainfrom
sideeffffect:rrule-custom
Open

Add RFC 5545 RRULE trigger support (dependency-free implementation)#1499
sideeffffect wants to merge 3 commits into
quartz-scheduler:mainfrom
sideeffffect:rrule-custom

Conversation

@sideeffffect

@sideeffffect sideeffffect commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Adds first-class support for RFC 5545 RRULE (iCalendar recurrence rules) as a scheduling option, mirroring the existing CronTrigger stack. New public API:

RRULE class Cron analogue
RRuleExpression CronExpression
RRuleTrigger / RRuleTriggerImpl CronTrigger / CronTriggerImpl
RRuleScheduleBuilder CronScheduleBuilder
RRuleCalendar CronCalendar
RRuleTriggerPersistenceDelegate CronTriggerPersistenceDelegate
RRuleTriggerSupport (JMX) CronTriggerSupport
Trigger trigger = newTrigger()
        .withIdentity("myTrigger")
        .withSchedule(rruleSchedule("FREQ=WEEKLY;BYDAY=MO,WE,FR;BYHOUR=10;BYMINUTE=0"))
        .startAt(futureDate(10, MINUTES))
        .build();

Design notes

  • Recurrence engine: implemented from scratch, with no third-party dependency, following the expand/limit semantics of the table in RFC 5545 §3.3.10. It was cross-validated against org.dmfs:lib-recur across a broad battery of rules and time zones (including DST transitions) to confirm equivalence on the mainstream rule set.
  • DTSTART anchor: RFC 5545 defines an RRULE relative to a DTSTART. In Quartz that anchor is supplied by the trigger's mandatory startTime, exactly as a CronTrigger uses its start-time as a lower bound. Following the RFC, the start instant only fires if it actually matches the pattern.
  • Persistence: a new QRTZ_RRULE_TRIGGERS table (RRULE string + time-zone) with DDL added to every vendor script under jdbcjobstore, a registered RRuleTriggerPersistenceDelegate, and a new TTYPE_RRULE discriminator. The delegate loads via its own dedicated SELECT (rather than the inlined bulk join) because the shared TIME_ZONE_ID column would collide in SELECT_BULK_TRIGGERS_BASE.
  • Supports FREQ, INTERVAL, COUNT, UNTIL, WKST, BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY (incl. ordinals), BYHOUR, BYMINUTE, BYSECOND, BYSETPOS.
  • Note on DST for sub-daily rules: the engine advances in absolute time, so an HOURLY rule fires once across a spring-forward transition (matching Quartz's cron behaviour) rather than emitting a duplicated wall-clock hour.

Tests & docs

Note

This is the dependency-free implementation. An alternative PR built on org.dmfs:lib-recur (#1498) exposed the identical public API and test suite; it has been closed in favour of this one per the discussion below.

🤖 Generated with Claude Code

Introduces an RRULE analogue of the existing CronTrigger stack, allowing
schedules to be defined with RFC 5545 recurrence rules (iCalendar RRULE):

  * RRuleExpression      - parser/evaluator for RRULE (analogue of CronExpression)
  * RRuleTrigger (+Impl) - the trigger interface and implementation
  * RRuleScheduleBuilder - DSL builder (rruleSchedule(...), daily/weekly/monthly)
  * RRuleCalendar        - excludes times matching an RRULE
  * RRuleTriggerPersistenceDelegate + QRTZ_RRULE_TRIGGERS DDL for every vendor
  * RRuleTriggerSupport  - JMX open-type support and wiring

The recurrence engine is implemented from scratch (no third-party library),
following the expand/limit semantics of RFC 5545 section 3.3.10. It supports
FREQ, INTERVAL, COUNT, UNTIL, WKST, BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY,
BYDAY (including ordinals), BYHOUR, BYMINUTE, BYSECOND and BYSETPOS.

The trigger's start-time plays the role of the RFC 5545 DTSTART anchor, exactly
as a CronTrigger uses its start-time as a lower bound.

Includes unit tests (expression, schedule builder, trigger, calendar) and a
Derby JDBC integration test exercising the persistence delegate end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ondra Pelech <ondra.pelech@gmail.com>
@melloware

Copy link
Copy Markdown
Contributor

+1 for the dependency free implementation!

@melloware melloware added the is:enhancement Enhancement to an existing feature label Aug 3, 2026
@sideeffffect
sideeffffect marked this pull request as ready for review August 3, 2026 16:25
@sideeffffect sideeffffect reopened this Aug 5, 2026
@sideeffffect
sideeffffect marked this pull request as draft August 5, 2026 13:50
@sideeffffect
sideeffffect marked this pull request as ready for review August 6, 2026 11:39
@jhouserizer

Copy link
Copy Markdown
Contributor

I agree with @melloware that the dependency-free version is better overall. I'll try to have a more thorough review this morning.

@jhouserizer

Copy link
Copy Markdown
Contributor

@sideeffffect , I appreciate this new feature, and know that many users will.

Could I trouble you+Claude to include some docs / examples for the new trigger type? e.g.:

https://github.com/quartz-scheduler/quartz/tree/main/examples
https://github.com/quartz-scheduler/quartz-scheduler.org-site/blob/master/documentation/quartz-2.5.x/tutorials/index.md

Adds an examples module entry (analogous to example3 for cron) that schedules
several jobs using RRuleScheduleBuilder.rruleSchedule(...), covering SECONDLY /
WEEKLY / MONTHLY frequencies, ordinal BYDAY (last Friday), BYMONTHDAY lists,
BYDAY limiting and a COUNT-bounded rule. Wires the runExample16 Gradle task and
lists it in examples_guide.txt.

Requested in review on PR quartz-scheduler#1499.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ondra Pelech <ondra.pelech@gmail.com>
@sideeffffect

Copy link
Copy Markdown
Author

Thanks for the quick look, @jhouserizer and @melloware! 🙏

I've added in-repo docs/examples in 3260c72:

  • example16 – an RRuleTriggerExample (analogous to example3 for cron) that schedules jobs via RRuleScheduleBuilder.rruleSchedule(...), demonstrating SECONDLY/WEEKLY/MONTHLY frequencies, ordinal BYDAY (last Friday of the month), BYMONTHDAY lists, BYDAY limiting (weekends only), and a COUNT-bounded rule.
  • Wired the runExample16 Gradle task and added it to examples_guide.txt.

Run it with ../gradlew :examples:runExample16.

The tutorials page you linked lives in the separate quartz-scheduler.org-site repo, so it can't be part of this PR. I'm happy to open a companion PR there (a "RRULE Triggers" tutorial section mirroring the CronTrigger tutorial) once you've settled on which of the two implementations (#1499 dependency-free vs #1498 lib-recur) you'd like to take — just say the word and I'll get it drafted.

@sideeffffect

Copy link
Copy Markdown
Author

Docs are up as well: I opened a companion tutorial PR against the site repo — quartz-scheduler/quartz-scheduler.org-site#123 (a "RRuleTrigger Tutorial" page mirroring the CronTrigger one, linked from the 2.5.x tutorials index).

@sideeffffect

Copy link
Copy Markdown
Author

Hello @jhouserizer , thanks for looking at this. Please let me know if there's anything more I can do 🙏

The Liquibase changelog (liquibase.quartz.init.xml) creates the Quartz schema
for Liquibase-based setups. Add the QRTZ_RRULE_TRIGGERS table (with its primary
key and foreign key to QRTZ_TRIGGERS), mirroring QRTZ_CRON_TRIGGERS, so the
RRULE trigger type is available there too, matching the per-vendor SQL scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ondra Pelech <ondra.pelech@gmail.com>
@sideeffffect

Copy link
Copy Markdown
Author

ping 😸 @melloware @jhouserizer

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

Labels

is:enhancement Enhancement to an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants