diff --git a/docs/concepts/schedules.md b/docs/concepts/schedules.md index 69bcf29119ea..771e749e5695 100644 --- a/docs/concepts/schedules.md +++ b/docs/concepts/schedules.md @@ -178,6 +178,9 @@ schedule: rrule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240730T040000Z' ``` +!!! info "Max RRule length" + Note the max supported character length of an `rrulestr` is 6500 characters + !!! info "Daylight saving time considerations" Note that as a calendar-oriented standard, `RRules` are sensitive to the initial timezone provided. A 9am daily schedule with a DST-aware start date will maintain a local 9am time through DST boundaries. A 9am daily schedule with a UTC start date will maintain a 9am UTC time. diff --git a/src/prefect/orion/schemas/schedules.py b/src/prefect/orion/schemas/schedules.py index 1a2df93c55b6..8a22bb820970 100644 --- a/src/prefect/orion/schemas/schedules.py +++ b/src/prefect/orion/schemas/schedules.py @@ -15,6 +15,8 @@ from prefect.orion.utilities.schemas import DateTimeTZ, PrefectBaseModel MAX_ITERATIONS = 1000 +# approx. 1 years worth of RDATEs + buffer +MAX_RRULE_LENGTH = 6500 def _prepare_scheduling_start_and_end( @@ -394,6 +396,11 @@ def validate_rrule_str(cls, v): # rrules errors are a mix of cryptic and informative # so reraise to be clear that the string was invalid raise ValueError(f'Invalid RRule string "{v}": {exc}') + if len(v) > MAX_RRULE_LENGTH: + raise ValueError( + f'Invalid RRule string "{v[:40]}..."\n' + f"Max length is {MAX_RRULE_LENGTH}, got {len(v)}" + ) return v @classmethod diff --git a/tests/orion/schemas/test_schedules.py b/tests/orion/schemas/test_schedules.py index 39b7e50a17b9..c36292096a1d 100644 --- a/tests/orion/schemas/test_schedules.py +++ b/tests/orion/schemas/test_schedules.py @@ -11,6 +11,7 @@ from prefect.orion.schemas.schedules import ( MAX_ITERATIONS, + MAX_RRULE_LENGTH, CronSchedule, IntervalSchedule, RRuleSchedule, @@ -634,13 +635,6 @@ async def test_rrule_returns_nothing_before_dtstart(self): dates = await s.get_dates(5, start=pendulum.now("UTC")) assert dates == [pendulum.datetime(2030, 1, 1).add(days=i) for i in range(5)] - async def test_rrule_returns_nothing_before_dtstart(self): - s = RRuleSchedule.from_rrule( - rrule.rrule(freq=rrule.DAILY, dtstart=pendulum.datetime(2030, 1, 1)) - ) - dates = await s.get_dates(5, start=pendulum.now("UTC")) - assert dates == [pendulum.datetime(2030, 1, 1).add(days=i) for i in range(5)] - async def test_rrule_validates_rrule_str(self): # generic validation error with pytest.raises(ValidationError, match="(Invalid RRule string)"): @@ -654,6 +648,15 @@ async def test_rrule_validates_rrule_str(self): with pytest.raises(ValidationError, match="(invalid 'FREQ': DAILYBAD)"): RRuleSchedule(rrule="FREQ=DAILYBAD") + async def test_rrule_max_rrule_len(self): + start = datetime(2000, 1, 1) + s = "RDATE:" + ",".join( + [start.add(days=i).format("YMMDD") + "T000000Z" for i in range(365 * 3)] + ) + assert len(s) > MAX_RRULE_LENGTH + with pytest.raises(ValidationError, match="Max length"): + RRuleSchedule(rrule=s) + async def test_rrule_schedule_handles_complex_rrulesets(self): s = RRuleSchedule( rrule="DTSTART:19970902T090000\n"