Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions ddtrace/internal/_runtime_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import typing as t
import uuid

from ddtrace.internal.settings import env

from . import forksafe


__all__ = [
"get_ancestor_runtime_id",
"get_process_role",
"get_runtime_id",
"get_parent_runtime_id",
"get_runtime_propagation_envs",
]


_ENV_ROOT_SESSION_ID = "_DD_ROOT_PY_SESSION_ID"
_ENV_PARENT_SESSION_ID = "_DD_PARENT_PY_SESSION_ID"


def _generate_runtime_id() -> str:
return uuid.uuid4().hex


_RUNTIME_ID: str = _generate_runtime_id()
# Seeded from env vars when this process was spawned (multiprocessing spawn/forkserver).
# For fork-based processes these are set by _set_runtime_id() via the forksafe hook.
_ANCESTOR_RUNTIME_ID: t.Optional[str] = env.get(_ENV_ROOT_SESSION_ID)
_PARENT_RUNTIME_ID: t.Optional[str] = env.get(_ENV_PARENT_SESSION_ID)
# IMPORTANT: Do not change t.Set to set until minimum Python version is 3.11+
# Module-level set[...] in Python 3.10 affects import timing. See packages.py for details.
_ON_RUNTIME_ID_CHANGE: t.Set[t.Callable[[str], None]] = set() # noqa: UP006


def on_runtime_id_change(cb: t.Callable[[str], None]) -> None:
"""Register a callback to be called when the runtime ID changes.

This can happen after a fork.
"""
global _ON_RUNTIME_ID_CHANGE
_ON_RUNTIME_ID_CHANGE.add(cb)


@forksafe.register
def _set_runtime_id() -> None:
global _RUNTIME_ID, _ANCESTOR_RUNTIME_ID, _PARENT_RUNTIME_ID

# Save the runtime ID of the common ancestor of all processes.
if _ANCESTOR_RUNTIME_ID is None:
_ANCESTOR_RUNTIME_ID = _RUNTIME_ID

_PARENT_RUNTIME_ID = _RUNTIME_ID
_RUNTIME_ID = _generate_runtime_id()
for cb in _ON_RUNTIME_ID_CHANGE:
cb(_RUNTIME_ID)


def get_runtime_id() -> str:
"""Return a unique string identifier for this runtime.

Do not store this identifier as it can change when, e.g., the process forks.
"""
return _RUNTIME_ID


def get_ancestor_runtime_id() -> t.Optional[str]:
"""Return the runtime ID of the common ancestor of this process.

Once this value is set (this will happen after a fork) it will not change
for the lifetime of the process. This function returns ``None`` for the
ancestor process.
"""
return _ANCESTOR_RUNTIME_ID


def get_parent_runtime_id() -> t.Optional[str]:
"""Return the runtime ID of the parent process.

Set after a fork or when seeded from the ``_DD_PARENT_PY_SESSION_ID`` environment
variable (multiprocessing spawn/forkserver). Returns ``None`` in the root process.
"""
return _PARENT_RUNTIME_ID


def get_process_role() -> t.Optional[str]:
"""Return the role of this process in a forking framework.

Returns ``'worker'`` if this process was forked from a parent (or spawned
as a child via multiprocessing), ``'main'`` if this process has forked
worker children, or ``None`` for a single-process application.
"""
if _PARENT_RUNTIME_ID is not None:
return "worker"
if forksafe.has_forked():
return "main"
return None


