Repository navigation
fix(tracer): isolate MicroVM identity refresh - #19820
litianningdatadog wants to merge 1 commit into
Conversation
❌ ErrorsYour PR has failed checks. Please review the issues below and take necessary action before merging. 🚦 2 Pipeline jobs failed
ℹ️ InfoNo other issues found (see more)🧪 All tests passed 🚧 1 test that failed was ignored due to quarantine Useful? React with 👍 / 👎 This comment will be updated automatically if new data arrives.🔗 Commit SHA: d88408b | Docs | View more details | Give us feedback! |
BenchmarksBenchmark execution time: 2026-10-16 14:28:30 Comparing candidate commit d88408b in PR branch Found 0 performance improvements and 4 performance regressions! Performance is the same for 352 metrics, 9 unstable metrics, 4 known flaky benchmarks, 4 flaky benchmarks without significant changes.
|
d59e112 to
16a5332
Compare
3c0ea31 to
eabf5ec
Compare
16a5332 to
8dd7e8e
Compare
eabf5ec to
5853b4a
Compare
cde3045 to
a0e3c42
Compare
5853b4a to
7afb3c1
Compare
Codeowners resolved asResolved from the full PR diff against |
Circular import analysis
|
Dependency direction analysis
|
There was a problem hiding this comment.
Pull request overview
This PR updates the native trace writer so that when runtime identity is refreshed (e.g., AWS Lambda MicroVM /run refresh), the native exporter is rebuilt to pick up the new runtime id captured at exporter construction time.
Changes:
- Wire
NativeWriterto runtime-id change notifications and rebuild the exporter on identity refresh. - Add tests to verify the exporter is rebuilt (without recreating the writer/buffer) and that the wiring works end-to-end via
runtime.refresh_identity().
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
ddtrace/internal/writer/writer.py |
Subscribes to runtime-id change events and rebuilds the native exporter on identity refresh. |
tests/tracer/test_writer.py |
Adds unit + subprocess tests ensuring identity refresh rebuilds the exporter without replacing the writer’s client/buffer state. |
Suppressed comments (2)
ddtrace/internal/writer/writer.py:945
- _on_identity_refresh can raise if _create_exporter() fails (e.g., native builder/telemetry initialization error). Because runtime.refresh_identity() iterates callbacks without exception handling, a failure here will propagate out of refresh_identity() and can also prevent other runtime-id listeners from running. It’s safer to only swap the exporter after successfully creating the replacement, and to swallow/log failures from the rebuild path.
old_exporter = self._exporter
self._exporter = self._create_exporter()
try:
old_exporter.shutdown(3_000_000_000)
except Exception:
ddtrace/internal/writer/writer.py:944
- Exporter rebuild/swap is not synchronized with payload sending. NativeWriter.flush_queue() can run on the periodic thread and can also be called from other threads, while _on_identity_refresh swaps self._exporter and shuts down the old exporter. Without a lock, this introduces a race where a concurrent _exporter.send() can observe a half-transition or have its exporter shutdown mid-send.
Consider adding a dedicated lock (similar to HTTPWriter._conn_lck) and holding it in _send_payload(), _on_identity_refresh(), set_test_session_token(), and any other place that swaps/shuts down the exporter.
old_exporter = self._exporter
self._exporter = self._create_exporter()
try:
old_exporter.shutdown(3_000_000_000)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b29531ab07
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb487b59fa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b585e1b387
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94d7001195
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 701fe64544
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a74cd2e33
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6b50c3b38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94a8535645
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 20964893b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 40629b6022
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f79af32e93
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1717155a9a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Two of the most critical issues are resource leaks during refresh: the replaced CIVisibilityWriter's periodic service is never stopped (retaining its thread, encoder, and connection), and discarded TraceExporter native background workers are neither cancelled nor unregistered. Additionally, several generation-check race conditions exist in tracer.py where span identity is validated once but not re-checked across finish processors, at root-span return time, or at remote-context activation, allowing stale spans or contexts to corrupt post-refresh state.
🤖 Bits Code Review · Commit 63940cd
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bbb36dab92
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fdd54edfe2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
The two most critical issues are a race condition in writer.py where a discarded writer's response callback can overwrite the replacement writer's sampling rates (findings 1 & 3), and a deadlock risk where discarded writers remain subscribed to telemetry worker-change callbacks, blocking span operations while holding the aggregator lock. Additional distinct issues include missing writer-recreation retry logic on MicroVM refresh failure and stale parent-context generation tracking across remote.copy() owners.
🤖 Bits Code Review · Commit 9c5b471
|
/code blockers |
|
View all feedbacks in Devflow UI.
Checking merge blockers for #19820...
Detected 8 merge blocker(s) to address: 🔴 Blocking
🟠 Pending
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 436c374e51
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| except BaseException: | ||
| # /run is not repeated, so a failed rebuild would leave the discarded writer installed. | ||
| # Retry it on the next span start, like a post-fork rebuild. | ||
| self._post_fork_writer_pending = True | ||
| raise |
There was a problem hiding this comment.
Isolate native rebuild panics from the /run request
When exporter reconstruction raises a PyO3 panic (a BaseException, which this handler explicitly anticipates), re-raising it bypasses the Exception-only guard in _notify_runtime_identity_refresh_callbacks; the event hub also always propagates BaseException. Because the callback runs inline during WSGI/ASGI request-start dispatch, the MicroVM /run invocation is aborted instead of proceeding to the first span, where the newly set pending flag could retry reconstruction. Keep the pending marker, but isolate recognized native panics on the normal request-hook path while preserving explicit raise_on_error behavior.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in e97992f. The rebuild still marks itself pending first, but a Rust panic (is_panic_exception) is now logged instead of re-raised, so it no longer gets past the refresh dispatcher or the event hub into /run, and the first span retries the rebuild. KeyboardInterrupt, SystemExit and ordinary exceptions still propagate. test_tracer_identity_refresh_contains_native_panic_from_writer_rebuild drives this through refresh_identity().
One difference from your suggestion: the panic is contained inside the tracer callback, so a direct refresh_identity(raise_on_error=True) caller won't see it either. It's still logged and the rebuild is still retried. Nothing on the /run path calls with raise_on_error=True, so I kept the callback simple instead of threading that flag through.
|
/code blockers |
|
View all feedbacks in Devflow UI.
Checking merge blockers for #19820...
Detected 5 merge blocker(s) to address: 🔴 Blocking
🟠 Pending
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc81a06965
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| identity_generation = parent_generation | ||
| else: | ||
| _set_runtime_identity_generation(child_of, identity_generation) | ||
| span._set_ctx_item(_RUNTIME_IDENTITY_GENERATION_KEY, identity_generation) |
There was a problem hiding this comment.
Record contexts created while tracing is disabled
When a MicroVM snapshot contains a span created while tracer.enabled is false, this stamps only the span: the conditional processor dispatch below skips SpanAggregator.on_span_start(), so the span's Context never enters the generation side table. If tracing is enabled after /run and the application reuses span.context as child_of, _get_runtime_identity_generation() returns None, line 686 promotes it to the current generation, and a child of the pre-snapshot trace can be exported. Fresh evidence beyond the earlier untracked-context discussion is that this is a tracer-created context that becomes untracked specifically on the disabled-tracing path; record its generation here regardless of whether processors run.
Useful? React with 👍 / 👎.
On a MicroVM identity refresh, drop buffered traces and the old native exporter without flushing them, and rebuild the writer for the new runtime ID, so traces from before the refresh are never sent or used as parents afterwards. - SpanAggregator publishes one (generation, runtime ID) value. Span start and finish read it without a lock; the write re-checks it under the aggregator lock as the single commit point. - Root spans take runtime-id from that value, so a span started after the ID rotated but before the tracer refreshed never carries the new ID to the old writer. - SpanAggregator uses forksafe.RLock only in MicroVM; others keep RLock. - drop() avoids shutdown()'s final stats flush but does not stop the exporter's native workers, so the replaced exporter's stats worker can still send pre-refresh stats later. - The refresh and shutdown() serialize on _post_fork_lock, so a writer built by a refresh is always the one shutdown stops, and a refresh that starts after shutdown builds nothing. The lock is reentrant because the exit-signal handler can run shutdown() on the refreshing thread; if that handler returns, the refresh stops the writer it just installed. - In a MicroVM, shutdown(timeout) waits at most timeout for that lock, because a refresh on another thread can hold it while waiting for an aggregator or writer lock held by the thread the exit signal interrupted. A refresh, or the span-start retry of a failed one, still running after the wait stops the writer it installs. - The refresh stores the tracer metadata whether or not the writer rebuild succeeded, and a Rust panic from the rebuild is logged instead of aborting the /run request; the rebuild is retried on the next span. That retry also logs an exception or Rust panic from the rebuild in MicroVM mode and keeps the retry pending, instead of raising it from the app's span start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stacked PRs:
Description
An explicit AWS Lambda MicroVM identity refresh changes the runtime ID, but the tracer's aggregation state, writer buffers, native exporter and active request context can still belong to the previous logical runtime.
This change subscribes the global tracer to MicroVM identity-refresh notifications. During a refresh it:
threading.RLock, so after a fork it can still be held by a thread that no longer exists; the old buffer is abandoned along with the old writer.TraceExporter.drop()instead of the flushingshutdown().drop()leaves the exporter's native workers running; see Risks.send(), whose retries take about 21 s against an unreachable agent. The send gate is rechecked under the exporter lock instead, so no send starts after a refresh closes it. In sync mode (AWS Lambda with the agent extension), a span finish only buffers the trace under the writer and span-aggregator locks;Tracer._on_span_finishsends it on the same thread after releasing the span-aggregator lock.tracer.flush()thread for a sync writer, which never starts a thread.recreate()doesn't join the writer thread or shut down an exporter whose drop is still pending. A discarded writer'son_shutdown()and__del__drop the exporter instead of shutting it down.rate_by_servicecan't overwrite rates the replacement writer received. A late 404/415 doesn't rebuild the exporter, and if the refresh wins while_downgrade()is building a replacement, the replacement is dropped instead of installed.Tracer.configure(), agentless writer swaps, post-fork writer recreation) with the refresh. Otherwise a concurrent swap could install a writer whose exporter captured the previous runtime ID.start_span()records its context's generation, sochild_of=span.contextis rejected too.trace.span_finishlisteners and registered span processors run./run, including a remote context. The/runhook fires before distributed-tracing header extraction, so a remote context active at refresh time belongs to the snapshot; extraction then activates the new request's incoming context.shutdown()on_post_fork_lock.shutdown()sets_shutting_downunder that lock before stopping anything, so it waits for a refresh in flight and stops the writer that refresh installs, and a refresh that starts after shutdown builds nothing. The lock is reentrant, because the exit-signal handler can runshutdown()on the thread that is refreshing; if that handler returns, the refresh sees_shutting_downafter the rebuild and stops the writer it just installed, shutting down the exporter of aNativeWriterthat never started.shutdown(timeout)waits at mosttimeout(clamped at 0) for_post_fork_lock, then sets_shutting_downeither way. Without the bound, an exit signal could deadlock: a refresh on another thread holds_post_fork_lockwhile waiting for the span-aggregator lock or the old writer's_writer_lock, which the thread running the signal handler holds. A refresh still running after the wait stops the writer it installs, through the same_shutting_downcheck.shutdown()without a timeout still waits as before. The span-start retry of a failed refresh (_ensure_post_fork_writer()) does the same in a MicroVM: it builds nothing once_shutting_downis set, and stops a writer it installs after that wait. Outside a MicroVM the post-fork rebuild is unchanged.finallyaround the rebuild, so a failed rebuild still records the new runtime ID. A Rust panic from the rebuild is logged instead of re-raised, because the refresh dispatcher only containsExceptionand the panic would otherwise abort the/runrequest. The span-start retry contains its own failures in MicroVM mode: anExceptionor Rust panic from the rebuild is logged (rate-limited) and the retry stays pending, so it doesn't fail the app's span start.KeyboardInterruptandSystemExitstill propagate, and outside MicroVM mode a failed post-fork rebuild still raises, as onmain.The identity-transition locks and callbacks are enabled only in MicroVM mode. Outside MicroVM mode the one change is that
shutdown()takes_post_fork_lockonce, so a shutdown racing a post-fork writer rebuild on another thread waits for it and then stops the rebuilt writer instead of leaving it running. The shutdown timeout doesn't bound that wait outside MicroVM mode.The stale-buffer reset previously proposed in #20089 is included because it is part of the tracer and writer identity-refresh behavior.
Reference
Testing
Tracer.configure()racing with the refresh can't leave a writer built for the pre-refresh runtime ID.test_tracer_microvm_identity_refresh_records_generation_of_root_rejected_by_start_spanforces a refresh between the identity read and the generation check instart_span(), then checks thatchild_of=span.contextis rejected.test_tracer_microvm_identity_refresh_after_shutdown_does_not_recreate_writerchecks that a refresh afterTracer.shutdown()builds no writer.test_tracer_shutdown_waits_for_in_flight_identity_refresh,test_tracer_identity_refresh_during_shutdown_does_not_recreate_writerandtest_tracer_shutdown_on_refreshing_thread_stops_replacement_writercover shutdown during a refresh, a refresh during shutdown, and shutdown run on the refreshing thread (no deadlock; the replacement writer is stopped, or has its exporter shut down if it never started).test_tracer_shutdown_waits_for_in_flight_post_fork_writer_recreationcovers the same wait for a post-fork rebuild. Each fails without the change.test_tracer_signal_shutdown_does_not_deadlock_with_refresh_waiting_on_writer_lockruns shutdown from a thread that holds the writer lock a refresh is waiting on (the cross-thread exit-signal case); shutdown returns after its timeout and the refresh then stops the writer it installed.test_tracer_timed_out_shutdown_stops_writer_from_retried_refresh_rebuildcovers a timed-out shutdown during the span-start retry of a failed refresh, andtest_tracer_post_fork_rebuild_ignores_shutdown_flag_outside_microvmchecks that the post-fork rebuild is unchanged outside MicroVM mode.test_tracer_microvm_shutdown_accepts_negative_timeoutcovers the clamp, andtest_tracer_shutdown_timeout_does_not_bound_post_fork_wait_outside_microvmchecks that the timeout doesn't cut the post-fork wait short outside MicroVM mode.test_tracer_microvm_identity_refresh_schedules_rebuild_after_recreate_failureandtest_tracer_identity_refresh_contains_native_panic_from_writer_rebuild: a failed rebuild is left pending and still stores the metadata;RuntimeErrorandKeyboardInterruptpropagate, while a Rust panic doesn't escaperefresh_identity().test_tracer_microvm_identity_refresh_retries_failed_writer_recreation_on_next_spanalso checks that the stored metadata carries the refreshed runtime ID.test_tracer_microvm_failed_rebuild_retry_is_contained_in_start_spanchecks that, in MicroVM mode, a span-start retry that fails again with aRuntimeErroror Rust panic doesn't raise and stays pending, andtest_tracer_post_fork_rebuild_failure_still_raises_outside_microvmthat the failure still raises outside it.write()nor the refresh, that the send gate is rechecked under the exporter lock, that a replaced writer's thread drops its exporter without callingshutdown(), thatstop()skips the join only for a replaced MicroVM writer, and that_downgrade()is skipped after a refresh.recreate()on a stopped sync writer doesn't wait for an in-flight send, that aflush_queue()sender drops the replaced exporter once its send ends, and that the exporter is dropped only once when several paths race to drop it.test_native_writer_send_finishing_after_refresh_skips_response_callbackandtest_native_writer_del_drops_exporter_when_refresh_lost_lock_to_non_sendercover the late response and the__del__drop.send(), a background span finish, an unrelated span start andrefresh_identity()each complete in under 1 s.refresh_identity()each complete in under 1 s, and the finishing thread then drops the replaced exporter without callingshutdown()./runrefresh took 0.00 s, and the agent received only the in-flight payload's 6 retry attempts.scripts/run-tests -r --venv ed437ab -- tests/tracer/test_writer.py tests/tracer/runtime/test_runtime_id.py tests/tracer/test_processors.py tests/tracer/test_tracer.py: 393 passed, 13 skipped, 1 xpassed. Pass-rafter rebasing onto a newermain; a stale native build fails 18 of these tests with'Span' object has no attribute '_override_sampling_decision'.test_trace_top_level_span_processor_partial_flushingandtest_tracer_shutdown_timeoutneed a trace agent onlocalhost:8126, which CI provides; rundocker compose up -d ddagentfirst when testing locally.tracervenv, nottracer-python-optimize, for these files:PYTHONOPTIMIZE=1strips the asserts inside@pytest.mark.subprocesstests, so failures there report as passes.scripts/run-tests --venv 128b106 -- tests/tracer/test_tracer.py tests/tracer/test_writer.py tests/tracer/runtime/test_runtime_id.py: 351 passed, 13 skipped, 1 xpassed.scripts/run-tests --venv 128b106 -- tests/tracer/runtime/test_runtime_id.py tests/tracer/test_tracer.py tests/tracer/test_writer.py -k "test_runtime_id or test_writer or shutdown or post_fork": 242 passed, 10 skipped, 1 xpassed.scripts/lint fmtandscripts/lint typingpassed on the changed files.Risks
Pre-refresh trace data is intentionally discarded during a MicroVM identity refresh so it can't be exported under a stale runtime ID. The transition-specific locking is limited to MicroVM identity-refresh state.
Known gap:
TraceExporter.drop()doesn't stop the workers the replaced exporter registered on the shared native runtime, and the libdatadog version this PR pins (v43.0.2) has no discard-only stop. After a refresh, the old exporter's client-side stats worker keeps running. It sends the pre-refresh stat buckets on its next flush interval (10 s by default) and the rest at process exit, under the snapshotted runtime ID, so every MicroVM restored from the same image reports the same buckets. The old/infofetcher also keeps polling the agent, as do the dogstatsd worker (health metrics) and the exporter-owned telemetry worker (only used without a shared telemetry worker) when enabled. There is at most one extra set of workers per process, because/runrefreshes identity once. Client-side stats are on by default only on Python 3.14+ or in agentless mode;DD_TRACE_STATS_COMPUTATION_ENABLED=falseavoids the duplicate stats, except for agentless stats and OTLP trace metrics, which always use the native concentrator. Closing the gap needs a libdatadog exporter shutdown that discards without flushing (DataDog/libdatadog#2478). Calling the normalshutdown()instead, even from a background thread, would send the same stale stats.Contexts this process never tracked are treated as current. That includes a remote or hand-built
Contextthat survived the snapshot without being activated, used as a parent, or copied into a context that was used as a parent, and a snapshot-inheritedContextstill active in a thread or asyncio task other than the one handling/run. Spans parented to one are still created after the refresh, so they carry the new runtime ID and go through the new writer. Rejecting untracked contexts instead would drop legitimate post-refresh traffic that never passes throughactivate_distributed_headers(SQS/Kafka/Pub/Sub consumers, OTel remote parents, manual propagation).The finish-time generation check before
trace.span_finishdoesn't take a lock. A span invalidated by a refresh that races with its finish can still reach finish listeners and span processors; the aggregator re-checks under its lock, so its trace is still not exported.In MicroVM mode, a writer swap outside a refresh (such as
Tracer.configure()) holds the span-aggregator lock while the old writer flushes, so span start/finish wait for that flush, including its send retries if the agent is unreachable. These swaps are rare, and ordinary periodic flushes no longer block span start/finish. If such a swap overlaps the runtime-ID rotation, the published runtime ID can briefly be newer than the one its writer was built with, until the refresh callback replaces that writer.LogWriter(MicroVM Lambda without the agent extension) writes to stdout under the span-aggregator lock in MicroVM mode, so the generation check and the write are atomic. A stalled stdout delays span finishes and/runwhile it lasts.A payload already in
send()when a refresh starts still runs its retries to completion on the thread that started it; nothing else waits on it, and no later payload from that writer is sent. For a sync writer, that thread is the span-finishing ortracer.flush()thread, so that one call still takes as long as the retries.In the exit-signal deadlock case, a MicroVM
shutdown(timeout)can take up to about 2 ×timeout: the bounded wait for an in-flight refresh, then the normal flush. If it gives up waiting while the span-start retry of a failed refresh (_ensure_post_fork_writer()) holds the lock, that retry stops the writer it installs. A Rust panic from the native metadata write still reaches/run, as it does from__init__and after a fork; the write runs after the rebuild, so it can't skip it.In MicroVM sync mode, the deferred send is drained by
Tracer._on_span_finish. Code that callsSpanAggregator.on_span_finishdirectly, without going through the tracer, has its trace sent on that thread's next traced span finish instead.Unchanged from
main, and not specific to the refresh: an exit signal that arrives while a writer rebuild is inside the old writer'sstop()deadlocks on that writer's_service_lock, as it does forTracer.configure()and the post-fork rebuild; and, outside MicroVM mode, a post-fork rebuild interrupted by a same-thread shutdown whose signal handler returns can still install a writer after shutdown.