Skip to content

Commit fbc97ab

Browse files
authored
fix(metrics): Fix compatibility with greenlet/gevent (#2756)
1 parent 2eeb8c5 commit fbc97ab

File tree

4 files changed

+82
-49
lines changed

4 files changed

+82
-49
lines changed

sentry_sdk/client.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import socket
66

77
from sentry_sdk._compat import (
8+
PY37,
89
datetime_utcnow,
910
string_types,
1011
text_type,
@@ -20,6 +21,7 @@
2021
get_type_name,
2122
get_default_release,
2223
handle_in_app,
24+
is_gevent,
2325
logger,
2426
)
2527
from sentry_sdk.serializer import serialize
@@ -256,14 +258,22 @@ def _capture_envelope(envelope):
256258
self.metrics_aggregator = None # type: Optional[MetricsAggregator]
257259
experiments = self.options.get("_experiments", {})
258260
if experiments.get("enable_metrics", True):
259-
from sentry_sdk.metrics import MetricsAggregator
260-
261-
self.metrics_aggregator = MetricsAggregator(
262-
capture_func=_capture_envelope,
263-
enable_code_locations=bool(
264-
experiments.get("metric_code_locations", True)
265-
),
266-
)
261+
# Context vars are not working correctly on Python <=3.6
262+
# with gevent.
263+
metrics_supported = not is_gevent() or PY37
264+
if metrics_supported:
265+
from sentry_sdk.metrics import MetricsAggregator
266+
267+
self.metrics_aggregator = MetricsAggregator(
268+
capture_func=_capture_envelope,
269+
enable_code_locations=bool(
270+
experiments.get("metric_code_locations", True)
271+
),
272+
)
273+
else:
274+
logger.info(
275+
"Metrics not supported on Python 3.6 and lower with gevent."
276+
)
267277

268278
max_request_body_size = ("always", "never", "small", "medium")
269279
if self.options["max_request_body_size"] not in max_request_body_size:

sentry_sdk/metrics.py

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@
1111
from functools import wraps, partial
1212

1313
import sentry_sdk
14-
from sentry_sdk._compat import PY2, text_type, utc_from_timestamp, iteritems
14+
from sentry_sdk._compat import text_type, utc_from_timestamp, iteritems
1515
from sentry_sdk.utils import (
1616
ContextVar,
1717
now,
1818
nanosecond_time,
1919
to_timestamp,
2020
serialize_frame,
2121
json_dumps,
22-
is_gevent,
2322
)
2423
from sentry_sdk.envelope import Envelope, Item
2524
from sentry_sdk.tracing import (
@@ -54,18 +53,7 @@
5453
from sentry_sdk._types import MetricValue
5554

5655

57-
try:
58-
from gevent.monkey import get_original # type: ignore
59-
from gevent.threadpool import ThreadPool # type: ignore
60-
except ImportError:
61-
import importlib
62-
63-
def get_original(module, name):
64-
# type: (str, str) -> Any
65-
return getattr(importlib.import_module(module), name)
66-
67-
68-
_in_metrics = ContextVar("in_metrics")
56+
_in_metrics = ContextVar("in_metrics", default=False)
6957
_sanitize_key = partial(re.compile(r"[^a-zA-Z0-9_/.-]+").sub, "_")
7058
_sanitize_value = partial(re.compile(r"[^\w\d_:/@\.{}\[\]$-]+", re.UNICODE).sub, "_")
7159
_set = set # set is shadowed below
@@ -96,7 +84,7 @@ def get_code_location(stacklevel):
9684
def recursion_protection():
9785
# type: () -> Generator[bool, None, None]
9886
"""Enters recursion protection and returns the old flag."""
99-
old_in_metrics = _in_metrics.get(False)
87+
old_in_metrics = _in_metrics.get()
10088
_in_metrics.set(True)
10189
try:
10290
yield old_in_metrics
@@ -423,16 +411,7 @@ def __init__(
423411
self._running = True
424412
self._lock = threading.Lock()
425413

426-
if is_gevent() and PY2:
427-
# get_original on threading.Event in Python 2 incorrectly returns
428-
# the gevent-patched class. Luckily, threading.Event is just an alias
429-
# for threading._Event in Python 2, and get_original on
430-
# threading._Event correctly gets us the stdlib original.
431-
event_cls = get_original("threading", "_Event")
432-
else:
433-
event_cls = get_original("threading", "Event")
434-
self._flush_event = event_cls() # type: threading.Event
435-
414+
self._flush_event = threading.Event() # type: threading.Event
436415
self._force_flush = False
437416

438417
# The aggregator shifts its flushing by up to an entire rollup window to
@@ -443,7 +422,7 @@ def __init__(
443422
# jittering.
444423
self._flush_shift = random.random() * self.ROLLUP_IN_SECONDS
445424

446-
self._flusher = None # type: Optional[Union[threading.Thread, ThreadPool]]
425+
self._flusher = None # type: Optional[threading.Thread]
447426
self._flusher_pid = None # type: Optional[int]
448427

449428
def _ensure_thread(self):
@@ -466,16 +445,11 @@ def _ensure_thread(self):
466445

467446
self._flusher_pid = pid
468447

469-
if not is_gevent():
470-
self._flusher = threading.Thread(target=self._flush_loop)
471-
self._flusher.daemon = True
472-
start_flusher = self._flusher.start
473-
else:
474-
self._flusher = ThreadPool(1)
475-
start_flusher = partial(self._flusher.spawn, func=self._flush_loop)
448+
self._flusher = threading.Thread(target=self._flush_loop)
449+
self._flusher.daemon = True
476450

477451
try:
478-
start_flusher()
452+
self._flusher.start()
479453
except RuntimeError:
480454
# Unfortunately at this point the interpreter is in a state that no
481455
# longer allows us to spawn a thread and we have to bail.

tests/test_metrics.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@
1313
except ImportError:
1414
import mock # python < 3.3
1515

16+
try:
17+
import gevent
18+
except ImportError:
19+
gevent = None
20+
21+
22+
minimum_python_37_with_gevent = pytest.mark.skipif(
23+
gevent and sys.version_info < (3, 7),
24+
reason="Require Python 3.7 or higher with gevent",
25+
)
26+
1627

1728
def parse_metrics(bytes):
1829
rv = []
@@ -45,6 +56,7 @@ def parse_metrics(bytes):
4556
return rv
4657

4758

59+
@minimum_python_37_with_gevent
4860
@pytest.mark.forked
4961
def test_incr(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
5062
sentry_init(
@@ -97,6 +109,7 @@ def test_incr(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
97109
}
98110

99111

112+
@minimum_python_37_with_gevent
100113
@pytest.mark.forked
101114
def test_timing(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
102115
sentry_init(
@@ -157,6 +170,7 @@ def test_timing(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
157170
)
158171

159172

173+
@minimum_python_37_with_gevent
160174
@pytest.mark.forked
161175
def test_timing_decorator(
162176
sentry_init, capture_envelopes, maybe_monkeypatched_threading
@@ -252,6 +266,7 @@ def amazing_nano():
252266
assert line.strip() == "assert amazing() == 42"
253267

254268

269+
@minimum_python_37_with_gevent
255270
@pytest.mark.forked
256271
def test_timing_basic(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
257272
sentry_init(
@@ -306,6 +321,7 @@ def test_timing_basic(sentry_init, capture_envelopes, maybe_monkeypatched_thread
306321
}
307322

308323

324+
@minimum_python_37_with_gevent
309325
@pytest.mark.forked
310326
def test_distribution(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
311327
sentry_init(
@@ -368,6 +384,7 @@ def test_distribution(sentry_init, capture_envelopes, maybe_monkeypatched_thread
368384
)
369385

370386

387+
@minimum_python_37_with_gevent
371388
@pytest.mark.forked
372389
def test_set(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
373390
sentry_init(
@@ -421,6 +438,7 @@ def test_set(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
421438
}
422439

423440

441+
@minimum_python_37_with_gevent
424442
@pytest.mark.forked
425443
def test_gauge(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
426444
sentry_init(
@@ -454,6 +472,7 @@ def test_gauge(sentry_init, capture_envelopes, maybe_monkeypatched_threading):
454472
}
455473

456474

475+
@minimum_python_37_with_gevent
457476
@pytest.mark.forked
458477
def test_multiple(sentry_init, capture_envelopes):
459478
sentry_init(
@@ -508,6 +527,7 @@ def test_multiple(sentry_init, capture_envelopes):
508527
}
509528

510529

530+
@minimum_python_37_with_gevent
511531
@pytest.mark.forked
512532
def test_transaction_name(
513533
sentry_init, capture_envelopes, maybe_monkeypatched_threading
@@ -548,6 +568,7 @@ def test_transaction_name(
548568
}
549569

550570

571+
@minimum_python_37_with_gevent
551572
@pytest.mark.forked
552573
@pytest.mark.parametrize("sample_rate", [1.0, None])
553574
def test_metric_summaries(
@@ -658,6 +679,7 @@ def test_metric_summaries(
658679
}
659680

660681

682+
@minimum_python_37_with_gevent
661683
@pytest.mark.forked
662684
def test_metrics_summary_disabled(
663685
sentry_init, capture_envelopes, maybe_monkeypatched_threading
@@ -702,6 +724,7 @@ def test_metrics_summary_disabled(
702724
assert "_metrics_summary" not in t["spans"][0]
703725

704726

727+
@minimum_python_37_with_gevent
705728
@pytest.mark.forked
706729
def test_metrics_summary_filtered(
707730
sentry_init, capture_envelopes, maybe_monkeypatched_threading
@@ -771,6 +794,7 @@ def should_summarize_metric(key, tags):
771794
} in t["d:foo@second"]
772795

773796

797+
@minimum_python_37_with_gevent
774798
@pytest.mark.forked
775799
def test_tag_normalization(
776800
sentry_init, capture_envelopes, maybe_monkeypatched_threading
@@ -818,6 +842,7 @@ def test_tag_normalization(
818842
# fmt: on
819843

820844

845+
@minimum_python_37_with_gevent
821846
@pytest.mark.forked
822847
def test_before_emit_metric(
823848
sentry_init, capture_envelopes, maybe_monkeypatched_threading
@@ -861,6 +886,7 @@ def before_emit(key, tags):
861886
}
862887

863888

889+
@minimum_python_37_with_gevent
864890
@pytest.mark.forked
865891
def test_aggregator_flush(
866892
sentry_init, capture_envelopes, maybe_monkeypatched_threading
@@ -881,6 +907,7 @@ def test_aggregator_flush(
881907
assert Hub.current.client.metrics_aggregator.buckets == {}
882908

883909

910+
@minimum_python_37_with_gevent
884911
@pytest.mark.forked
885912
def test_tag_serialization(
886913
sentry_init, capture_envelopes, maybe_monkeypatched_threading
@@ -921,6 +948,7 @@ def test_tag_serialization(
921948
}
922949

923950

951+
@minimum_python_37_with_gevent
924952
@pytest.mark.forked
925953
def test_flush_recursion_protection(
926954
sentry_init, capture_envelopes, monkeypatch, maybe_monkeypatched_threading
@@ -953,11 +981,12 @@ def bad_capture_envelope(*args, **kwargs):
953981
assert m[0][1] == "counter@none"
954982

955983

984+
@minimum_python_37_with_gevent
956985
@pytest.mark.forked
957986
def test_flush_recursion_protection_background_flush(
958987
sentry_init, capture_envelopes, monkeypatch, maybe_monkeypatched_threading
959988
):
960-
monkeypatch.setattr(metrics.MetricsAggregator, "FLUSHER_SLEEP_TIME", 0.1)
989+
monkeypatch.setattr(metrics.MetricsAggregator, "FLUSHER_SLEEP_TIME", 0.01)
961990
sentry_init(
962991
release="fun-release",
963992
environment="not-fun-env",
@@ -984,3 +1013,29 @@ def bad_capture_envelope(*args, **kwargs):
9841013
m = parse_metrics(envelope.items[0].payload.get_bytes())
9851014
assert len(m) == 1
9861015
assert m[0][1] == "counter@none"
1016+
1017+
1018+
@pytest.mark.skipif(
1019+
not gevent or sys.version_info >= (3, 7),
1020+
reason="Python 3.6 or lower and gevent required",
1021+
)
1022+
@pytest.mark.forked
1023+
def test_disable_metrics_for_old_python_with_gevent(
1024+
sentry_init, capture_envelopes, maybe_monkeypatched_threading
1025+
):
1026+
if maybe_monkeypatched_threading != "greenlet":
1027+
pytest.skip("Test specifically for gevent/greenlet")
1028+
1029+
sentry_init(
1030+
release="fun-release",
1031+
environment="not-fun-env",
1032+
_experiments={"enable_metrics": True},
1033+
)
1034+
envelopes = capture_envelopes()
1035+
1036+
metrics.incr("counter")
1037+
1038+
Hub.current.flush()
1039+
1040+
assert Hub.current.client.metrics_aggregator is None
1041+
assert not envelopes

tox.ini

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,6 @@ deps =
247247
{py3.6,py3.7,py3.8,py3.9,py3.10,py3.11,py3.12}-common: pytest<7.0.0
248248

249249
# === Gevent ===
250-
# See http://www.gevent.org/install.html#older-versions-of-python
251-
# for justification of the versions pinned below
252-
py3.5-gevent: gevent==20.9.0
253-
# See https://stackoverflow.com/questions/51496550/runtime-warning-greenlet-greenlet-size-changed
254-
# for justification why greenlet is pinned here
255-
py3.5-gevent: greenlet==0.4.17
256250
{py2.7,py3.6,py3.7,py3.8,py3.9,py3.10,py3.11,py3.12}-gevent: gevent>=22.10.0, <22.11.0
257251
# See https://github.com/pytest-dev/pytest/issues/9621
258252
# and https://github.com/pytest-dev/pytest-forked/issues/67

0 commit comments

Comments
 (0)