def get_runtime_propagation_envs() -> dict[str, str]:
"""Return session lineage env vars to inject into child process environments.

These vars allow exec-based child processes (subprocess, multiprocessing spawn)
to reconstruct the process lineage without relying on fork inheritance.
"""
ancestor = get_ancestor_runtime_id()
current = get_runtime_id()
session_vars: dict[str, str] = {_ENV_ROOT_SESSION_ID: ancestor if ancestor is not None else current}
if current is not None:
session_vars[_ENV_PARENT_SESSION_ID] = current
return session_vars
2 changes: 1 addition & 1 deletion ddtrace/internal/core/crashtracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
from ddtrace.internal import excepthook
from ddtrace.internal import forksafe
from ddtrace.internal import process_tags
from ddtrace.internal._runtime_id import get_runtime_id
from ddtrace.internal.compat import ensure_text
from ddtrace.internal.logger import get_logger
from ddtrace.internal.runtime import get_runtime_id
from ddtrace.internal.settings import env
from ddtrace.internal.settings._agent import config as agent_config
from ddtrace.internal.settings.crashtracker import config as crashtracker_config
Expand Down
4 changes: 2 additions & 2 deletions ddtrace/internal/remoteconfig/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import ddtrace
from ddtrace.internal import gitmetadata
from ddtrace.internal import process_tags
from ddtrace.internal import runtime
from ddtrace.internal._runtime_id import get_runtime_id
from ddtrace.internal.hostname import get_hostname
from ddtrace.internal.logger import get_logger
from ddtrace.internal.packages import is_distribution_available
Expand Down Expand Up @@ -109,7 +109,7 @@ def ensure_native(self) -> Any:
agent_url=str(self.agent_url),
tracer_version=tracer_version,
client_id=self.id,
runtime_id=runtime.get_runtime_id(),
runtime_id=get_runtime_id(),
service=ddtrace.config.service or "",
env=ddtrace.config.env or "",
app_version=ddtrace.config.version or "",
Expand Down
109 changes: 7 additions & 102 deletions ddtrace/internal/runtime/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import typing as t
import uuid

from ddtrace.internal.settings import env

from .. import forksafe
from ddtrace.internal._runtime_id import get_ancestor_runtime_id
from ddtrace.internal._runtime_id import get_parent_runtime_id
from ddtrace.internal._runtime_id import get_process_role

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update process-role mocks to patch the moved state

When the profiling suite runs test_uwsgi_postfork_worker_role_via_mock or test_uwsgi_main_role_via_mock, tests/profiling/test_process_role.py still calls monkeypatch.setattr(ddtrace.internal.runtime, "_PARENT_RUNTIME_ID", ...). This wrapper no longer defines that attribute, so both tests raise AttributeError before reaching their assertions; update those mocks to patch ddtrace.internal._runtime_id (where get_process_role now reads the state), or preserve compatible forwarding.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Retarget process-role state mocks after the move

The profiling test suite fails, so the PR cannot complete required validation.

Assertion details
  • Input: Run test_uwsgi_postfork_worker_role_via_mock or test_uwsgi_main_role_via_mock in tests/profiling/test_process_role.py.
  • Expected: The profiling tests must patch _PARENT_RUNTIME_ID in ddtrace.internal._runtime_id, where the state now exists.
  • Actual: Both monkeypatch.setattr calls raise AttributeError because ddtrace.internal.runtime no longer has _PARENT_RUNTIME_ID. The tests stop before their assertions.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

from ddtrace.internal._runtime_id import get_runtime_id
from ddtrace.internal._runtime_id import get_runtime_propagation_envs
from ddtrace.internal._runtime_id import on_runtime_id_change


__all__ = [
Expand All @@ -12,100 +12,5 @@
"get_runtime_id",
"get_parent_runtime_id",
"get_runtime_propagation_envs",
"on_runtime_id_change",
]


_ENV_ROOT_SESSION_ID = "_DD_ROOT_PY_SESSION_ID"
_ENV_PARENT_SESSION_ID = "_DD_PARENT_PY_SESSION_ID"


def _generate_runtime_id() -> str:
return uuid.uuid4().hex


_RUNTIME_ID: str = _generate_runtime_id()
# Seeded from env vars when this process was spawned (multiprocessing spawn/forkserver).
# For fork-based processes these are set by _set_runtime_id() via the forksafe hook.
_ANCESTOR_RUNTIME_ID: t.Optional[str] = env.get(_ENV_ROOT_SESSION_ID)
_PARENT_RUNTIME_ID: t.Optional[str] = env.get(_ENV_PARENT_SESSION_ID)
# IMPORTANT: Do not change t.Set to set until minimum Python version is 3.11+
# Module-level set[...] in Python 3.10 affects import timing. See packages.py for details.
_ON_RUNTIME_ID_CHANGE: t.Set[t.Callable[[str], None]] = set() # noqa: UP006


