Skip to content

Commit 26ecc05

Browse files
authored
fix(celery): Vendor parts of functools to avoid conflict with newrelic (getsentry#685)
1 parent f46373c commit 26ecc05

File tree

10 files changed

+109
-21
lines changed

10 files changed

+109
-21
lines changed

sentry_sdk/_functools.py

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
A backport of Python 3 functools to Python 2/3. The only important change
3+
we rely upon is that `update_wrapper` handles AttributeError gracefully.
4+
"""
5+
6+
from functools import partial
7+
8+
from sentry_sdk._types import MYPY
9+
10+
if MYPY:
11+
from typing import Any
12+
from typing import Callable
13+
14+
15+
WRAPPER_ASSIGNMENTS = (
16+
"__module__",
17+
"__name__",
18+
"__qualname__",
19+
"__doc__",
20+
"__annotations__",
21+
)
22+
WRAPPER_UPDATES = ("__dict__",)
23+
24+
25+
def update_wrapper(
26+
wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES
27+
):
28+
# type: (Any, Any, Any, Any) -> Any
29+
"""Update a wrapper function to look like the wrapped function
30+
31+
wrapper is the function to be updated
32+
wrapped is the original function
33+
assigned is a tuple naming the attributes assigned directly
34+
from the wrapped function to the wrapper function (defaults to
35+
functools.WRAPPER_ASSIGNMENTS)
36+
updated is a tuple naming the attributes of the wrapper that
37+
are updated with the corresponding attribute from the wrapped
38+
function (defaults to functools.WRAPPER_UPDATES)
39+
"""
40+
for attr in assigned:
41+
try:
42+
value = getattr(wrapped, attr)
43+
except AttributeError:
44+
pass
45+
else:
46+
setattr(wrapper, attr, value)
47+
for attr in updated:
48+
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
49+
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
50+
# from the wrapped function when updating __dict__
51+
wrapper.__wrapped__ = wrapped
52+
# Return the wrapper so this can be used as a decorator via partial()
53+
return wrapper
54+
55+
56+
def wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES):
57+
# type: (Callable[..., Any], Any, Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]
58+
"""Decorator factory to apply update_wrapper() to a wrapper function
59+
60+
Returns a decorator that invokes update_wrapper() with the decorated
61+
function as the wrapper argument and the arguments to wraps() as the
62+
remaining arguments. Default arguments are as for update_wrapper().
63+
This is a convenience function to simplify applying partial() to
64+
update_wrapper().
65+
"""
66+
return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)

sentry_sdk/integrations/asgi.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
"""
66

77
import asyncio
8-
import functools
98
import inspect
109
import urllib
1110

11+
from sentry_sdk._functools import partial
1212
from sentry_sdk._types import MYPY
1313
from sentry_sdk.hub import Hub, _should_send_default_pii
1414
from sentry_sdk.integrations._wsgi_common import _filter_headers
@@ -92,9 +92,7 @@ async def _run_app(self, scope, callback):
9292
with hub.configure_scope() as sentry_scope:
9393
sentry_scope.clear_breadcrumbs()
9494
sentry_scope._name = "asgi"
95-
processor = functools.partial(
96-
self.event_processor, asgi_scope=scope
97-
)
95+
processor = partial(self.event_processor, asgi_scope=scope)
9896
sentry_scope.add_event_processor(processor)
9997

10098
if scope["type"] in ("http", "websocket"):

sentry_sdk/integrations/beam.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import sys
44
import types
5-
from functools import wraps
5+
from sentry_sdk._functools import wraps
66

77
from sentry_sdk.hub import Hub
88
from sentry_sdk._compat import reraise

sentry_sdk/integrations/celery.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import absolute_import
22

3-
import functools
43
import sys
54

65
from sentry_sdk.hub import Hub
@@ -10,6 +9,7 @@
109
from sentry_sdk.integrations import Integration, DidNotEnable
1110
from sentry_sdk.integrations.logging import ignore_logger
1211
from sentry_sdk._types import MYPY
12+
from sentry_sdk._functools import wraps
1313

1414
if MYPY:
1515
from typing import Any
@@ -87,7 +87,7 @@ def sentry_build_tracer(name, task, *args, **kwargs):
8787

8888
def _wrap_apply_async(task, f):
8989
# type: (Any, F) -> F
90-
@functools.wraps(f)
90+
@wraps(f)
9191
def apply_async(*args, **kwargs):
9292
# type: (*Any, **Any) -> Any
9393
hub = Hub.current
@@ -118,7 +118,7 @@ def _wrap_tracer(task, f):
118118
# This is the reason we don't use signals for hooking in the first place.
119119
# Also because in Celery 3, signal dispatch returns early if one handler
120120
# crashes.
121-
@functools.wraps(f)
121+
@wraps(f)
122122
def _inner(*args, **kwargs):
123123
# type: (*Any, **Any) -> Any
124124
hub = Hub.current
@@ -157,7 +157,7 @@ def _wrap_task_call(task, f):
157157
# functools.wraps is important here because celery-once looks at this
158158
# method's name.
159159
# https://github.com/getsentry/sentry-python/issues/421
160-
@functools.wraps(f)
160+
@wraps(f)
161161
def _inner(*args, **kwargs):
162162
# type: (*Any, **Any) -> Any
163163
try:

sentry_sdk/integrations/django/middleware.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,17 @@
22
Create spans from Django middleware invocations
33
"""
44

5-
from functools import wraps
6-
75
from django import VERSION as DJANGO_VERSION
86

97
from sentry_sdk import Hub
8+
from sentry_sdk._functools import wraps
9+
from sentry_sdk._types import MYPY
1010
from sentry_sdk.utils import (
1111
ContextVar,
1212
transaction_from_function,
1313
capture_internal_exceptions,
1414
)
1515

16-
from sentry_sdk._types import MYPY
17-
1816
if MYPY:
1917
from typing import Any
2018
from typing import Callable

sentry_sdk/integrations/serverless.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import functools
21
import sys
32

43
from sentry_sdk.hub import Hub
54
from sentry_sdk.utils import event_from_exception
65
from sentry_sdk._compat import reraise
6+
from sentry_sdk._functools import wraps
77

88

99
from sentry_sdk._types import MYPY
@@ -42,7 +42,7 @@ def serverless_function(f=None, flush=True): # noqa
4242
# type: (Optional[F], bool) -> Union[F, Callable[[F], F]]
4343
def wrapper(f):
4444
# type: (F) -> F
45-
@functools.wraps(f)
45+
@wraps(f)
4646
def inner(*args, **kwargs):
4747
# type: (*Any, **Any) -> Any
4848
with Hub(Hub.current) as hub:

sentry_sdk/integrations/wsgi.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import functools
21
import sys
32

3+
from sentry_sdk._functools import partial
44
from sentry_sdk.hub import Hub, _should_send_default_pii
55
from sentry_sdk.utils import (
66
ContextVar,
@@ -121,9 +121,7 @@ def __call__(self, environ, start_response):
121121
try:
122122
rv = self.app(
123123
environ,
124-
functools.partial(
125-
_sentry_start_response, start_response, span
126-
),
124+
partial(_sentry_start_response, start_response, span),
127125
)
128126
except BaseException:
129127
reraise(*_capture_exception(hub))

sentry_sdk/scope.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from copy import copy
22
from collections import deque
3-
from functools import wraps
43
from itertools import chain
54

6-
from sentry_sdk.utils import logger, capture_internal_exceptions
5+
from sentry_sdk._functools import wraps
76
from sentry_sdk._types import MYPY
7+
from sentry_sdk.utils import logger, capture_internal_exceptions
88

99
if MYPY:
1010
from typing import Any

test-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ pytest-localserver==0.5.0
66
pytest-cov==2.8.1
77
gevent
88
eventlet
9+
newrelic

tests/integrations/celery/test_celery.py

+27
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,30 @@ def dummy_task(self):
309309

310310
# if this is nonempty, the worker never really forked
311311
assert not runs
312+
313+
314+
@pytest.mark.forked
315+
@pytest.mark.parametrize("newrelic_order", ["sentry_first", "sentry_last"])
316+
def test_newrelic_interference(init_celery, newrelic_order, celery_invocation):
317+
def instrument_newrelic():
318+
import celery.app.trace as celery_mod
319+
from newrelic.hooks.application_celery import instrument_celery_execute_trace
320+
321+
assert hasattr(celery_mod, "build_tracer")
322+
instrument_celery_execute_trace(celery_mod)
323+
324+
if newrelic_order == "sentry_first":
325+
celery = init_celery()
326+
instrument_newrelic()
327+
elif newrelic_order == "sentry_last":
328+
instrument_newrelic()
329+
celery = init_celery()
330+
else:
331+
raise ValueError(newrelic_order)
332+
333+
@celery.task(name="dummy_task", bind=True)
334+
def dummy_task(self, x, y):
335+
return x / y
336+
337+
assert dummy_task.apply(kwargs={"x": 1, "y": 1}).wait() == 1
338+
assert celery_invocation(dummy_task, 1, 1)[0].wait() == 1

0 commit comments

Comments
 (0)