Skip to content

Commit a08ce8e

Browse files
authored
fix(huey): Fix group and chord handling in enqueue (#6392)
The Huey integration was not properly handling task groups and chords when enqueuing. When a group/chord is enqueued, we would attempt to access the `name` attribute of the group/chord object for the span. They don't have one, causing an AttributeError. Fixes py-2426 Fixes #6310
1 parent 212e0cd commit a08ce8e

2 files changed

Lines changed: 144 additions & 5 deletions

File tree

sentry_sdk/integrations/huey.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
except ImportError:
3535
raise DidNotEnable("Huey is not installed")
3636

37+
try:
38+
from huey.api import chord as HueyChord
39+
from huey.api import group as HueyGroup
40+
except ImportError:
41+
HueyChord = None
42+
HueyGroup = None
43+
3744

3845
HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException)
3946

@@ -53,22 +60,37 @@ def patch_enqueue() -> None:
5360

5461
@ensure_integration_enabled(HueyIntegration, old_enqueue)
5562
def _sentry_enqueue(
56-
self: "Huey", task: "Task"
63+
self: "Huey", item: "Union[Task, HueyGroup, HueyChord]"
5764
) -> "Optional[Union[Result, ResultGroup]]":
65+
if HueyChord is not None and isinstance(item, HueyChord):
66+
span_name = "Huey Chord"
67+
elif HueyGroup is not None and isinstance(item, HueyGroup):
68+
span_name = "Huey Task Group"
69+
else:
70+
span_name = item.name
71+
5872
with sentry_sdk.start_span(
5973
op=OP.QUEUE_SUBMIT_HUEY,
60-
name=task.name,
74+
name=span_name,
6175
origin=HueyIntegration.origin,
6276
):
63-
if not isinstance(task, PeriodicTask):
77+
if (
78+
not isinstance(item, PeriodicTask)
79+
and not (HueyGroup is not None and isinstance(item, HueyGroup))
80+
and not (HueyChord is not None and isinstance(item, HueyChord))
81+
):
6482
# Attach trace propagation data to task kwargs. We do
6583
# not do this for periodic tasks, as these don't
6684
# really have an originating transaction.
67-
task.kwargs["sentry_headers"] = {
85+
# Additionally, we do not do this for Huey groups or chords, as enqueue will
86+
# recursively call this method for each task within the list, resulting
87+
# in the trace propagation data being attached to each task individually
88+
# (which we want)
89+
item.kwargs["sentry_headers"] = {
6890
BAGGAGE_HEADER_NAME: get_baggage(),
6991
SENTRY_TRACE_HEADER_NAME: get_traceparent(),
7092
}
71-
return old_enqueue(self, task)
93+
return old_enqueue(self, item)
7294

7395
Huey.enqueue = _sentry_enqueue
7496

tests/integrations/huey/test_huey.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111

1212
HUEY_VERSION = parse_version(HUEY_VERSION)
1313

14+
try:
15+
from huey.api import chord, group
16+
except ImportError:
17+
chord = None
18+
group = None
19+
1420

1521
@pytest.fixture
1622
def init_huey(sentry_init):
@@ -222,3 +228,114 @@ def propagated_trace_task():
222228
(event,) = events
223229

224230
assert event["contexts"]["trace"]["origin"] == "auto.queue.huey"
231+
232+
233+
@pytest.mark.skipif(HUEY_VERSION < (3, 0), reason="group was added in 3.0")
234+
def test_huey_enqueue_group(init_huey, capture_events):
235+
huey = init_huey()
236+
237+
events = capture_events()
238+
239+
@huey.task()
240+
def task1():
241+
pass
242+
243+
@huey.task()
244+
def task2():
245+
pass
246+
247+
with start_transaction() as transaction:
248+
huey.enqueue(group([task1.s(), task2.s()]))
249+
250+
for _ in range(2):
251+
task = huey.dequeue()
252+
huey.execute(task)
253+
254+
assert len(events) == 3
255+
256+
# Assert enqueue spans were successfully recorded
257+
producer_event = events[0]
258+
assert producer_event["type"] == "transaction"
259+
assert producer_event["contexts"]["trace"]["trace_id"] == transaction.trace_id
260+
assert producer_event["contexts"]["trace"]["origin"] == "manual"
261+
262+
spans = producer_event["spans"]
263+
assert len(spans) == 3
264+
assert spans[0]["op"] == "queue.submit.huey"
265+
assert spans[0]["description"] == "Huey Task Group"
266+
assert spans[1]["op"] == "queue.submit.huey"
267+
assert spans[1]["description"] == "task1"
268+
assert spans[2]["op"] == "queue.submit.huey"
269+
assert spans[2]["description"] == "task2"
270+
271+
# Consumer transaction assertions (one per task)
272+
consumer_events = events[1:]
273+
for _, (consumer_event, expected_name) in enumerate(
274+
zip(consumer_events, ["task1", "task2"])
275+
):
276+
assert consumer_event["type"] == "transaction"
277+
assert consumer_event["transaction"] == expected_name
278+
assert consumer_event["transaction_info"] == {"source": "task"}
279+
assert consumer_event["contexts"]["trace"]["op"] == "queue.task.huey"
280+
assert consumer_event["contexts"]["trace"]["origin"] == "auto.queue.huey"
281+
assert consumer_event["contexts"]["trace"]["status"] == "ok"
282+
assert consumer_event["contexts"]["trace"]["trace_id"] == transaction.trace_id
283+
assert "huey_task_id" in consumer_event["tags"]
284+
assert consumer_event["tags"]["huey_task_retry"] is False
285+
286+
287+
@pytest.mark.skipif(HUEY_VERSION < (3, 0), reason="chord was added in 3.0")
288+
def test_huey_enqueue_chord(init_huey, capture_events):
289+
huey = init_huey()
290+
291+
events = capture_events()
292+
293+
@huey.task()
294+
def task1():
295+
pass
296+
297+
@huey.task()
298+
def task2(results):
299+
pass
300+
301+
with start_transaction() as transaction:
302+
huey.enqueue(chord([task1.s()], task2.s()))
303+
304+
for _ in range(2):
305+
task = huey.dequeue()
306+
huey.execute(task)
307+
308+
assert len(events) == 3
309+
310+
# Enqueue spans
311+
producer_event = events[0]
312+
assert producer_event["contexts"]["trace"]["trace_id"] == transaction.trace_id
313+
assert producer_event["contexts"]["trace"]["origin"] == "manual"
314+
315+
spans = producer_event["spans"]
316+
assert len(spans) == 2
317+
assert spans[0]["op"] == "queue.submit.huey"
318+
assert spans[0]["description"] == "Huey Chord"
319+
assert spans[1]["op"] == "queue.submit.huey"
320+
assert spans[1]["description"] == "task1"
321+
322+
task1_event = events[1]
323+
# Confirm the first task enqueued the chord callback
324+
task1_spans = task1_event["spans"]
325+
assert len(task1_spans) == 1
326+
assert task1_spans[0]["op"] == "queue.submit.huey"
327+
assert task1_spans[0]["description"] == "task2"
328+
329+
consumer_events = events[1:]
330+
for _, (consumer_event, expected_name) in enumerate(
331+
zip(consumer_events, ["task1", "task2"])
332+
):
333+
assert consumer_event["type"] == "transaction"
334+
assert consumer_event["transaction"] == expected_name
335+
assert consumer_event["transaction_info"] == {"source": "task"}
336+
assert consumer_event["contexts"]["trace"]["op"] == "queue.task.huey"
337+
assert consumer_event["contexts"]["trace"]["origin"] == "auto.queue.huey"
338+
assert consumer_event["contexts"]["trace"]["status"] == "ok"
339+
assert consumer_event["contexts"]["trace"]["trace_id"] == transaction.trace_id
340+
assert "huey_task_id" in consumer_event["tags"]
341+
assert consumer_event["tags"]["huey_task_retry"] is False

0 commit comments

Comments
 (0)