def on_runtime_id_change(cb: t.Callable[[str], None]) -> None:
"""Register a callback to be called when the runtime ID changes.

This can happen after a fork.
"""
global _ON_RUNTIME_ID_CHANGE
_ON_RUNTIME_ID_CHANGE.add(cb)


@forksafe.register
def _set_runtime_id():
global _RUNTIME_ID, _ANCESTOR_RUNTIME_ID, _PARENT_RUNTIME_ID

# Save the runtime ID of the common ancestor of all processes.
if _ANCESTOR_RUNTIME_ID is None:
_ANCESTOR_RUNTIME_ID = _RUNTIME_ID

_PARENT_RUNTIME_ID = _RUNTIME_ID
_RUNTIME_ID = _generate_runtime_id()
for cb in _ON_RUNTIME_ID_CHANGE:
cb(_RUNTIME_ID)


def get_runtime_id() -> str:
"""Return a unique string identifier for this runtime.

Do not store this identifier as it can change when, e.g., the process forks.
"""
return _RUNTIME_ID


def get_ancestor_runtime_id() -> t.Optional[str]:
"""Return the runtime ID of the common ancestor of this process.

Once this value is set (this will happen after a fork) it will not change
for the lifetime of the process. This function returns ``None`` for the
ancestor process.
"""
return _ANCESTOR_RUNTIME_ID


def get_parent_runtime_id() -> t.Optional[str]:
"""Return the runtime ID of the parent process.

Set after a fork or when seeded from the ``_DD_PARENT_PY_SESSION_ID`` environment
variable (multiprocessing spawn/forkserver). Returns ``None`` in the root process.
"""
return _PARENT_RUNTIME_ID


def get_process_role() -> t.Optional[str]:
"""Return the role of this process in a forking framework.

Returns ``'worker'`` if this process was forked from a parent (or spawned
as a child via multiprocessing), ``'main'`` if this process has forked
worker children, or ``None`` for a single-process application.
"""
if _PARENT_RUNTIME_ID is not None:
return "worker"
if forksafe.has_forked():
return "main"
return None


def get_runtime_propagation_envs() -> dict[str, str]:
"""Return session lineage env vars to inject into child process environments.

These vars allow exec-based child processes (subprocess, multiprocessing spawn)
to reconstruct the process lineage without relying on fork inheritance.
"""
ancestor = get_ancestor_runtime_id()
current = get_runtime_id()
session_vars: dict[str, str] = {_ENV_ROOT_SESSION_ID: ancestor if ancestor is not None else current}
if current is not None:
session_vars[_ENV_PARENT_SESSION_ID] = current
return session_vars
2 changes: 1 addition & 1 deletion ddtrace/internal/symbol_db/remoteconfig.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import typing as t

from ddtrace.internal._runtime_id import get_ancestor_runtime_id
from ddtrace.internal.forksafe import get_generation
from ddtrace.internal.forksafe import has_forked
from ddtrace.internal.ipc import SharedStringFile
Expand All @@ -9,7 +10,6 @@
from ddtrace.internal.remoteconfig import Payload
from ddtrace.internal.remoteconfig import RCCallback
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller
from ddtrace.internal.runtime import get_ancestor_runtime_id
from ddtrace.internal.symbol_db.symbols import SymbolDatabaseUploader


