-
Notifications
You must be signed in to change notification settings - Fork 581
fix(tracing): Break transaction / span circular references before garbage collection
#1184
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
Changes from all commits
114e251
dfdf9aa
4f5c1bf
577600b
9fe89d0
ef45c85
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -189,15 +189,15 @@ def processor(event, hint): | |
| assert len(json_dumps(event)) < max_bytes | ||
|
|
||
| # Some spans are discarded. | ||
| assert len(event["spans"]) == 999 | ||
| assert len(event["spans"]) == 1000 | ||
|
|
||
| # Some spans have their descriptions truncated. Because the test always | ||
| # generates the same amount of descriptions and truncation is deterministic, | ||
| # the number here should never change across test runs. | ||
| # | ||
| # Which exact span descriptions are truncated depends on the span durations | ||
| # of each SQL query and is non-deterministic. | ||
| assert len(event["_meta"]["spans"]) == 536 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this change? If this report changed because of spanrecorder changes I wonder if we should subtract 1 just to not change reporting format
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure why testing for I guess I'm not clear on the reason for this particular assertion, though. This is measuring how many spans get their description truncated, I take it? Why is that something we want to test against? |
||
| assert len(event["_meta"]["spans"]) == 537 | ||
|
|
||
| for i, span in enumerate(event["spans"]): | ||
| description = span["description"] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,18 @@ | ||
| import pytest | ||
| import gc | ||
| import uuid | ||
| import os | ||
|
|
||
| import sentry_sdk | ||
| from sentry_sdk import Hub, start_span, start_transaction | ||
| from sentry_sdk.tracing import Span, Transaction | ||
| from sentry_sdk.tracing_utils import has_tracestate_enabled | ||
|
|
||
| try: | ||
| from unittest import mock # python 3.3 and above | ||
| except ImportError: | ||
| import mock # python < 3.3 | ||
|
|
||
|
|
||
| def test_span_trimming(sentry_init, capture_events): | ||
| sentry_init(traces_sample_rate=1.0, _experiments={"max_spans": 3}) | ||
|
|
@@ -16,14 +25,12 @@ def test_span_trimming(sentry_init, capture_events): | |
|
|
||
| (event,) = events | ||
|
|
||
| # the transaction is its own first span (which counts for max_spans) but it | ||
| # doesn't show up in the span list in the event, so this is 1 less than our | ||
| # max_spans value | ||
| assert len(event["spans"]) == 2 | ||
| assert len(event["spans"]) == 3 | ||
|
|
||
| span1, span2 = event["spans"] | ||
| span1, span2, span3 = event["spans"] | ||
| assert span1["op"] == "foo0" | ||
| assert span2["op"] == "foo1" | ||
| assert span3["op"] == "foo2" | ||
|
|
||
|
|
||
| def test_transaction_naming(sentry_init, capture_events): | ||
|
|
@@ -152,6 +159,79 @@ def test_finds_non_orphan_span_on_scope(sentry_init): | |
| assert scope._span.op == "sniffing" | ||
|
|
||
|
|
||
| def test_circular_references(monkeypatch, sentry_init, request): | ||
| # TODO: We discovered while writing this test about transaction/span | ||
| # reference cycles that there's actually also a circular reference in | ||
| # `serializer.py`, between the functions `_serialize_node` and | ||
| # `_serialize_node_impl`, both of which are defined inside of the main | ||
| # `serialize` function, and each of which calls the other one. For now, in | ||
| # order to avoid having those ref cycles give us a false positive here, we | ||
| # can mock out `serialize`. In the long run, though, we should probably fix | ||
| # that. (Whenever we do work on fixing it, it may be useful to add | ||
| # | ||
| # gc.set_debug(gc.DEBUG_LEAK) | ||
| # request.addfinalizer(lambda: gc.set_debug(~gc.DEBUG_LEAK)) | ||
| # | ||
| # immediately after the initial collection below, so we can see what new | ||
| # objects the garbage collecter has to clean up once `transaction.finish` is | ||
| # called and the serializer runs.) | ||
| monkeypatch.setattr( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interesting way to deal with this! like it |
||
| sentry_sdk.client, | ||
| "serialize", | ||
| mock.Mock( | ||
| return_value=None, | ||
| ), | ||
| ) | ||
|
|
||
| # In certain versions of python, in some environments (specifically, python | ||
| # 3.4 when run in GH Actions), we run into a `ctypes` bug which creates | ||
| # circular references when `uuid4()` is called, as happens when we're | ||
| # generating event ids. Mocking it with an implementation which doesn't use | ||
| # the `ctypes` function lets us avoid having false positives when garbage | ||
| # collecting. See https://bugs.python.org/issue20519. | ||
| monkeypatch.setattr( | ||
| uuid, | ||
| "uuid4", | ||
| mock.Mock( | ||
| return_value=uuid.UUID(bytes=os.urandom(16)), | ||
| ), | ||
| ) | ||
|
|
||
| gc.disable() | ||
| request.addfinalizer(gc.enable) | ||
|
|
||
| sentry_init(traces_sample_rate=1.0) | ||
|
|
||
| # Make sure that we're starting with a clean slate before we start creating | ||
| # transaction/span reference cycles | ||
| gc.collect() | ||
|
|
||
| dogpark_transaction = start_transaction(name="dogpark") | ||
| sniffing_span = dogpark_transaction.start_child(op="sniffing") | ||
| wagging_span = dogpark_transaction.start_child(op="wagging") | ||
|
|
||
| # At some point, you have to stop sniffing - there are balls to chase! - so finish | ||
| # this span while the dogpark transaction is still open | ||
| sniffing_span.finish() | ||
|
|
||
| # The wagging, however, continues long past the dogpark, so that span will | ||
| # NOT finish before the transaction ends. (Doing it in this order proves | ||
| # that both finished and unfinished spans get their cycles broken.) | ||
| dogpark_transaction.finish() | ||
|
|
||
| # Eventually you gotta sleep... | ||
| wagging_span.finish() | ||
|
|
||
| # assuming there are no cycles by this point, these should all be able to go | ||
| # out of scope and get their memory deallocated without the garbage | ||
| # collector having anything to do | ||
| del sniffing_span | ||
| del wagging_span | ||
| del dogpark_transaction | ||
|
|
||
| assert gc.collect() == 0 | ||
|
|
||
|
|
||
| # TODO (kmclb) remove this test once tracestate is a real feature | ||
| @pytest.mark.parametrize("tracestate_enabled", [True, False, None]) | ||
| def test_has_tracestate_enabled(sentry_init, tracestate_enabled): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean the entire
Span._span_recorderfield should be moved to Transaction?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the long run, yes, I think so.