sentry-arroyo 2.37.1__py3-none-any.whl → 2.38.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arroyo/processing/processor.py +4 -22
- arroyo/utils/stuck_detector.py +58 -0
- {sentry_arroyo-2.37.1.dist-info → sentry_arroyo-2.38.0.dist-info}/METADATA +1 -1
- {sentry_arroyo-2.37.1.dist-info → sentry_arroyo-2.38.0.dist-info}/RECORD +8 -6
- tests/utils/test_stuck_detector.py +46 -0
- {sentry_arroyo-2.37.1.dist-info → sentry_arroyo-2.38.0.dist-info}/WHEEL +0 -0
- {sentry_arroyo-2.37.1.dist-info → sentry_arroyo-2.38.0.dist-info}/licenses/LICENSE +0 -0
- {sentry_arroyo-2.37.1.dist-info → sentry_arroyo-2.38.0.dist-info}/top_level.txt +0 -0
arroyo/processing/processor.py
CHANGED
|
@@ -30,6 +30,7 @@ from arroyo.processing.strategies.abstract import (
|
|
|
30
30
|
from arroyo.types import BrokerValue, Message, Partition, Topic, TStrategyPayload
|
|
31
31
|
from arroyo.utils.logging import handle_internal_error
|
|
32
32
|
from arroyo.utils.metrics import get_metrics
|
|
33
|
+
from arroyo.utils.stuck_detector import get_all_thread_stacks
|
|
33
34
|
|
|
34
35
|
logger = logging.getLogger(__name__)
|
|
35
36
|
|
|
@@ -40,25 +41,6 @@ DEFAULT_JOIN_TIMEOUT = 25.0 # In seconds
|
|
|
40
41
|
F = TypeVar("F", bound=Callable[[Any], Any])
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def get_all_thread_stacks() -> str:
|
|
44
|
-
"""Get stack traces from all threads without using signals."""
|
|
45
|
-
import sys
|
|
46
|
-
import threading
|
|
47
|
-
import traceback
|
|
48
|
-
|
|
49
|
-
stacks = []
|
|
50
|
-
frames = sys._current_frames()
|
|
51
|
-
threads_by_id = {t.ident: t for t in threading.enumerate()}
|
|
52
|
-
|
|
53
|
-
for thread_id, frame in frames.items():
|
|
54
|
-
thread = threads_by_id.get(thread_id)
|
|
55
|
-
thread_name = thread.name if thread else f"Unknown-{thread_id}"
|
|
56
|
-
stack = "".join(traceback.format_stack(frame))
|
|
57
|
-
stacks.append(f"Thread {thread_name} ({thread_id}):\n{stack}")
|
|
58
|
-
|
|
59
|
-
return "\n\n".join(stacks)
|
|
60
|
-
|
|
61
|
-
|
|
62
44
|
def _rdkafka_callback(metrics: MetricsBuffer) -> Callable[[F], F]:
|
|
63
45
|
def decorator(f: F) -> F:
|
|
64
46
|
@functools.wraps(f)
|
|
@@ -166,9 +148,9 @@ class StreamProcessor(Generic[TStrategyPayload]):
|
|
|
166
148
|
self.__processor_factory = processor_factory
|
|
167
149
|
self.__metrics_buffer = MetricsBuffer()
|
|
168
150
|
|
|
169
|
-
self.__processing_strategy: Optional[
|
|
170
|
-
|
|
171
|
-
|
|
151
|
+
self.__processing_strategy: Optional[ProcessingStrategy[TStrategyPayload]] = (
|
|
152
|
+
None
|
|
153
|
+
)
|
|
172
154
|
|
|
173
155
|
self.__message: Optional[BrokerValue[TStrategyPayload]] = None
|
|
174
156
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import traceback
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Iterator
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_all_thread_stacks() -> str:
|
|
15
|
+
"""Get stack traces from all threads without using signals."""
|
|
16
|
+
stacks = []
|
|
17
|
+
frames = sys._current_frames()
|
|
18
|
+
threads_by_id = {t.ident: t for t in threading.enumerate()}
|
|
19
|
+
|
|
20
|
+
for thread_id, frame in frames.items():
|
|
21
|
+
thread = threads_by_id.get(thread_id)
|
|
22
|
+
thread_name = thread.name if thread else f"Unknown-{thread_id}"
|
|
23
|
+
stack = "".join(traceback.format_stack(frame))
|
|
24
|
+
stacks.append(f"Thread {thread_name} ({thread_id}):\n{stack}")
|
|
25
|
+
|
|
26
|
+
return "\n\n".join(stacks)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextmanager
|
|
30
|
+
def stuck_detector(
|
|
31
|
+
name: str = "stuck-detector",
|
|
32
|
+
timeout_seconds: float = 30.0,
|
|
33
|
+
) -> Iterator[None]:
|
|
34
|
+
"""
|
|
35
|
+
Context manager that spawns a daemon thread to detect stuck operations.
|
|
36
|
+
If the wrapped code block doesn't complete within timeout_seconds,
|
|
37
|
+
logs all thread stack traces for debugging.
|
|
38
|
+
"""
|
|
39
|
+
done = threading.Event()
|
|
40
|
+
|
|
41
|
+
def detector() -> None:
|
|
42
|
+
start = time.time()
|
|
43
|
+
while not done.wait(timeout=1):
|
|
44
|
+
if time.time() - start > timeout_seconds:
|
|
45
|
+
logger.warning(
|
|
46
|
+
"%s: Operation stuck for %s seconds, stacks:\n%s",
|
|
47
|
+
name,
|
|
48
|
+
timeout_seconds,
|
|
49
|
+
get_all_thread_stacks(),
|
|
50
|
+
)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
thread = threading.Thread(target=detector, daemon=True, name=name)
|
|
54
|
+
thread.start()
|
|
55
|
+
try:
|
|
56
|
+
yield
|
|
57
|
+
finally:
|
|
58
|
+
done.set()
|
|
@@ -16,7 +16,7 @@ arroyo/backends/local/storages/__init__.py,sha256=AGYujdAAcn3osoj9jq84IzTywYbkID
|
|
|
16
16
|
arroyo/backends/local/storages/abstract.py,sha256=1qVQp6roxHkK6XT2aklZyZk1qq7RzcPN6Db_CA5--kg,2901
|
|
17
17
|
arroyo/backends/local/storages/memory.py,sha256=AoKDsVZzBXkOJyWArKWp3vfGfU9xLlKFXE9gsJiMIzQ,2613
|
|
18
18
|
arroyo/processing/__init__.py,sha256=vZVg0wJvJfoVzlzGvnL59bT6YNIRJNQ5t7oU045Qbk4,87
|
|
19
|
-
arroyo/processing/processor.py,sha256
|
|
19
|
+
arroyo/processing/processor.py,sha256=-ZN1pZhEMLa9fkRA_FSnTgQ7FxXc1xuEXZtzHBvVIpA,22063
|
|
20
20
|
arroyo/processing/strategies/__init__.py,sha256=EU_JMb54eOxMxaC5mIFpI-sAF-X2ZScbE8czBZ7bQkY,1106
|
|
21
21
|
arroyo/processing/strategies/abstract.py,sha256=nu7juEz_aQmQIH35Z8u--FBuLjkK8_LQ1hIG2xpw9AA,4808
|
|
22
22
|
arroyo/processing/strategies/batching.py,sha256=s89xC6lQpBseEaApu1iNTipXGKeO95OMwinj2VBKn9s,4778
|
|
@@ -42,11 +42,12 @@ arroyo/utils/metric_defs.py,sha256=Dmw5gBcH80sz_Z5hNH2tmUpGh8IN4QDPA3OEwR_KENQ,9
|
|
|
42
42
|
arroyo/utils/metrics.py,sha256=kcyUR5cacoPMoU80RHSUhTMNzEcMBDpTXzcyW7yWZBk,3308
|
|
43
43
|
arroyo/utils/profiler.py,sha256=aiYy2RRPX_IiDIO7AnFM3hARaHCctS3rqUS5nrHXbSg,2452
|
|
44
44
|
arroyo/utils/retries.py,sha256=4MRhHUR7da9x1ytlo7YETo8S9HEebXmPF2-mKP4xYz0,3445
|
|
45
|
+
arroyo/utils/stuck_detector.py,sha256=bHJbOuVKa_AGI0i0l3EbdTI0mwIub2svtY9epeS1a4g,1670
|
|
45
46
|
examples/transform_and_produce/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
47
|
examples/transform_and_produce/batched.py,sha256=st2R6qTneAtV0JFbKP30Ti3sJDYj8Jkbmta9JckKdZU,2636
|
|
47
48
|
examples/transform_and_produce/script.py,sha256=8kSMIjQNqGYEVyE0PvrfJh-a_UYCrJSstTp_De7kyyg,2306
|
|
48
49
|
examples/transform_and_produce/simple.py,sha256=H7xqxItjl4tx34wVW5dy6mB9G39QucAtxkJSBzVmjgA,1637
|
|
49
|
-
sentry_arroyo-2.
|
|
50
|
+
sentry_arroyo-2.38.0.dist-info/licenses/LICENSE,sha256=0Ng3MFdEcnz0sVD1XvGBBzbavvNp_7OAM5yVObB46jU,10829
|
|
50
51
|
tests/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
52
|
tests/backends/mixins.py,sha256=sfNyE0VTeiD3GHOnBYl-9urvPuURI2G1BWke0cz7Dvc,20445
|
|
52
53
|
tests/backends/test_commit.py,sha256=iTHfK1qsBxim0XwxgMvNNSMqDUMEHoYkYBDcgxGBFbs,831
|
|
@@ -75,7 +76,8 @@ tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
75
76
|
tests/utils/test_concurrent.py,sha256=Gwdzym2UZ1HO3rhOSGmzxImWcLFygY8P7MXHT3Q0xTE,455
|
|
76
77
|
tests/utils/test_metrics.py,sha256=bI0EtGgPokMQyEqX58i0-8zvLfxRP2nWaWr2wLMaJ_o,917
|
|
77
78
|
tests/utils/test_retries.py,sha256=AxJLkXWeL9AjHv_p1n0pe8CXXJp24ZQIuYBHfNcmiz4,3075
|
|
78
|
-
|
|
79
|
-
sentry_arroyo-2.
|
|
80
|
-
sentry_arroyo-2.
|
|
81
|
-
sentry_arroyo-2.
|
|
79
|
+
tests/utils/test_stuck_detector.py,sha256=IcNqL51L-UwDXA2-w70NB3nYrxLFwWqCdWhI5O5Q5Rs,1574
|
|
80
|
+
sentry_arroyo-2.38.0.dist-info/METADATA,sha256=2D8NJrqb6DLK70kjcx93sAiRdoxSmxBjFBO9IXC35Rg,2208
|
|
81
|
+
sentry_arroyo-2.38.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
82
|
+
sentry_arroyo-2.38.0.dist-info/top_level.txt,sha256=DVdMZKysL_iIxm5aY0sYgZtP5ZXMg9YBaBmGQHVmDXA,22
|
|
83
|
+
sentry_arroyo-2.38.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
from arroyo.utils.stuck_detector import get_all_thread_stacks, stuck_detector
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_get_all_thread_stacks() -> None:
|
|
10
|
+
"""Test that get_all_thread_stacks returns stack traces for all threads."""
|
|
11
|
+
stacks = get_all_thread_stacks()
|
|
12
|
+
assert "MainThread" in stacks
|
|
13
|
+
assert "test_get_all_thread_stacks" in stacks # Current function should be in stack
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_stuck_detector_does_not_trigger_before_timeout() -> None:
|
|
17
|
+
"""Test that stuck detector doesn't trigger when operation completes quickly."""
|
|
18
|
+
with patch.object(
|
|
19
|
+
__import__("arroyo.utils.stuck_detector", fromlist=["logger"]).logger,
|
|
20
|
+
"warning",
|
|
21
|
+
) as mock_warn:
|
|
22
|
+
with stuck_detector(timeout_seconds=30):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
mock_warn.assert_not_called()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_stuck_detector_logs_after_timeout() -> None:
|
|
29
|
+
"""Test that stuck detector logs warning with stack traces when timeout exceeded."""
|
|
30
|
+
warning_logged = threading.Event()
|
|
31
|
+
|
|
32
|
+
def mock_warning(*args: object, **kwargs: object) -> None:
|
|
33
|
+
warning_logged.set()
|
|
34
|
+
|
|
35
|
+
with patch.object(
|
|
36
|
+
__import__("arroyo.utils.stuck_detector", fromlist=["logger"]).logger,
|
|
37
|
+
"warning",
|
|
38
|
+
side_effect=mock_warning,
|
|
39
|
+
) as mock_warn:
|
|
40
|
+
with stuck_detector(timeout_seconds=0.05):
|
|
41
|
+
warning_logged.wait(timeout=2)
|
|
42
|
+
|
|
43
|
+
mock_warn.assert_called_once()
|
|
44
|
+
call_args = mock_warn.call_args
|
|
45
|
+
assert "Operation stuck" in call_args[0][0]
|
|
46
|
+
assert "MainThread" in call_args[0][3]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|