Skip to content

Commit 1c8b4e0

Browse files
lewazoantonpirkersentrivana
authored
Add support for cron jobs in ARQ integration (#2088)
Co-authored-by: Anton Pirker <[email protected]> Co-authored-by: Ivana Kellyerova <[email protected]>
1 parent 6fe2974 commit 1c8b4e0

File tree

2 files changed

+113
-40
lines changed

2 files changed

+113
-40
lines changed

sentry_sdk/integrations/arq.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@
2626
raise DidNotEnable("Arq is not installed")
2727

2828
if TYPE_CHECKING:
29-
from typing import Any, Dict, Optional
29+
from typing import Any, Dict, Optional, Union
3030

3131
from sentry_sdk._types import EventProcessor, Event, ExcInfo, Hint
3232

33+
from arq.cron import CronJob
3334
from arq.jobs import Job
3435
from arq.typing import WorkerCoroutine
3536
from arq.worker import Function
@@ -61,7 +62,7 @@ def setup_once():
6162

6263
patch_enqueue_job()
6364
patch_run_job()
64-
patch_func()
65+
patch_create_worker()
6566

6667
ignore_logger("arq.worker")
6768

@@ -186,23 +187,40 @@ async def _sentry_coroutine(ctx, *args, **kwargs):
186187
return _sentry_coroutine
187188

188189

189-
def patch_func():
190+
def patch_create_worker():
190191
# type: () -> None
191-
old_func = arq.worker.func
192+
old_create_worker = arq.worker.create_worker
192193

193-
def _sentry_func(*args, **kwargs):
194-
# type: (*Any, **Any) -> Function
194+
def _sentry_create_worker(*args, **kwargs):
195+
# type: (*Any, **Any) -> Worker
195196
hub = Hub.current
196197

197198
if hub.get_integration(ArqIntegration) is None:
198-
return old_func(*args, **kwargs)
199+
return old_create_worker(*args, **kwargs)
199200

200-
func = old_func(*args, **kwargs)
201+
settings_cls = args[0]
201202

202-
if not getattr(func, "_sentry_is_patched", False):
203-
func.coroutine = _wrap_coroutine(func.name, func.coroutine)
204-
func._sentry_is_patched = True
203+
functions = settings_cls.functions
204+
cron_jobs = settings_cls.cron_jobs
205205

206-
return func
206+
settings_cls.functions = [_get_arq_function(func) for func in functions]
207+
settings_cls.cron_jobs = [_get_arq_cron_job(cron_job) for cron_job in cron_jobs]
207208

208-
arq.worker.func = _sentry_func
209+
return old_create_worker(*args, **kwargs)
210+
211+
arq.worker.create_worker = _sentry_create_worker
212+
213+
214+
def _get_arq_function(func):
215+
# type: (Union[str, Function, WorkerCoroutine]) -> Function
216+
arq_func = arq.worker.func(func)
217+
arq_func.coroutine = _wrap_coroutine(arq_func.name, arq_func.coroutine)
218+
219+
return arq_func
220+
221+
222+
def _get_arq_cron_job(cron_job):
223+
# type: (CronJob) -> CronJob
224+
cron_job.coroutine = _wrap_coroutine(cron_job.name, cron_job.coroutine)
225+
226+
return cron_job

tests/integrations/arq/test_arq.py

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1+
import asyncio
12
import pytest
23

34
from sentry_sdk import start_transaction
45
from sentry_sdk.integrations.arq import ArqIntegration
56

7+
import arq.worker
8+
from arq import cron
69
from arq.connections import ArqRedis
710
from arq.jobs import Job
811
from arq.utils import timestamp_ms
9-
from arq.worker import Retry, Worker
1012

1113
from fakeredis.aioredis import FakeRedis
1214

1315

16+
def async_partial(async_fn, *args, **kwargs):
17+
# asyncio.iscoroutinefunction (Used in the integration code) in Python < 3.8
18+
# does not detect async functions in functools.partial objects.
19+
# This partial implementation returns a coroutine instead.
20+
async def wrapped(ctx):
21+
return await async_fn(ctx, *args, **kwargs)
22+
23+
return wrapped
24+
25+
1426
@pytest.fixture(autouse=True)
1527
def patch_fakeredis_info_command():
1628
from fakeredis._fakesocket import FakeSocket
@@ -28,7 +40,10 @@ def info(self, section):
2840

2941
@pytest.fixture
3042
def init_arq(sentry_init):
31-
def inner(functions, allow_abort_jobs=False):
43+
def inner(functions_=None, cron_jobs_=None, allow_abort_jobs_=False):
44+
functions_ = functions_ or []
45+
cron_jobs_ = cron_jobs_ or []
46+
3247
sentry_init(
3348
integrations=[ArqIntegration()],
3449
traces_sample_rate=1.0,
@@ -38,9 +53,16 @@ def inner(functions, allow_abort_jobs=False):
3853

3954
server = FakeRedis()
4055
pool = ArqRedis(pool_or_conn=server.connection_pool)
41-
return pool, Worker(
42-
functions, redis_pool=pool, allow_abort_jobs=allow_abort_jobs
43-
)
56+
57+
class WorkerSettings:
58+
functions = functions_
59+
cron_jobs = cron_jobs_
60+
redis_pool = pool
61+
allow_abort_jobs = allow_abort_jobs_
62+
63+
worker = arq.worker.create_worker(WorkerSettings)
64+
65+
return pool, worker
4466

4567
return inner
4668

@@ -70,7 +92,7 @@ async def increase(ctx, num):
7092
async def test_job_retry(capture_events, init_arq):
7193
async def retry_job(ctx):
7294
if ctx["job_try"] < 2:
73-
raise Retry
95+
raise arq.worker.Retry
7496

7597
retry_job.__qualname__ = retry_job.__name__
7698

@@ -105,36 +127,69 @@ async def division(_, a, b=0):
105127

106128
division.__qualname__ = division.__name__
107129

108-
pool, worker = init_arq([division])
130+
cron_func = async_partial(division, a=1, b=int(not job_fails))
131+
cron_func.__qualname__ = division.__name__
132+
133+
cron_job = cron(cron_func, minute=0, run_at_startup=True)
134+
135+
pool, worker = init_arq(functions_=[division], cron_jobs_=[cron_job])
109136

110137
events = capture_events()
111138

112139
job = await pool.enqueue_job("division", 1, b=int(not job_fails))
113140
await worker.run_job(job.job_id, timestamp_ms())
114141

115-
if job_fails:
116-
error_event = events.pop(0)
117-
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
118-
assert error_event["exception"]["values"][0]["mechanism"]["type"] == "arq"
142+
loop = asyncio.get_event_loop()
143+
task = loop.create_task(worker.async_run())
144+
await asyncio.sleep(1)
119145

120-
(event,) = events
121-
assert event["type"] == "transaction"
122-
assert event["transaction"] == "division"
123-
assert event["transaction_info"] == {"source": "task"}
146+
task.cancel()
147+
148+
await worker.close()
124149

125150
if job_fails:
126-
assert event["contexts"]["trace"]["status"] == "internal_error"
127-
else:
128-
assert event["contexts"]["trace"]["status"] == "ok"
129-
130-
assert "arq_task_id" in event["tags"]
131-
assert "arq_task_retry" in event["tags"]
132-
133-
extra = event["extra"]["arq-job"]
134-
assert extra["task"] == "division"
135-
assert extra["args"] == [1]
136-
assert extra["kwargs"] == {"b": int(not job_fails)}
137-
assert extra["retry"] == 1
151+
error_func_event = events.pop(0)
152+
error_cron_event = events.pop(1)
153+
154+
assert error_func_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
155+
assert error_func_event["exception"]["values"][0]["mechanism"]["type"] == "arq"
156+
157+
func_extra = error_func_event["extra"]["arq-job"]
158+
assert func_extra["task"] == "division"
159+
160+
assert error_cron_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
161+
assert error_cron_event["exception"]["values"][0]["mechanism"]["type"] == "arq"
162+
163+
cron_extra = error_cron_event["extra"]["arq-job"]
164+
assert cron_extra["task"] == "cron:division"
165+
166+
[func_event, cron_event] = events
167+
168+
assert func_event["type"] == "transaction"
169+
assert func_event["transaction"] == "division"
170+
assert func_event["transaction_info"] == {"source": "task"}
171+
172+
assert "arq_task_id" in func_event["tags"]
173+
assert "arq_task_retry" in func_event["tags"]
174+
175+
func_extra = func_event["extra"]["arq-job"]
176+
177+
assert func_extra["task"] == "division"
178+
assert func_extra["kwargs"] == {"b": int(not job_fails)}
179+
assert func_extra["retry"] == 1
180+
181+
assert cron_event["type"] == "transaction"
182+
assert cron_event["transaction"] == "cron:division"
183+
assert cron_event["transaction_info"] == {"source": "task"}
184+
185+
assert "arq_task_id" in cron_event["tags"]
186+
assert "arq_task_retry" in cron_event["tags"]
187+
188+
cron_extra = cron_event["extra"]["arq-job"]
189+
190+
assert cron_extra["task"] == "cron:division"
191+
assert cron_extra["kwargs"] == {}
192+
assert cron_extra["retry"] == 1
138193

139194

140195
@pytest.mark.asyncio

0 commit comments

Comments
 (0)