-
Notifications
You must be signed in to change notification settings - Fork 3.5k
chore(devex): block new schedules at minute zero #106997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
aspicer
wants to merge
6
commits into
master
Choose a base branch
from
aspicer/jitter-lint-rule
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+243
−1
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ac8712d
chore(devex): block new schedules at minute zero
aspicer 75c3867
chore(devex): catch minute-zero cron constants and zero offsets
aspicer b807ead
chore(devex): exempt the rolling-hour and customer-timed schedules
aspicer 4d9eed2
chore: merge master into jitter-lint-rule
aspicer ab5e252
chore(devex): exempt the today briefing scheduler from the minute-zer…
aspicer fa8c036
chore: merge master into jitter-lint-rule
aspicer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
.semgrep/rules/devex/schedule-must-avoid-minute-zero.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| # Test cases for schedule-must-avoid-minute-zero. | ||
| # ruff: noqa | ||
| import datetime as dt | ||
| import os | ||
| from datetime import timedelta | ||
|
|
||
| import dagster | ||
| from celery.schedules import crontab | ||
| from temporalio.client import ScheduleIntervalSpec, ScheduleSpec | ||
|
|
||
| from posthog.scheduling.jitter import deterministic_offset | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| sender.add_periodic_task(crontab(hour="*", minute="0"), refresh_cache.s()) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| sender.add_periodic_task(crontab(minute="0"), kill_stale_runs.s()) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| sender.add_periodic_task(crontab(minute="0", hour="*/12"), refresh_fields.s()) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| sender.add_periodic_task(crontab(minute=0), sweep.s()) | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| sender.add_periodic_task(crontab(hour="*", minute="23"), refresh_cache.s()) | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| sender.add_periodic_task(crontab(hour="3", minute="0"), daily_cleanup.s()) | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| sender.add_periodic_task(crontab(minute="*/5"), poll.s()) | ||
|
|
||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| @dagster.schedule(cron_schedule="0 * * * *", job=hourly_job) | ||
| def hourly_schedule(context): | ||
| return dagster.RunRequest() | ||
|
|
||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| every_six_hours = dagster.ScheduleDefinition(job=job, cron_schedule="0 */6 * * *") | ||
|
|
||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| @dagster.schedule(cron_schedule="17 * * * *", job=hourly_job) | ||
| def offset_schedule(context): | ||
| return dagster.RunRequest() | ||
|
|
||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| daily = dagster.ScheduleDefinition(job=job, cron_schedule="0 3 * * *") | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| HOURLY_CRON_SCHEDULE = os.getenv("HOURLY_CRON_SCHEDULE", "0 * * * *") | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| named_hourly = dagster.ScheduleDefinition(job=job, cron_schedule=HOURLY_CRON_SCHEDULE) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| hourly_cron = ScheduleSpec(cron_expressions=["0 * * * *"]) | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| every_minute = ScheduleSpec(cron_expressions=["*/1 * * * *"]) | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| daily_cron = ScheduleSpec(cron_expressions=["2 3 * * *"], jitter=timedelta(minutes=30)) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| hourly_interval = ScheduleIntervalSpec(every=timedelta(hours=1)) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| quarter_hour = ScheduleIntervalSpec(every=dt.timedelta(minutes=15)) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| named_interval = ScheduleIntervalSpec(every=SCHEDULE_INTERVAL) | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| offset_interval = ScheduleIntervalSpec(every=timedelta(hours=1), offset=timedelta(minutes=2)) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| none_offset = ScheduleIntervalSpec(every=timedelta(hours=1), offset=None) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| empty_offset = ScheduleIntervalSpec(every=timedelta(hours=1), offset=timedelta()) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| zero_offset = ScheduleIntervalSpec(every=timedelta(hours=1), offset=timedelta(0)) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| zero_minutes_offset = ScheduleIntervalSpec(every=dt.timedelta(hours=1), offset=dt.timedelta(minutes=0)) | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| entity_interval = ScheduleIntervalSpec(every=SCHEDULE_INTERVAL, offset=deterministic_offset(key, SCHEDULE_INTERVAL)) | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| daily_interval = ScheduleIntervalSpec(every=timedelta(days=1)) | ||
|
|
||
| # ruleid: schedule-must-avoid-minute-zero | ||
| six_hourly_interval = ScheduleIntervalSpec(every=timedelta(hours=6)) | ||
|
|
||
| # ok: schedule-must-avoid-minute-zero | ||
| day_in_hours_interval = ScheduleIntervalSpec(every=timedelta(hours=24)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| rules: | ||
| - id: schedule-must-avoid-minute-zero | ||
| message: | | ||
| This schedule fires at minute zero of the hour. | ||
|
|
||
| Fleet schedules that fire together concentrate query load. Check how the job tracks | ||
| its input before moving it: a rolling lookback equal to the interval can skip data | ||
| when its start moves. Jitter needs a cursor or enough overlapping lookback. | ||
|
|
||
| - Celery beat and Dagster: write a fixed minute that no other job in that hour holds, | ||
| for example `crontab(minute="23")` or `cron_schedule="23 * * * *"`. | ||
| - Temporal fleet schedule: start two minutes after the boundary and add jitter, | ||
| for example `ScheduleIntervalSpec(every=timedelta(hours=1), offset=timedelta(minutes=2))` | ||
| with `ScheduleSpec(..., jitter=timedelta(minutes=10))`. | ||
| - Sub-hourly or per-entity Temporal schedule: pass | ||
| `offset=deterministic_offset(<stable id>, <interval>)` from `posthog/scheduling/jitter.py`. | ||
|
|
||
| Keep the existing time when a customer chose it or a fixed lookback requires it. Add | ||
| `# nosemgrep: schedule-must-avoid-minute-zero -- <reason>`. | ||
| languages: [python] | ||
| severity: ERROR | ||
| pattern-either: | ||
| # Celery beat at minute 0 of every hour, or of every N hours. crontab's hour defaults to "*". | ||
| - patterns: | ||
| - pattern-either: | ||
| - pattern: crontab(..., minute="0", ...) | ||
| - pattern: crontab(..., minute="00", ...) | ||
| - pattern: crontab(..., minute=0, ...) | ||
| - pattern-either: | ||
| - patterns: | ||
| - pattern: crontab(...) | ||
| - pattern-not: crontab(..., hour=$HOUR, ...) | ||
| - patterns: | ||
| - pattern: crontab(..., hour="$HOUR", ...) | ||
| - metavariable-regex: | ||
| metavariable: $HOUR | ||
| regex: ^\*(/\d+)?$ | ||
| # A five-field cron string with minute 0 and an hour field of "*" or "*/N". The rule matches the | ||
| # literal wherever it is written, because a Dagster or Temporal schedule often reads it from a | ||
| # constant or an environment default in another module, which semgrep cannot follow. | ||
| - patterns: | ||
| - pattern: '"$CRON"' | ||
| - metavariable-regex: | ||
| metavariable: $CRON | ||
| regex: ^\s*0+\s+\*(/\d+)?(\s+\S+){3}\s*$ | ||
| # A Temporal interval with no offset, or a zero offset, starts on the epoch grid, so a sub-daily | ||
| # interval that fits evenly into the day fires at minute zero. An interval the rule cannot read | ||
| # counts as short. | ||
| - patterns: | ||
| - pattern-either: | ||
| - patterns: | ||
| - pattern: ScheduleIntervalSpec(...) | ||
| - pattern-not: ScheduleIntervalSpec(..., offset=$OFFSET, ...) | ||
| - pattern: ScheduleIntervalSpec(..., offset=None, ...) | ||
| - pattern: ScheduleIntervalSpec(..., offset=datetime.timedelta(), ...) | ||
| - pattern: ScheduleIntervalSpec(..., offset=datetime.timedelta(0), ...) | ||
| - pattern: ScheduleIntervalSpec(..., offset=datetime.timedelta($UNIT=0), ...) | ||
| - pattern-either: | ||
| - patterns: | ||
| - pattern: ScheduleIntervalSpec(..., every=$INTERVAL, ...) | ||
| - metavariable-regex: | ||
| metavariable: $INTERVAL | ||
| regex: ^[A-Za-z_][\w.]*$ | ||
| - patterns: | ||
| - pattern: ScheduleIntervalSpec(..., every=datetime.timedelta(hours=$HOURS), ...) | ||
| - metavariable-regex: | ||
| metavariable: $HOURS | ||
| regex: ^([1-9]|1\d|2[0-3]|[A-Za-z_][\w.]*)$ | ||
| - patterns: | ||
| - pattern: ScheduleIntervalSpec(..., every=datetime.timedelta(minutes=$MINUTES), ...) | ||
| - metavariable-regex: | ||
| metavariable: $MINUTES | ||
| regex: ^([1-9]\d{0,2}|1[0-3]\d\d|14[0-3]\d|[A-Za-z_][\w.]*)$ | ||
| - patterns: | ||
| - pattern: ScheduleIntervalSpec(..., every=datetime.timedelta(seconds=$SECONDS), ...) | ||
| - metavariable-regex: | ||
| metavariable: $SECONDS | ||
| regex: ^([1-9]\d{0,3}|[1-7]\d{4}|8[0-5]\d{3}|86[0-3]\d\d|[A-Za-z_][\w.]*)$ | ||
| # Constant propagation also matches each use of a cron constant, so one schedule would need a | ||
| # nosemgrep comment at the definition and at every use. Without it, the rule reports the definition only. | ||
| options: | ||
| constant_propagation: false | ||
| paths: | ||
| include: | ||
| - dags/**/*.py | ||
| - ee/**/*.py | ||
| - posthog/**/*.py | ||
| - products/**/*.py | ||
| exclude: | ||
| - '**/test/**' | ||
| - '**/tests/**' | ||
| - '**/test_*.py' | ||
| - '**/*_test.py' | ||
| - '**/conftest*.py' | ||
| metadata: | ||
| category: performance | ||
| subcategory: audit | ||
| confidence: HIGH | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.