Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
86f51cd
feat(apscheduler): Add cron monitoring for scheduled jobs
wedamija Oct 2, 2026
ac7e4be
ci(apscheduler): Keep unrelated populate_tox cache entries
wedamija Oct 2, 2026
f2d174a
ref(crons): Share interval schedule conversion
wedamija Oct 2, 2026
07b4f9c
test(apscheduler): Don't let misfire grace time flake job tests
wedamija Oct 3, 2026
8c285ef
Merge branch 'master' into danf/apscheduler-crons
wedamija Oct 5, 2026
14d3b3b
fix(apscheduler): Address review feedback on cron monitoring
wedamija Oct 5, 2026
672657e
Merge remote-tracking branch 'origin/master' into HEAD
wedamija Oct 22, 2026
8cab675
fix(apscheduler): Skip monitoring triggers with an end_date
wedamija Oct 22, 2026
2d8954f
Merge remote-tracking branch 'origin/master' into danf/apscheduler-cr…
wedamija Oct 22, 2026
849200f
ref(crons): Share the monitor timezone name helper
wedamija Oct 22, 2026
b09f31e
feat(apscheduler): Add trigger jitter to the check-in margin
wedamija Oct 22, 2026
eec709e
Merge remote-tracking branch 'origin/master' into danf/apscheduler-crons
wedamija Oct 23, 2026
f357733
Merge remote-tracking branch 'origin/master' into tmp-aps-merge
wedamija Oct 28, 2026
dbb0b80
Merge remote-tracking branch 'origin/master' into danf/apscheduler-crons
wedamija Nov 3, 2026
761ea09
fix(apscheduler): Close check-ins of runs that fail inside the executor
wedamija Nov 3, 2026
cbff58b
fix(apscheduler): Skip jobs already decorated with monitor
wedamija Nov 3, 2026
84a6262
fix(apscheduler): Check in once per submitted backlog
wedamija Nov 3, 2026
522f3cc
fix(crons): Only report known IANA timezone names
wedamija Nov 3, 2026
f4068fd
ref(apscheduler): Trim docstrings and comments, drop dead 4.x check
wedamija Nov 3, 2026
dc9b6ac
test(apscheduler): Check generated id warnings with real jobs and caplog
wedamija Nov 3, 2026
668f666
test(apscheduler): Cover combining, calendar interval and pytz cron t…
wedamija Nov 3, 2026
e04ac77
ref(crons): Type-ignore the optional timezone database imports
wedamija Nov 3, 2026
0012437
fix(crons): Don't count zoneinfo's localtime file as a timezone
wedamija Nov 3, 2026
eeb460b
fix(arq): Skip cron jobs already decorated with monitor
wedamija Nov 3, 2026
fcdeaca
fix(apscheduler): Send no check-in for missed runs
wedamija Nov 3, 2026
7729a7b
test(apscheduler): Cover decorated methods and crashes with several r…
wedamija Nov 3, 2026
e319fb1
fix(apscheduler): Close a missed run's check-in if in_progress was sent
wedamija Nov 3, 2026
e1da346
test(apscheduler): Keep the missed-run test clear of cron slots falli…
wedamija Nov 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/test-integrations-tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ jobs:
- name: Setup Test Env
run: |
uv sync
- name: Test apscheduler
run: |
set -x # print commands that are executed
./scripts/runtox.sh "py${{ matrix.python-version }}-apscheduler"
- name: Test arq
run: |
set -x # print commands that are executed
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ ignore_missing_imports = true
module = "huey.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "apscheduler.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "openai.*"
ignore_missing_imports = true
Expand Down
5 changes: 5 additions & 0 deletions scripts/populate_tox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
"python": ">=3.8",
"num_versions": 2,
},
"apscheduler": {
"package": "apscheduler",
# 4.x is a rewrite with a different API and isn't supported.
"include": "<4",
},
"arq": {
"package": "arq",
"deps": {
Expand Down
4 changes: 4 additions & 0 deletions scripts/populate_tox/package_dependencies.jsonl

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions scripts/populate_tox/releases.jsonl

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions scripts/split_tox_gh_actions/split_tox_gh_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"requests",
],
"Tasks": [
"apscheduler",
"arq",
"beam",
"celery",
Expand Down
2 changes: 2 additions & 0 deletions sentry_sdk/crons/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,14 @@
with self:
return await fn(*args, **kwargs)

inner._sentry_monitored = True # type: ignore[attr-defined]
return inner

def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]":
@wraps(fn)
def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
with self:
return fn(*args, **kwargs)

inner._sentry_monitored = True # type: ignore[attr-defined]

Check warning on line 138 in sentry_sdk/crons/decorator.py

View check run for this annotation

@sentry/warden / warden: code-review

Unwrap bound methods before checking the decorator marker

For a decorated instance method, `job.func` is a bound method and does not expose the `_sentry_monitored` attribute set on its underlying wrapper function. `_begin_check_in` therefore treats the method as unmonitored and sends an additional APScheduler check-in alongside the decorator's check-in. Check the marker on `getattr(job.func, "__func__", job.func)` instead.
return inner
55 changes: 54 additions & 1 deletion sentry_sdk/crons/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from datetime import timedelta, timezone
from functools import lru_cache
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Optional, Tuple
from datetime import tzinfo
from typing import FrozenSet, Optional, Set, Tuple

from sentry_sdk._types import MonitorConfigSchedule, MonitorConfigScheduleUnit

Expand All @@ -27,3 +30,53 @@ def _get_interval_schedule(seconds: float) -> "Optional[MonitorConfigSchedule]":
"unit": unit,
}
return None


@lru_cache(maxsize=None)
def _get_iana_timezone_names() -> "FrozenSet[str]":
names: "Set[str]" = set()
try:
from zoneinfo import available_timezones

names.update(available_timezones())
except ImportError:
try:
from backports.zoneinfo import ( # type: ignore[import-not-found, no-redef]
available_timezones,
)

names.update(available_timezones())
except ImportError:
pass

try:
import pytz # type: ignore[import-untyped]

# pytz's lazy set looks empty to set.update until it's iterated.
names.update(iter(pytz.all_timezones_set))
except ImportError:
pass

# Some systems keep these files in the zoneinfo directory, but they aren't zones.
names.difference_update(("localtime", "posixrules"))
return frozenset(names)


def _get_timezone_name(tz: "tzinfo") -> "Optional[str]":
"""
Return the IANA name of the timezone, or None if it has none.
"""
# zoneinfo and pytz. tzlocal can give a zone a non-IANA key, like "local"
# or the basename of a TZ file path.
name = getattr(tz, "key", None) or getattr(tz, "zone", None)
if isinstance(name, str):
return name if name in _get_iana_timezone_names() else None

if isinstance(tz, timezone):
offset = tz.utcoffset(None)
if offset % timedelta(hours=1):
return None
hours = offset // timedelta(hours=1)
return "Etc/GMT%+d" % -hours if hours else "UTC"

return None
1 change: 1 addition & 0 deletions sentry_sdk/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def iter_default_integrations(
"aiohttp": (3, 4),
"aiomysql": (0, 3, 0),
"anthropic": (0, 16),
"apscheduler": (3, 3),
"ariadne": (0, 20),
"arq": (0, 23),
"asyncpg": (0, 23),
Expand Down
Loading
Loading