8000 fix(celery): Vendor parts of functools to avoid conflict with newrelic by untitaker · Pull Request #685 · getsentry/sentry-python · GitHub
[go: up one dir, main page]

Skip to content

fix(celery): Vendor parts of functools to avoid conflict with newrelic #685

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

Merged
merged 8 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
57 changes: 57 additions & 0 deletions sentry_sdk/_functools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
A backport of Python 3 functools to Python 2/3. The only important change
we rely upon is that `update_wrapper` handles AttributeError gracefully.
"""

from functools import partial

WRAPPER_ASSIGNMENTS = (
"__module__",
"__name__",
"__qualname__",
"__doc__",
"__annotations__",
)
WRAPPER_UPDATES = ("__dict__",)


def update_wrapper(
wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES
):
"""Update a wrapper function to look like the wrapped function

wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
"""
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped
# Return the wrapper so this can be used as a decorator via partial()
return wrapper


def wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES):
"""Decorator factory to apply update_wrapper() to a wrapper function

Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
"""
return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"""

import asyncio
import functools
import inspect
import urllib

from sentry_sdk._functools import partial
from sentry_sdk._types import MYPY
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.integrations._wsgi_common import _filter_headers
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/beam.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import sys
import types
from functools import wraps
from sentry_sdk._functools import wraps

from sentry_sdk.hub import Hub
from sentry_sdk._compat import reraise
Expand Down
8 changes: 4 additions & 4 deletions sentry_sdk/integrations/celery.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import absolute_import

import functools
import sys

from sentry_sdk.hub import Hub
Expand All @@ -10,6 +9,7 @@
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk._types import MYPY
from sentry_sdk._functools import wraps

if MYPY:
from typing import Any
Expand Down Expand Up @@ -87,7 +87,7 @@ def sentry_build_tracer(name, task, *args, **kwargs):

def _wrap_apply_async(task, f):
# type: (Any, F) -> F
@functools.wraps(f)
@wraps(f)
def apply_async(*args, **kwargs):
# type: (*Any, **Any) -> Any
hub = Hub.current
Expand Down Expand Up @@ -118,7 +118,7 @@ def _wrap_tracer(task, f):
# This is the reason we don't use signals for hooking in the first place.
# Also because in Celery 3, signal dispatch returns early if one handler
# crashes.
@functools.wraps(f)
@wraps(f)
def _inner(*args, **kwargs):
# type: (*Any, **Any) -> Any
hub = Hub.current
Expand Down Expand Up @@ -157,7 +157,7 @@ def _wrap_task_call(task, f):
# functools.wraps is important here because celery-once looks at this
# method's name.
# https://github.com/getsentry/sentry-python/issues/421
@functools.wraps(f)
@wraps(f)
def _inner(*args, **kwargs):
# type: (*Any, **Any) -> Any
try:
Expand Down
6 changes: 2 additions & 4 deletions sentry_sdk/integrations/django/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@
Create spans from Django middleware invocations
"""

from functools import wraps

from django import VERSION as DJANGO_VERSION

from sentry_sdk import Hub
from sentry_sdk._functools import wraps
from sentry_sdk._types import MYPY
from sentry_sdk.utils import (
ContextVar,
transaction_from_function,
capture_internal_exceptions,
)

from sentry_sdk._types import MYPY

if MYPY:
from typing import Any
from typing import Callable
Expand Down
4 changes: 2 additions & 2 deletions sentry_sdk/integrations/serverless.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import functools
import sys

from sentry_sdk.hub import Hub
from sentry_sdk.utils import event_from_exception
from sentry_sdk._compat import reraise
from sentry_sdk._functools import wraps


from sentry_sdk._types import MYPY
Expand Down Expand Up @@ -42,7 +42,7 @@ def serverless_function(f=None, flush=True): # noqa
# type: (Optional[F], bool) -> Union[F, Callable[[F], F]]
def wrapper(f):
# type: (F) -> F
@functools.wraps(f)
@wraps(f)
def inner(*args, **kwargs):
# type: (*Any, **Any) -> Any
with Hub(Hub.current) as hub:
Expand Down
6 changes: 2 additions & 4 deletions sentry_sdk/integrations/wsgi.py
F438
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import functools
import sys

from sentry_sdk._functools import partial
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.utils import (
ContextVar,
Expand Down Expand Up @@ -121,9 +121,7 @@ def __call__(self, environ, start_response):
try:
rv = self.app(
environ,
functools.partial(
_sentry_start_response, start_response, span
),
partial(_sentry_start_response, start_response, span),
)
except BaseException:
reraise(*_capture_exception(hub))
Expand Down
4 changes: 2 additions & 2 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from copy import copy
from collections import deque
from functools import wraps
from itertools import chain

from sentry_sdk.utils import logger, capture_internal_exceptions
from sentry_sdk._functools import wraps
from sentry_sdk._types import MYPY
from sentry_sdk.utils import logger, capture_internal_exceptions

if MYPY:
from typing import Any
Expand Down
5 changes: 3 additions & 2 deletions test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ tox==3.7.0
Werkzeug==0.15.5
pytest-localserver==0.5.0
pytest-cov==2.8.1
gevent
eventlet
gevent==20.5.0
eventlet==0.25.2
newrelic==5.12
32 changes: 32 additions & 0 deletions tests/integrations/celery/test_celery.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import threading
import functools

import pytest

Expand Down Expand Up @@ -309,3 +310,34 @@ def dummy_task(self):

# if this is nonempty, the worker never really forked
assert not runs


@pytest.mark.forked
@pytest.mark.parametrize("newrelic_order", ["sentry_first", "sentry_last"])
def test_newrelic_interference(
capture_events, init_celery, newrelic_order, celery_invocation
):
def instrument_newrelic():
import celery.app.trace as celery_mod
from newrelic.hooks.application_celery import instrument_celery_execute_trace

assert hasattr(celery_mod, "build_tracer")
instrument_celery_execute_trace(celery_mod)

if newrelic_order == "sentry_first":
celery = init_celery()
instrument_newrelic()
elif newrelic_order == "sentry_last":
instrument_newrelic()
celery = init_celery()
else:
raise ValueError(newrelic_order)

events = capture_events()

@celery.task(name="dummy_task", bind=True)
def dummy_task(self, x, y):
return x / y

assert dummy_task.apply(kwargs={"x": 1, "y": 1}).wait() == 1
assert celery_invocation(dummy_task, 1, 1)[0].wait() == 1
0