Skip to content

Commit 40111cb

Browse files
fix(tracing): ensure Context is serializable (#4432) (#4439)
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) Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent e9ae959 commit 40111cb

File tree

5 files changed

+134
-0
lines changed

5 files changed

+134
-0
lines changed

ddtrace/context.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,20 @@
1414

1515

1616
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
@@ -75,3 +77,19 @@ def validate_traceparent(context, sampled_expected):
7577
span = Span("span_c")
7678
span.context.sampling_priority = 1
7779
validate_traceparent(span.context, "01")
80+
81+
82+
@pytest.mark.parametrize(
83+
"context",
84+
[
85+
Context(),
86+
Context(trace_id=123, span_id=321),
87+
Context(trace_id=123, span_id=321, dd_origin="synthetics", sampling_priority=2),
88+
Context(trace_id=123, span_id=321, meta={"meta": "value"}, metrics={"metric": 4.556}),
89+
],
90+
)
91+
def test_context_serializable(context):
92+
# type: (Context) -> None
93+
state = pickle.dumps(context)
94+
restored = pickle.loads(state)
95+
assert context == restored

0 commit comments

Comments
 (0)