Expand Down
4 changes: 2 additions & 2 deletions ddtrace/internal/symbol_db/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from ddtrace import config
from ddtrace.internal import forksafe
from ddtrace.internal import packages
from ddtrace.internal._runtime_id import get_ancestor_runtime_id
from ddtrace.internal._runtime_id import get_runtime_id
from ddtrace.internal.compat import singledispatchmethod
from ddtrace.internal.constants import DEFAULT_SERVICE_NAME
from ddtrace.internal.logger import get_logger
Expand All @@ -36,8 +38,6 @@
from ddtrace.internal.native import SymDBSender
from ddtrace.internal.native_runtime import get_native_runtime
from ddtrace.internal.periodic import Timer
from ddtrace.internal.runtime import get_ancestor_runtime_id
from ddtrace.internal.runtime import get_runtime_id
from ddtrace.internal.safety import _isinstance
from ddtrace.internal.settings._agent import config as agent_config
from ddtrace.internal.settings.dynamic_instrumentation import config as di_config
Expand Down
6 changes: 3 additions & 3 deletions ddtrace/internal/telemetry/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
from ...internal import atexit
from ...internal import excepthook
from ...internal import forksafe
from .._runtime_id import get_ancestor_runtime_id
from .._runtime_id import get_parent_runtime_id
from .._runtime_id import get_runtime_id
from ..periodic import PeriodicService
from ..runtime import get_ancestor_runtime_id
from ..runtime import get_parent_runtime_id
from ..runtime import get_runtime_id
from ..utils.formats import get_test_session_token
from ..utils.version import version as tracer_version
from .constants import TELEMETRY_APM_PRODUCT
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/internal/writer/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
from typing import TextIO
from urllib.parse import urlparse as _urlparse

from ddtrace.internal._runtime_id import get_runtime_id
from ddtrace.internal.dist_computing.utils import in_ray_job
from ddtrace.internal.hostname import get_hostname
import ddtrace.internal.native as native
from ddtrace.internal.native import AgentResponse
from ddtrace.internal.native._native import SpanData
from ddtrace.internal.native_runtime import get_native_runtime
from ddtrace.internal.runtime import get_runtime_id
from ddtrace.internal.settings import env
from ddtrace.internal.settings._agent import config as agent_config
from ddtrace.internal.settings._config import config
Expand Down
8 changes: 4 additions & 4 deletions tests/profiling/test_process_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ def test_uwsgi_postfork_worker_role_via_mock(monkeypatch: pytest.MonkeyPatch) ->
postfork callback would. The role check verifies the detection primitive
under mock.
"""
import ddtrace.internal.runtime as _runtime_mod
import ddtrace.internal._runtime_id as _runtime_id_mod

def _raise_main(*args: object, **kwargs: object) -> None:
raise profiler.uwsgi.uWSGIMasterProcess() # type: ignore[attr-defined]

monkeypatch.setattr(profiler.uwsgi, "check_uwsgi", _raise_main) # type: ignore[attr-defined]

# Simulate what happens to _PARENT_RUNTIME_ID after a real fork in a worker.
monkeypatch.setattr(_runtime_mod, "_PARENT_RUNTIME_ID", "fake-parent-id")
monkeypatch.setattr(_runtime_id_mod, "_PARENT_RUNTIME_ID", "fake-parent-id")

from ddtrace.internal.runtime import get_process_role

Expand All @@ -121,13 +121,13 @@ def _raise_main(*args: object, **kwargs: object) -> None:

def test_uwsgi_main_role_via_mock(monkeypatch: pytest.MonkeyPatch) -> None:
"""uWSGI main process: get_process_role() returns None before any fork."""
import ddtrace.internal._runtime_id as _runtime_id_mod
import ddtrace.internal.forksafe as _forksafe
import ddtrace.internal.runtime as _runtime_mod

# Reset process-lifetime state that may be True if subprocess.run() was called
# earlier in this pytest session (subprocess.run uses os.fork on Linux).
monkeypatch.setattr(_forksafe, "_forked", False)
monkeypatch.setattr(_runtime_mod, "_PARENT_RUNTIME_ID", None)
monkeypatch.setattr(_runtime_id_mod, "_PARENT_RUNTIME_ID", None)

from ddtrace.internal.runtime import get_process_role

Expand Down
Loading