Skip to content

Commit c4f2056

Browse files
mergify[bot]brettlangdonmabdinurjd
authored
fix(tracing): ensure Context is serializable (backport #4432) (#4438)
* fix(tracing): ensure Context is serializable (#4432) ddtrace.context.Context object is not serializable, meaning it cannot be pickled/shared between processes. This breaks the example usage we have in our documentation for passing context through to other threads. e.g. Process(target=_target, args=(ctx, )) This fix added __getstate__ and __setstate__ methods to Context class to have pickle ignore the RLock which cannot be serialized. Co-authored-by: Munir Abdinur <[email protected]> Co-authored-by: Kyle Verhoog <[email protected]> (cherry picked from commit 96e6bca) # Conflicts: # ddtrace/context.py # tests/tracer/test_context.py * fix conflict * Update tests/tracer/test_context.py * merge conflict: remove traceparent from context * Update ddtrace/context.py Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: Munir Abdinur <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Julien Danjou <[email protected]>
1 parent 55878f0 commit c4f2056

File tree

5 files changed

+135
-1
lines changed

5 files changed

+135
-1
lines changed

ddtrace/context.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,21 @@
1313
from .internal.logger import get_logger
1414

1515

16-
if TYPE_CHECKING:
16+
if TYPE_CHECKING: # pragma: no cover
17+
from typing import Tuple
18+
1719
from .span import Span
1820
from .span import _MetaDictType
1921
from .span import _MetricDictType
2022

23+
_ContextState = Tuple[
24+
Optional[int], # trace_id
25+
Optional[int], # span_id
26+
_MetaDictType, # _meta
27+
_MetricDictType, # _metrics
28+
]
29+
30+
2131
log = get_logger(__name__)
2232

2333

@@ -63,6 +73,22 @@ def __init__(
6373
# https://github.com/DataDog/dd-trace-py/blob/a1932e8ddb704d259ea8a3188d30bf542f59fd8d/ddtrace/tracer.py#L489-L508
6474
self._lock = threading.RLock()
6575

76+
def __getstate__(self):
77+
# type: () -> _ContextState
78+
return (
79+
self.trace_id,
80+
self.span_id,
81+
self._meta,
82+
self._metrics,
83+
# Note: self._lock is not serializable
84+
)
85+
86+
def __setstate__(self, state):
87+
# type: (_ContextState) -> None
88+
self.trace_id, self.span_id, self._meta, self._metrics = state
89+
# We cannot serialize and lock, so we must recreate it unless we already have one
90+
self._lock = threading.RLock()
91+
6692
def _with_span(self, span):
6793
# type: (Span) -> Context
6894
"""Return a shallow copy of the context with the given span."""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
tracing: make ``ddtrace.context.Context`` serializable which fixes distributed tracing across processes.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pytest
2+
3+
from tests.utils import snapshot
4+
5+
from .test_integration import AGENT_VERSION
6+
7+
8+
pytestmark = pytest.mark.skipif(AGENT_VERSION != "testagent", reason="Tests only compatible with a testagent")
9+
10+
11+
@snapshot()
12+
def test_context_multiprocess(run_python_code_in_subprocess):
13+
# Testing example from our docs:
14+
# https://ddtrace.readthedocs.io/en/stable/advanced_usage.html#tracing-across-processes
15+
code = """
16+
from multiprocessing import Process
17+
import time
18+
19+
from ddtrace import tracer
20+
21+
22+
def _target(ctx):
23+
tracer.context_provider.activate(ctx)
24+
with tracer.trace("proc"):
25+
time.sleep(0.1)
26+
tracer.shutdown()
27+
28+
29+
def main():
30+
with tracer.trace("work"):
31+
proc = Process(target=_target, args=(tracer.current_trace_context(), ))
32+
proc.start()
33+
time.sleep(0.25)
34+
proc.join()
35+
36+
37+
if __name__ == "__main__":
38+
main()
39+
"""
40+
41+
stdout, stderr, status, _ = run_python_code_in_subprocess(code=code)
42+
assert status == 0, (stdout, stderr)
43+
assert stdout == b"", stderr
44+
assert stderr == b"", stdout
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[[
2+
{
3+
"name": "work",
4+
"service": null,
5+
"resource": "work",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"meta": {
10+
"_dd.p.dm": "-0",
11+
"runtime-id": "f706b29e0e8049178c7f1e0ac8d01ab7"
12+
},
13+
"metrics": {
14+
"_dd.agent_psr": 1.0,
15+
"_dd.top_level": 1,
16+
"_dd.tracer_kr": 1.0,
17+
"_sampling_priority_v1": 1,
18+
"system.pid": 25193
19+
},
20+
"duration": 259538000,
21+
"start": 1667237294717521000
22+
},
23+
{
24+
"name": "proc",
25+
"service": null,
26+
"resource": "proc",
27+
"trace_id": 0,
28+
"span_id": 2,
29+
"parent_id": 1,
30+
"meta": {
31+
"_dd.p.dm": "-0",
32+
"runtime-id": "38ca0ac0547f4097b2e030ebff1064c7"
33+
},
34+
"metrics": {
35+
"_dd.top_level": 1,
36+
"_dd.tracer_kr": 1.0,
37+
"_sampling_priority_v1": 1,
38+
"system.pid": 25194
39+
},
40+
"duration": 100317000,
41+
"start": 1667237294727339000
42+
}]]

tests/tracer/test_context.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pickle
2+
13
import pytest
24

35
from ddtrace.context import Context
@@ -48,3 +50,19 @@ def test_eq(ctx1, ctx2):
4850
)
4951
def test_not_eq(ctx1, ctx2):
5052
assert ctx1 != ctx2
53+
54+
55+
@pytest.mark.parametrize(
56+
"context",
57+
[
58+
Context(),
59+
Context(trace_id=123, span_id=321),
60+
Context(trace_id=123, span_id=321, dd_origin="synthetics", sampling_priority=2),
61+
Context(trace_id=123, span_id=321, meta={"meta": "value"}, metrics={"metric": 4.556}),
62+
],
63+
)
64+
def test_context_serializable(context):
65+
# type: (Context) -> None
66+
state = pickle.dumps(context)
67+
restored = pickle.loads(state)
68+
assert context == restored

0 commit comments

Comments
 (0)