Skip to content

Commit d1f9beb

Browse files
authored
Hook up Rust Enhancers to assemble_stacktrace_component (#66587)
The code is gated behind a random rollout option and runs in comparison mode at first. A second rollout option will be added to fully use the results from Rust code later on.
1 parent 66f24c1 commit d1f9beb

File tree

8 files changed

+137
-25
lines changed

8 files changed

+137
-25
lines changed

requirements-base.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ rfc3986-validator>=0.1.1
6363
# [end] jsonschema format validators
6464
sentry-arroyo>=2.16.2
6565
sentry-kafka-schemas>=0.1.61
66-
sentry-ophio==0.1.5
66+
sentry-ophio==0.2.3
6767
sentry-redis-tools>=0.1.7
6868
sentry-relay>=0.8.49
6969
sentry-sdk>=1.40.6

requirements-dev-frozen.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ sentry-devenv==1.2.3
178178
sentry-forked-django-stubs==4.2.7.post3
179179
sentry-forked-djangorestframework-stubs==3.14.5.post1
180180
sentry-kafka-schemas==0.1.61
181-
sentry-ophio==0.1.5
181+
sentry-ophio==0.2.3
182182
sentry-redis-tools==0.1.7
183183
sentry-relay==0.8.49
184184
sentry-sdk==1.40.6

requirements-frozen.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ rsa==4.8
120120
s3transfer==0.10.0
121121
sentry-arroyo==2.16.2
122122
sentry-kafka-schemas==0.1.61
123-
sentry-ophio==0.1.5
123+
sentry-ophio==0.2.3
124124
sentry-redis-tools==0.1.7
125125
sentry-relay==0.8.49
126126
sentry-sdk==1.40.6

src/sentry/grouping/enhancer/__init__.py

Lines changed: 118 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
from parsimonious.grammar import Grammar
1717
from parsimonious.nodes import NodeVisitor
1818
from sentry_ophio.enhancers import Cache as RustCache
19+
from sentry_ophio.enhancers import Component as RustComponent
1920
from sentry_ophio.enhancers import Enhancements as RustEnhancements
2021

2122
from sentry import projectoptions
23+
from sentry.features.rollout import in_random_rollout
2224
from sentry.grouping.component import GroupingComponent
2325
from sentry.stacktraces.functions import set_in_app
2426
from sentry.utils import metrics
@@ -96,7 +98,7 @@
9698

9799
class StacktraceState:
98100
def __init__(self):
99-
self.vars = {"max-frames": 0, "min-frames": 0, "invert-stacktrace": 0}
101+
self.vars = {"max-frames": 0, "min-frames": 0, "invert-stacktrace": False}
100102
self.setters = {}
101103

102104
def set(self, var, value, rule=None):
@@ -171,7 +173,25 @@ def parse_rust_enhancements(
171173
return rust_enhancements
172174

173175

176+
RustAssembleResult = tuple[bool | None, str | None, bool, list[RustComponent]]
174177
RustEnhancedFrames = list[tuple[str | None, bool | None]]
178+
RustExceptionData = dict[str, bytes | None]
179+
180+
181+
def make_rust_exception_data(
182+
exception_data: dict[str, Any],
183+
) -> RustExceptionData:
184+
e = exception_data or {}
185+
e = {
186+
"ty": e.get("type"),
187+
"value": e.get("value"),
188+
"mechanism": get_path(e, "mechanism", "type"),
189+
}
190+
for key in e.keys():
191+
value = e[key]
192+
if isinstance(value, str):
193+
e[key] = value.encode("utf-8")
194+
return e
175195

176196

177197
def apply_rust_enhancements(
@@ -188,19 +208,8 @@ def apply_rust_enhancements(
188208
return None
189209

190210
try:
191-
e = exception_data or {}
192-
e = {
193-
"ty": e.get("type"),
194-
"value": e.get("value"),
195-
"mechanism": get_path(e, "mechanism", "type"),
196-
}
197-
for key in e.keys():
198-
value = e[key]
199-
if isinstance(value, str):
200-
e[key] = value.encode("utf-8")
201-
202211
rust_enhanced_frames = rust_enhancements.apply_modifications_to_frames(
203-
iter(match_frames), e
212+
match_frames, make_rust_exception_data(exception_data)
204213
)
205214
metrics.incr("rust_enhancements.modifications_run")
206215
return rust_enhanced_frames
@@ -228,6 +237,85 @@ def compare_rust_enhancers(
228237
sentry_sdk.capture_message("Rust Enhancements mismatch")
229238

230239

240+
def assemble_rust_components(
241+
rust_enhancements: RustEnhancements | None,
242+
match_frames: list[dict[str, bytes]],
243+
exception_data: dict[str, Any],
244+
components: list[GroupingComponent],
245+
) -> RustAssembleResult | None:
246+
"""
247+
If `RustEnhancements` were successfully parsed and usage is enabled,
248+
this will update all the frame `components` contributions.
249+
250+
This primarily means updating the `contributes`, `hint` as well as other attributes
251+
of each frames `GroupingComponent`.
252+
Instead of modifying the input `components` directly, the results are returned
253+
as a list of `RustComponent`.
254+
"""
255+
if not rust_enhancements:
256+
return None
257+
258+
try:
259+
rust_components = [
260+
RustComponent(
261+
is_prefix_frame=c.is_prefix_frame or False,
262+
is_sentinel_frame=c.is_sentinel_frame or False,
263+
contributes=c.contributes,
264+
)
265+
for c in components
266+
]
267+
268+
rust_results = rust_enhancements.assemble_stacktrace_component(
269+
match_frames, make_rust_exception_data(exception_data), rust_components
270+
)
271+
272+
return (
273+
rust_results.contributes,
274+
rust_results.hint,
275+
rust_results.invert_stacktrace,
276+
rust_components,
277+
)
278+
except Exception:
279+
logger.exception("failed running Rust Enhancements component contributions")
280+
return None
281+
282+
283+
def compare_rust_components(
284+
component: GroupingComponent,
285+
invert_stacktrace: bool,
286+
rust_results: RustAssembleResult | None,
287+
frames: Sequence[dict[str, Any]],
288+
):
289+
"""
290+
Compares the results of `rust_results` with the component modifications
291+
applied by Python code directly to `components`.
292+
293+
This will log an internal error on every mismatch.
294+
"""
295+
if not rust_results:
296+
return
297+
298+
contributes, hint, invert, rust_components_ = rust_results
299+
300+
python_components = [
301+
(c.contributes, c.is_prefix_frame, c.is_sentinel_frame) for c in component.values
302+
]
303+
rust_components = [
304+
(c.contributes, c.is_prefix_frame, c.is_sentinel_frame) for c in rust_components_
305+
]
306+
307+
python_res = (component.contributes, component.hint, invert_stacktrace, python_components)
308+
rust_res = (contributes, hint, invert, rust_components)
309+
310+
if python_res != rust_res:
311+
with sentry_sdk.push_scope() as scope:
312+
scope.set_extra("python_res", python_res)
313+
scope.set_extra("rust_res", rust_res)
314+
scope.set_extra("frames", frames)
315+
316+
sentry_sdk.capture_message("Rust Enhancements mismatch")
317+
318+
231319
class Enhancements:
232320
# NOTE: You must add a version to ``VERSIONS`` any time attributes are added
233321
# to this class, s.t. no enhancements lacking these attributes are loaded
@@ -314,12 +402,11 @@ def apply_modifications_to_frame(
314402

315403
compare_rust_enhancers(frames, rust_enhanced_frames)
316404

317-
def update_frame_components_contributions(self, components, frames, platform, exception_data):
318-
in_memory_cache: dict[str, str] = {}
319-
320-
match_frames = [create_match_frame(frame, platform) for frame in frames]
321-
405+
def update_frame_components_contributions(
406+
self, components, frames, match_frames, platform, exception_data
407+
):
322408
stacktrace_state = StacktraceState()
409+
in_memory_cache: dict[str, str] = {}
323410
# Apply direct frame actions and update the stack state alongside
324411
for rule in self._updater_rules:
325412
for idx, action in rule.get_matching_frame_actions(
@@ -360,10 +447,18 @@ def assemble_stacktrace_component(
360447
Internally this invokes the `update_frame_components_contributions` method
361448
but also handles cases where the entire stacktrace should be discarded.
362449
"""
450+
match_frames = [create_match_frame(frame, platform) for frame in frames]
451+
452+
rust_results = None
453+
if in_random_rollout("grouping.rust_enhancers.compare_components"):
454+
rust_results = assemble_rust_components(
455+
self.rust_enhancements, match_frames, exception_data, components
456+
)
457+
363458
hint = None
364459
contributes = None
365460
stacktrace_state = self.update_frame_components_contributions(
366-
components, frames, platform, exception_data
461+
components, frames, match_frames, platform, exception_data
367462
)
368463

369464
min_frames = stacktrace_state.get("min-frames")
@@ -378,12 +473,14 @@ def assemble_stacktrace_component(
378473
hint = stacktrace_state.add_to_hint(hint, var="min-frames")
379474
contributes = False
380475

381-
inverted_hierarchy = stacktrace_state.get("invert-stacktrace")
476+
invert_stacktrace = stacktrace_state.get("invert-stacktrace")
382477
component = GroupingComponent(
383478
id="stacktrace", values=components, hint=hint, contributes=contributes, **kw
384479
)
385480

386-
return component, inverted_hierarchy
481+
compare_rust_components(component, invert_stacktrace, rust_results, frames)
482+
483+
return component, invert_stacktrace
387484

388485
def as_dict(self, with_rules=False):
389486
rv = {

src/sentry/options/defaults.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,6 +2066,13 @@
20662066
default=0.0,
20672067
flags=FLAG_AUTOMATOR_MODIFIABLE,
20682068
)
2069+
# Rate at which to run the Rust implementation of `assemble_stacktrace_component`
2070+
# and compare the results
2071+
register(
2072+
"grouping.rust_enhancers.compare_components",
2073+
default=0.0,
2074+
flags=FLAG_AUTOMATOR_MODIFIABLE,
2075+
)
20692076
# Rate to move from outbox based webhook delivery to webhookpayload.
20702077
register(
20712078
"hybridcloud.webhookpayload.rollout",

tests/sentry/grouping/test_enhancer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sentry.grouping.enhancer import Enhancements
99
from sentry.grouping.enhancer.exceptions import InvalidEnhancerConfig
1010
from sentry.grouping.enhancer.matchers import create_match_frame
11+
from sentry.testutils.helpers.options import override_options
1112
from sentry.testutils.pytest.fixtures import django_db_all
1213

1314

@@ -30,7 +31,6 @@ def dump_obj(obj):
3031

3132

3233
@pytest.mark.parametrize("version", [1, 2])
33-
@django_db_all
3434
def test_basic_parsing(insta_snapshot, version):
3535
enhancement = Enhancements.from_config_string(
3636
"""
@@ -463,6 +463,8 @@ def test_range_matching_direct():
463463

464464
@pytest.mark.parametrize("action", ["+", "-"])
465465
@pytest.mark.parametrize("type", ["prefix", "sentinel"])
466+
@django_db_all # because of `options` usage
467+
@override_options({"grouping.rust_enhancers.compare_components": 1.0})
466468
def test_sentinel_and_prefix(action, type):
467469
enhancements = Enhancements.from_config_string(f"function:foo {action}{type}")
468470

tests/sentry/grouping/test_fingerprinting.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from sentry.grouping.api import get_default_grouping_config_dict
44
from sentry.grouping.fingerprinting import FingerprintingRules, InvalidFingerprintingConfig
5+
from sentry.testutils.pytest.fixtures import django_db_all
56
from tests.sentry.grouping import with_fingerprint_input
67

78
GROUPING_CONFIG = get_default_grouping_config_dict()
@@ -158,6 +159,7 @@ def test_discover_field_parsing(insta_snapshot):
158159

159160

160161
@with_fingerprint_input("input")
162+
@django_db_all # because of `options` usage
161163
def test_event_hash_variant(insta_snapshot, input):
162164
config, evt = input.create_event()
163165

tests/sentry/grouping/test_variants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
)
1111
from sentry.grouping.component import GroupingComponent
1212
from sentry.grouping.strategies.configurations import CONFIGURATIONS
13+
from sentry.testutils.helpers.options import override_options
14+
from sentry.testutils.pytest.fixtures import django_db_all
1315
from sentry.utils import json
1416
from tests.sentry.grouping import with_grouping_input
1517

@@ -59,6 +61,8 @@ def _dump_component(component, indent):
5961

6062
@with_grouping_input("grouping_input")
6163
@pytest.mark.parametrize("config_name", CONFIGURATIONS.keys(), ids=lambda x: x.replace("-", "_"))
64+
@django_db_all # because of `options` usage
65+
@override_options({"grouping.rust_enhancers.compare_components": 1.0})
6266
def test_event_hash_variant(config_name, grouping_input, insta_snapshot, log):
6367
grouping_config = get_default_grouping_config_dict(config_name)
6468
evt = grouping_input.create_event(grouping_config)

0 commit comments

Comments
 (0)