fix(telemetry): tolerate malformed dist-info in get_distributions - #17994
Conversation
A single installed distribution with malformed or unreadable METADATA (missing ``Name:`` header, unparseable PKG-INFO, transient IOError during interpreter teardown) made ``get_distributions`` raise. ``@callonce`` cached that exception for the lifetime of the process, and the per-module ``try/except + log.debug(..., exc_info=True)`` added by #17593 logged the chained ``AttributeError``/``KeyError`` traceback once per imported module per heartbeat per worker -- gigabytes of stderr per CI job on customer environments running pytest under ``uv venv --system-site-packages``. The strict ``metadata["name"]`` access has been latent since well before 4.5. In 4.5 a malformed dist made the heartbeat raise once per cycle (handled silently by ``threading.excepthook``); the per-module guard added in 4.8 to harden against shutdown-time iterator failures inadvertently turned that into per-module log spam every heartbeat forever. Fix: per-dist ``try/except`` in ``get_distributions``, ``_package_for_root_module_mapping``, and the ``<3.10`` ``_packages_distributions`` fallback. Skip malformed entries individually, return what could be parsed, warn once per bad dist (deduped via ``_BAD_DISTS_WARNED``). Use ``metadata["Name"] or ""`` (canonical PEP 566 capitalization, email-Message lookup is case-insensitive) to neutralize both the legacy silent-None path and the future-strict KeyError path emitted by ``importlib.metadata._adapters._warn`` / ``importlib_metadata`` backport. The hardening guard in ``update_imported_dependencies`` is unchanged -- it still defends against shutdown-time iterator failures that cannot be caught from inside ``get_distributions``. Refs: #17593 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codeowners resolved as |
BenchmarksBenchmark execution time: 2026-05-11 16:25:31 Comparing candidate commit c4023d3 in PR branch Found 0 performance improvements and 5 performance regressions! Performance is the same for 590 metrics, 4 unstable metrics. scenario:iastaspects-index_aspect
scenario:iastaspects-stringio_aspect
scenario:iastaspectsospath-ospathbasename_aspect
scenario:span-start
scenario:telemetryaddmetric-1-count-metric-1-times
|
- Revert ``metadata["Name"]`` -> ``metadata["name"]`` in ``get_distributions`` and ``_package_for_root_module_mapping``. Email-Message lookups are case-insensitive so the two are runtime-equivalent; the original casing matches the surrounding code. - Drop ``or ""`` coercion: the ``if name and version:`` guard already short-circuits on both ``None`` and ``""``. - Drop the silent-None test (passed pre-fix too — didn't validate the fix), the metadata-attribute-error test (over-specified — guards a hypothetical refactor not covered by other tests), and merge the cache-result test into the keeper. Two tests remain, one per modified function, both load-bearing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Variables assigned inside ``try`` were used after it on a path that
``except: continue`` skipped — runtime-safe but a code smell ("possibly
unbound" to a static reader). Move the use inside the try in all three
modified functions; drop the now-redundant ``continue`` at end-of-loop.
Compress both AIDEV-NOTEs to two lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ``_log_bad_dist`` now emits at DEBUG. The skip is graceful and the malformed dist is usually a permanent fixture of the host (e.g. system Python on Debian/RHEL); WARNING just polluted normal logs. - Drop the outer try/except around ``importlib_metadata.distributions()`` in ``_package_for_root_module_mapping``. The per-dist try/except already covers per-iteration failures; the iterator-creation case is consistent with ``get_distributions`` which doesn't guard it either. - Fix test isolation in ``reset_packages_caches``: clear the @CallOnce cache on teardown too. The fixture site-packages used by these tests was bleeding into ``test_filename_to_package`` running next in the same xdist worker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The iterator-creation case is genuinely catastrophic (no dists enumerable at all) and worth a one-shot ``LOG.warning(..., exc_info=True)`` rather than letting @CallOnce cache the exception and re-raise from ``filename_to_package`` and friends every time. Per-dist failures stay at DEBUG via ``_log_bad_dist``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ae43e5b to
a7d63c6
Compare
|
This change is marked for backport to 4.7 and it does not conflict with that branch. |
|
This change is marked for backport to 4.8 and it does not conflict with that branch. |
|
This change is marked for backport to 4.6 and it does not conflict with that branch. |
|
/merge -p urgent |
|
View all feedbacks in Devflow UI.
The expected merge time in
Build pipeline has failing jobs for b9729cf: What to do next?
DetailsSince those jobs are not marked as being allowed to fail, the pipeline will most likely fail. |
…7994) ## Problem `ddtrace.internal.packages.get_distributions` does strict `metadata["name"]` access. On environments where one installed dist has malformed METADATA (missing `Name:`, unparseable PKG-INFO) **and** missing-key access raises (`importlib_metadata` backport, `-W error::DeprecationWarning`, future Python), the call raises and `@callonce` caches the exception for the lifetime of the process. ## Impact The telemetry dependency tracker (added in #17593) calls `get_distributions` per imported module on every heartbeat, wrapped in `try/except + log.debug(exc_info=True)`. Every call after the first re-raises the cached exception, producing a chained `AttributeError`/`KeyError` traceback per module per heartbeat per worker — customer reports of ~16 GiB of stderr per pytest CI job under `uv venv --system-site-packages` trace back to this. The same bug exists, less loudly, in `_package_for_root_module_mapping` and the `<3.10` `_packages_distributions` fallback: one bad dist collapses the whole mapping to `None`, silently breaking `is_third_party` / `filename_to_package` for the rest of the process. ## Solution Per-dist `try/except` in all three functions: skip malformed entries individually, return what could be parsed, warn once per bad dist (deduped via `_BAD_DISTS_WARNED`). The `update_imported_dependencies` shutdown-hardening guard from #17593 is unchanged — it still defends against shutdown-time iterator failures that can't be caught from inside `get_distributions`. **Verification:** end-to-end against the customer reproducer (50 modules + one bad dist on path) — pre-fix logs 50 chained tracebacks, post-fix logs 0 (one diagnostic `Skipping distribution` warning). `scripts/run-tests --venv 803a341 -- tests/internal/test_packages*` → 7 passed. Backport candidate to 4.8.x. Refs: #17593 ## Perf While we added a`try/except` it is not generating a sigifnificant impact on python < 3.11, as the leading overhead comes from I/O ``` ┌─────────┬──────────┬────────────┬─────────────────┐ │ Python │ plain │ try/except │ Δ per iter │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.10.13 │ 26.39 ns │ 28.88 ns │ +2.5 ns │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.11.8 │ 22.84 ns │ 23.30 ns │ +0.5 ns (noise) │ └─────────┴──────────┴────────────┴─────────────────┘ ``` For `get_distributions` specifically: it's `@callonce` (runs once per worker) over ~150 dists and is dominated by METADATA file I/O (~27 ms total on 3.10). The worst-case added cost is 2.5 ns × 150 ≈ 0.4 µs, six orders of magnitude under the function's runtime — invisible in an end-to-end bench against real importlib.metadata.distributions(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: juanjux <juanjo.alvarezmartinez@datadoghq.com> Co-authored-by: brett.langdon <brett.langdon@datadoghq.com> (cherry picked from commit ba713e3) Co-authored-by: Luca Abbati <luca.abbati@datadoghq.com>
…7994) ## Problem `ddtrace.internal.packages.get_distributions` does strict `metadata["name"]` access. On environments where one installed dist has malformed METADATA (missing `Name:`, unparseable PKG-INFO) **and** missing-key access raises (`importlib_metadata` backport, `-W error::DeprecationWarning`, future Python), the call raises and `@callonce` caches the exception for the lifetime of the process. ## Impact The telemetry dependency tracker (added in #17593) calls `get_distributions` per imported module on every heartbeat, wrapped in `try/except + log.debug(exc_info=True)`. Every call after the first re-raises the cached exception, producing a chained `AttributeError`/`KeyError` traceback per module per heartbeat per worker — customer reports of ~16 GiB of stderr per pytest CI job under `uv venv --system-site-packages` trace back to this. The same bug exists, less loudly, in `_package_for_root_module_mapping` and the `<3.10` `_packages_distributions` fallback: one bad dist collapses the whole mapping to `None`, silently breaking `is_third_party` / `filename_to_package` for the rest of the process. ## Solution Per-dist `try/except` in all three functions: skip malformed entries individually, return what could be parsed, warn once per bad dist (deduped via `_BAD_DISTS_WARNED`). The `update_imported_dependencies` shutdown-hardening guard from #17593 is unchanged — it still defends against shutdown-time iterator failures that can't be caught from inside `get_distributions`. **Verification:** end-to-end against the customer reproducer (50 modules + one bad dist on path) — pre-fix logs 50 chained tracebacks, post-fix logs 0 (one diagnostic `Skipping distribution` warning). `scripts/run-tests --venv 803a341 -- tests/internal/test_packages*` → 7 passed. Backport candidate to 4.8.x. Refs: #17593 ## Perf While we added a`try/except` it is not generating a sigifnificant impact on python < 3.11, as the leading overhead comes from I/O ``` ┌─────────┬──────────┬────────────┬─────────────────┐ │ Python │ plain │ try/except │ Δ per iter │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.10.13 │ 26.39 ns │ 28.88 ns │ +2.5 ns │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.11.8 │ 22.84 ns │ 23.30 ns │ +0.5 ns (noise) │ └─────────┴──────────┴────────────┴─────────────────┘ ``` For `get_distributions` specifically: it's `@callonce` (runs once per worker) over ~150 dists and is dominated by METADATA file I/O (~27 ms total on 3.10). The worst-case added cost is 2.5 ns × 150 ≈ 0.4 µs, six orders of magnitude under the function's runtime — invisible in an end-to-end bench against real importlib.metadata.distributions(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: juanjux <juanjo.alvarezmartinez@datadoghq.com> Co-authored-by: brett.langdon <brett.langdon@datadoghq.com> (cherry picked from commit ba713e3) Co-authored-by: Luca Abbati <luca.abbati@datadoghq.com>
…7994) ## Problem `ddtrace.internal.packages.get_distributions` does strict `metadata["name"]` access. On environments where one installed dist has malformed METADATA (missing `Name:`, unparseable PKG-INFO) **and** missing-key access raises (`importlib_metadata` backport, `-W error::DeprecationWarning`, future Python), the call raises and `@callonce` caches the exception for the lifetime of the process. ## Impact The telemetry dependency tracker (added in #17593) calls `get_distributions` per imported module on every heartbeat, wrapped in `try/except + log.debug(exc_info=True)`. Every call after the first re-raises the cached exception, producing a chained `AttributeError`/`KeyError` traceback per module per heartbeat per worker — customer reports of ~16 GiB of stderr per pytest CI job under `uv venv --system-site-packages` trace back to this. The same bug exists, less loudly, in `_package_for_root_module_mapping` and the `<3.10` `_packages_distributions` fallback: one bad dist collapses the whole mapping to `None`, silently breaking `is_third_party` / `filename_to_package` for the rest of the process. ## Solution Per-dist `try/except` in all three functions: skip malformed entries individually, return what could be parsed, warn once per bad dist (deduped via `_BAD_DISTS_WARNED`). The `update_imported_dependencies` shutdown-hardening guard from #17593 is unchanged — it still defends against shutdown-time iterator failures that can't be caught from inside `get_distributions`. **Verification:** end-to-end against the customer reproducer (50 modules + one bad dist on path) — pre-fix logs 50 chained tracebacks, post-fix logs 0 (one diagnostic `Skipping distribution` warning). `scripts/run-tests --venv 803a341 -- tests/internal/test_packages*` → 7 passed. Backport candidate to 4.8.x. Refs: #17593 ## Perf While we added a`try/except` it is not generating a sigifnificant impact on python < 3.11, as the leading overhead comes from I/O ``` ┌─────────┬──────────┬────────────┬─────────────────┐ │ Python │ plain │ try/except │ Δ per iter │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.10.13 │ 26.39 ns │ 28.88 ns │ +2.5 ns │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.11.8 │ 22.84 ns │ 23.30 ns │ +0.5 ns (noise) │ └─────────┴──────────┴────────────┴─────────────────┘ ``` For `get_distributions` specifically: it's `@callonce` (runs once per worker) over ~150 dists and is dominated by METADATA file I/O (~27 ms total on 3.10). The worst-case added cost is 2.5 ns × 150 ≈ 0.4 µs, six orders of magnitude under the function's runtime — invisible in an end-to-end bench against real importlib.metadata.distributions(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: juanjux <juanjo.alvarezmartinez@datadoghq.com> Co-authored-by: brett.langdon <brett.langdon@datadoghq.com> (cherry picked from commit ba713e3) Co-authored-by: Luca Abbati <luca.abbati@datadoghq.com>
…7994) ## Problem `ddtrace.internal.packages.get_distributions` does strict `metadata["name"]` access. On environments where one installed dist has malformed METADATA (missing `Name:`, unparseable PKG-INFO) **and** missing-key access raises (`importlib_metadata` backport, `-W error::DeprecationWarning`, future Python), the call raises and `@callonce` caches the exception for the lifetime of the process. ## Impact The telemetry dependency tracker (added in #17593) calls `get_distributions` per imported module on every heartbeat, wrapped in `try/except + log.debug(exc_info=True)`. Every call after the first re-raises the cached exception, producing a chained `AttributeError`/`KeyError` traceback per module per heartbeat per worker — customer reports of ~16 GiB of stderr per pytest CI job under `uv venv --system-site-packages` trace back to this. The same bug exists, less loudly, in `_package_for_root_module_mapping` and the `<3.10` `_packages_distributions` fallback: one bad dist collapses the whole mapping to `None`, silently breaking `is_third_party` / `filename_to_package` for the rest of the process. ## Solution Per-dist `try/except` in all three functions: skip malformed entries individually, return what could be parsed, warn once per bad dist (deduped via `_BAD_DISTS_WARNED`). The `update_imported_dependencies` shutdown-hardening guard from #17593 is unchanged — it still defends against shutdown-time iterator failures that can't be caught from inside `get_distributions`. **Verification:** end-to-end against the customer reproducer (50 modules + one bad dist on path) — pre-fix logs 50 chained tracebacks, post-fix logs 0 (one diagnostic `Skipping distribution` warning). `scripts/run-tests --venv 803a341 -- tests/internal/test_packages*` → 7 passed. Backport candidate to 4.8.x. Refs: #17593 ## Perf While we added a`try/except` it is not generating a sigifnificant impact on python < 3.11, as the leading overhead comes from I/O ``` ┌─────────┬──────────┬────────────┬─────────────────┐ │ Python │ plain │ try/except │ Δ per iter │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.10.13 │ 26.39 ns │ 28.88 ns │ +2.5 ns │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.11.8 │ 22.84 ns │ 23.30 ns │ +0.5 ns (noise) │ └─────────┴──────────┴────────────┴─────────────────┘ ``` For `get_distributions` specifically: it's `@callonce` (runs once per worker) over ~150 dists and is dominated by METADATA file I/O (~27 ms total on 3.10). The worst-case added cost is 2.5 ns × 150 ≈ 0.4 µs, six orders of magnitude under the function's runtime — invisible in an end-to-end bench against real importlib.metadata.distributions(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: juanjux <juanjo.alvarezmartinez@datadoghq.com> Co-authored-by: brett.langdon <brett.langdon@datadoghq.com>
…7994) ## Problem `ddtrace.internal.packages.get_distributions` does strict `metadata["name"]` access. On environments where one installed dist has malformed METADATA (missing `Name:`, unparseable PKG-INFO) **and** missing-key access raises (`importlib_metadata` backport, `-W error::DeprecationWarning`, future Python), the call raises and `@callonce` caches the exception for the lifetime of the process. ## Impact The telemetry dependency tracker (added in #17593) calls `get_distributions` per imported module on every heartbeat, wrapped in `try/except + log.debug(exc_info=True)`. Every call after the first re-raises the cached exception, producing a chained `AttributeError`/`KeyError` traceback per module per heartbeat per worker — customer reports of ~16 GiB of stderr per pytest CI job under `uv venv --system-site-packages` trace back to this. The same bug exists, less loudly, in `_package_for_root_module_mapping` and the `<3.10` `_packages_distributions` fallback: one bad dist collapses the whole mapping to `None`, silently breaking `is_third_party` / `filename_to_package` for the rest of the process. ## Solution Per-dist `try/except` in all three functions: skip malformed entries individually, return what could be parsed, warn once per bad dist (deduped via `_BAD_DISTS_WARNED`). The `update_imported_dependencies` shutdown-hardening guard from #17593 is unchanged — it still defends against shutdown-time iterator failures that can't be caught from inside `get_distributions`. **Verification:** end-to-end against the customer reproducer (50 modules + one bad dist on path) — pre-fix logs 50 chained tracebacks, post-fix logs 0 (one diagnostic `Skipping distribution` warning). `scripts/run-tests --venv 803a341 -- tests/internal/test_packages*` → 7 passed. Backport candidate to 4.8.x. Refs: #17593 ## Perf While we added a`try/except` it is not generating a sigifnificant impact on python < 3.11, as the leading overhead comes from I/O ``` ┌─────────┬──────────┬────────────┬─────────────────┐ │ Python │ plain │ try/except │ Δ per iter │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.10.13 │ 26.39 ns │ 28.88 ns │ +2.5 ns │ ├─────────┼──────────┼────────────┼─────────────────┤ │ 3.11.8 │ 22.84 ns │ 23.30 ns │ +0.5 ns (noise) │ └─────────┴──────────┴────────────┴─────────────────┘ ``` For `get_distributions` specifically: it's `@callonce` (runs once per worker) over ~150 dists and is dominated by METADATA file I/O (~27 ms total on 3.10). The worst-case added cost is 2.5 ns × 150 ≈ 0.4 µs, six orders of magnitude under the function's runtime — invisible in an end-to-end bench against real importlib.metadata.distributions(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: juanjux <juanjo.alvarezmartinez@datadoghq.com> Co-authored-by: brett.langdon <brett.langdon@datadoghq.com>
Problem
ddtrace.internal.packages.get_distributionsdoes strictmetadata["name"]access. On environments where one installed dist has malformed METADATA (missingName:, unparseable PKG-INFO) and missing-key access raises (importlib_metadatabackport,-W error::DeprecationWarning, future Python), the call raises and@calloncecaches the exception for the lifetime of the process.Impact
The telemetry dependency tracker (added in #17593) calls
get_distributionsper imported module on every heartbeat, wrapped intry/except + log.debug(exc_info=True). Every call after the first re-raises the cached exception, producing a chainedAttributeError/KeyErrortraceback per module per heartbeat per worker — customer reports of ~16 GiB of stderr per pytest CI job underuv venv --system-site-packagestrace back to this.The same bug exists, less loudly, in
_package_for_root_module_mappingand the<3.10_packages_distributionsfallback: one bad dist collapses the whole mapping toNone, silently breakingis_third_party/filename_to_packagefor the rest of the process.Solution
Per-dist
try/exceptin all three functions: skip malformed entries individually, return what could be parsed, warn once per bad dist (deduped via_BAD_DISTS_WARNED). Theupdate_imported_dependenciesshutdown-hardening guard from #17593 is unchanged — it still defends against shutdown-time iterator failures that can't be caught from insideget_distributions.Verification: end-to-end against the customer reproducer (50 modules + one bad dist on path) — pre-fix logs 50 chained tracebacks, post-fix logs 0 (one diagnostic
Skipping distributionwarning).scripts/run-tests --venv 803a341 -- tests/internal/test_packages*→ 7 passed.Backport candidate to 4.8.x. Refs: #17593
Perf
While we added a
try/exceptit is not generating a sigifnificant impact on python < 3.11, as the leading overhead comes from I/OFor
get_distributionsspecifically: it's@callonce(runs once per worker) over ~150 dists and isdominated by METADATA file I/O (~27 ms total on 3.10). The worst-case added cost is 2.5 ns × 150 ≈
0.4 µs, six orders of magnitude under the function's runtime — invisible in an end-to-end bench
against real importlib.metadata.distributions().
🤖 Generated with Claude Code