|
7 | 7 | import zlib |
8 | 8 | from functools import wraps, partial |
9 | 9 | from threading import Event, Lock, Thread |
| 10 | +from contextlib import contextmanager |
10 | 11 |
|
11 | 12 | from sentry_sdk._compat import text_type |
12 | 13 | from sentry_sdk.hub import Hub |
|
26 | 27 | from typing import Iterable |
27 | 28 | from typing import Callable |
28 | 29 | from typing import Optional |
| 30 | + from typing import Generator |
29 | 31 | from typing import Tuple |
30 | 32 |
|
31 | 33 | from sentry_sdk._types import BucketKey |
|
53 | 55 | ) |
54 | 56 |
|
55 | 57 |
|
| 58 | +@contextmanager |
| 59 | +def recursion_protection(): |
| 60 | + # type: () -> Generator[bool, None, None] |
| 61 | + """Enters recursion protection and returns the old flag.""" |
| 62 | + try: |
| 63 | + in_metrics = _thread_local.in_metrics |
| 64 | + except AttributeError: |
| 65 | + in_metrics = False |
| 66 | + _thread_local.in_metrics = True |
| 67 | + try: |
| 68 | + yield in_metrics |
| 69 | + finally: |
| 70 | + _thread_local.in_metrics = in_metrics |
| 71 | + |
| 72 | + |
56 | 73 | def metrics_noop(func): |
57 | 74 | # type: (Any) -> Any |
| 75 | + """Convenient decorator that uses `recursion_protection` to |
| 76 | + make a function a noop. |
| 77 | + """ |
| 78 | + |
58 | 79 | @wraps(func) |
59 | 80 | def new_func(*args, **kwargs): |
60 | 81 | # type: (*Any, **Any) -> Any |
61 | | - try: |
62 | | - in_metrics = _thread_local.in_metrics |
63 | | - except AttributeError: |
64 | | - in_metrics = False |
65 | | - _thread_local.in_metrics = True |
66 | | - try: |
| 82 | + with recursion_protection() as in_metrics: |
67 | 83 | if not in_metrics: |
68 | 84 | return func(*args, **kwargs) |
69 | | - finally: |
70 | | - _thread_local.in_metrics = in_metrics |
71 | 85 |
|
72 | 86 | return new_func |
73 | 87 |
|
@@ -449,7 +463,16 @@ def _emit( |
449 | 463 | encoded_metrics = _encode_metrics(flushable_buckets) |
450 | 464 | metric_item = Item(payload=encoded_metrics, type="statsd") |
451 | 465 | envelope = Envelope(items=[metric_item]) |
452 | | - self._capture_func(envelope) |
| 466 | + |
| 467 | + # A malfunctioning transport might create a forever loop of metric |
| 468 | + # emission when it emits a metric in capture_envelope. We still |
| 469 | + # allow the capture to take place, but interior metric incr calls |
| 470 | + # or similar will be disabled. In the background thread this can |
| 471 | + # never happen, but in the force flush case which happens in the |
| 472 | + # foreground we might make it here unprotected. |
| 473 | + with recursion_protection(): |
| 474 | + self._capture_func(envelope) |
| 475 | + |
453 | 476 | return envelope |
454 | 477 |
|
455 | 478 | def _serialize_tags( |
@@ -495,8 +518,10 @@ def _get_aggregator_and_update_tags(key, tags): |
495 | 518 |
|
496 | 519 | callback = client.options.get("_experiments", {}).get("before_emit_metric") |
497 | 520 | if callback is not None: |
498 | | - if not callback(key, updated_tags): |
499 | | - return None, updated_tags |
| 521 | + with recursion_protection() as in_metrics: |
| 522 | + if not in_metrics: |
| 523 | + if not callback(key, updated_tags): |
| 524 | + return None, updated_tags |
500 | 525 |
|
501 | 526 | return client.metrics_aggregator, updated_tags |
502 | 527 |
|
|
0 commit comments