Skip to content

Avoid calling synchronized wrappers when capturing - #12106

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
masterfrom
dd/fix/di-synchronized-collection-deadlock
Jul 31, 2026
Merged

Avoid calling synchronized wrappers when capturing#12106
gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
masterfrom
dd/fix/di-synchronized-collection-deadlock

Conversation

@tylfin

@tylfin tylfin commented Jul 29, 2026

Copy link
Copy Markdown
Member

What Does This Do

Treats java.util.Collections$Synchronized* wrappers as unsafe to call during snapshot capture, so they are serialized as plain objects instead of through size() / entrySet() / iterator().

Fixes DYNIS-64

Motivation

Snapshot capture runs on the instrumented thread, and every method on a synchronized wrapper acquires the wrapper's mutex. Capture therefore makes the application thread take an application lock, in an order the application itself never uses. A customer hit a three-thread deadlock this way and had to disable Exception Replay to recover.

Monitor entry is untimed and uninterruptible, so neither the eval timeout nor the existing exception handling can unwind a thread that is already blocked. Not calling into the wrapper at all is the only reliable fix.

Additional Notes

  • Trade-off: synchronized collections no longer show their contents in snapshots. On JDK 9+ the object fallback cannot read the wrapper's private fields either, because java.base does not open java.util to the agent, so these values are captured without contents. Improving that rendering is a follow-up.
  • Probe conditions and log templates over a synchronized collection now report Unsupported Collection class: ... instead of evaluating, since the expression language shares the same safety check. ListValue.isEmpty() was missing that check and is guarded here too, so an isEmpty() condition can no longer take the mutex.
  • Out of scope: Vector, Stack, Hashtable and Properties lock on the instance, Collections.unmodifiableMap(synchronizedMap(...)) delegates through to the wrapper, and Guava's Synchronized$* wrappers carry the same hazard. Left for a follow-up, which should land after the rendering fix above.

Testing

  • WellKnownClassesTest covers the nine JDK synchronized wrapper types plus the collections that must stay safe.
  • IsEmptyExpressionTest covers the guarded isEmpty() path for a synchronized list and map.
  • SnapshotSerializationTest.synchronizedMapLockedByAnotherThread serializes a synchronized map while another thread holds its mutex. It blocks until the timeout before the fix and completes after it.

Contributor Checklist

@datadog-prod-us1-4

datadog-prod-us1-4 Bot commented Jul 29, 2026

Copy link
Copy Markdown

View session in Datadog

Bits Code status: ✅ Done

Comment @DataDog to request changes

@datadog-prod-us1-3

Copy link
Copy Markdown

I can only run on private repositories.

@datadog-prod-us1-4

datadog-prod-us1-4 Bot commented Jul 29, 2026

Copy link
Copy Markdown

🎯 Code Coverage (details)
Patch Coverage: 88.89%
Overall Coverage: 61.41% (+3.62%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 80f278e | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented Jul 29, 2026

Copy link
Copy Markdown

Debugger benchmarks

Parameters

Baseline Candidate
baseline_or_candidate baseline candidate
ci_job_date 1785503988 1785504334
end_time 2026-07-31T13:21:14 2026-07-31T13:27:00
git_branch master dd/fix/di-synchronized-collection-deadlock
git_commit_sha b22cb26 80f278e
start_time 2026-07-31T13:19:49 2026-07-31T13:25:35
See matching parameters
Baseline Candidate
ci_job_id 1912300351 1912300351
ci_pipeline_id 128178835 128178835
cpu_model Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz
git_commit_date 1785488676 1785488676

Summary

Found 0 performance improvements and 0 performance regressions! Performance is the same for 9 metrics, 6 unstable metrics.

See unchanged results
scenario Δ mean agg_http_req_duration_min Δ mean agg_http_req_duration_p50 Δ mean agg_http_req_duration_p75 Δ mean agg_http_req_duration_p99 Δ mean throughput
scenario:noprobe unstable
[-21.554µs; +35.716µs] or [-7.379%; +12.227%]
unstable
[-34.442µs; +46.447µs] or [-10.225%; +13.790%]
unstable
[-45.372µs; +60.154µs] or [-12.852%; +17.040%]
unstable
[-180.306µs; +55.683µs] or [-14.178%; +4.378%]
same
scenario:basic unsure
[+0.891µs; +6.634µs] or [+0.339%; +2.527%]
same same unstable
[-176.950µs; +75.458µs] or [-15.479%; +6.601%]
unstable
[-218.546op/s; +96.595op/s] or [-8.742%; +3.864%]
scenario:loop same same unsure
[-10.061µs; -0.172µs] or [-0.111%; -0.002%]
same same
Request duration reports for reports
gantt
    title reports - request duration [CI 0.99] : candidate=None, baseline=None
    dateFormat X
    axisFormat %s
section baseline
noprobe (336.83 µs) : 305, 369
.   : milestone, 337,
basic (296.479 µs) : 290, 303
.   : milestone, 296,
loop (8.982 ms) : 8977, 8987
.   : milestone, 8982,
section candidate
noprobe (342.832 µs) : 299, 387
.   : milestone, 343,
basic (298.62 µs) : 292, 305
.   : milestone, 299,
loop (8.985 ms) : 8980, 8990
.   : milestone, 8985,
Loading
  • baseline results
Scenario Request median duration [CI 0.99]
noprobe 336.83 µs [305.05 µs, 368.61 µs]
basic 296.479 µs [289.545 µs, 303.413 µs]
loop 8.982 ms [8.977 ms, 8.987 ms]
  • candidate results
Scenario Request median duration [CI 0.99]
noprobe 342.832 µs [298.714 µs, 386.95 µs]
basic 298.62 µs [291.778 µs, 305.462 µs]
loop 8.985 ms [8.98 ms, 8.99 ms]

@dd-octo-sts

dd-octo-sts Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.01 s 13.88 s [+0.1%; +1.7%] (maybe worse)
startup:insecure-bank:tracing:Agent 12.92 s 12.90 s [-0.5%; +0.8%] (no difference)
startup:petclinic:appsec:Agent 16.83 s 16.66 s [-0.1%; +2.1%] (no difference)
startup:petclinic:iast:Agent 16.85 s 16.95 s [-1.5%; +0.3%] (no difference)
startup:petclinic:profiling:Agent 16.72 s 16.84 s [-1.9%; +0.4%] (no difference)
startup:petclinic:sca:Agent 16.64 s 16.52 s [-0.3%; +1.7%] (no difference)
startup:petclinic:tracing:Agent 16.10 s 16.19 s [-1.4%; +0.3%] (no difference)

Commit: 80f278e3 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@tylfin tylfin removed the Bits AI label Jul 29, 2026
@tylfin
tylfin marked this pull request as ready for review July 29, 2026 23:01
@tylfin
tylfin requested a review from a team as a code owner July 29, 2026 23:01
@tylfin
tylfin requested review from jpbempel and removed request for a team July 29, 2026 23:01
@dd-octo-sts

dd-octo-sts Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@datadog-prod-us1-4 datadog-prod-us1-4 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Datadog Autotest: PASS

More details

The synchronized-wrapper guard covers every direct JDK Collections synchronized variant observed on Java 21, including SynchronizedRandomAccessList, while leaving ordinary and unmodifiable collections unchanged. No diff-only behavioral regression was found; the focused Gradle tests remain unexecuted because the sandbox lacks the repository's required Java 25 toolchain.

Was this helpful? React 👍 or 👎

📊 Validated against 10 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 9fd8f4f · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fd8f4f0f8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@tylfin tylfin added comp: debugger Dynamic Instrumentation type: bug fix Bug fix labels Jul 30, 2026
@jpbempel jpbempel changed the title fix(debugger): avoid locking synchronized wrappers Avoid calling synchronized wrappers when capturing Jul 31, 2026

@jpbempel jpbempel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM as a quick fix for this particular case.
but needs rework for more cases and get back to an identical feature as non-synchronized collections

@jpbempel
jpbempel enabled auto-merge July 31, 2026 08:18
datadog-bits and others added 3 commits July 31, 2026 11:04
…rialization

Calling size()/entrySet()/iterator() on java.util.Collections$Synchronized*
wrappers acquires the wrapper mutex on the instrumented thread, which can
deadlock the application. Treat them as unsafe so they are serialized as
regular objects instead.
Add a direct WellKnownClasses.isSafe test over the nine JDK
Collections$Synchronized* types and the collections that must stay
safe, so the prefix check cannot regress unnoticed.

Harden the serialization regression test: the mutex holder is a daemon
thread and the latch wait moved inside the try block, so a failed wait
cannot leave the monitor held and block the test JVM. The assertion now
checks the backing map is emitted as a field.
ListValue.isEmpty() was the only expression language entry point that
called into a collection without consulting WellKnownClasses, so an
isEmpty() condition or log template over a Collections.synchronizedList
could still take the wrapper mutex on the instrumented thread and
deadlock. Mirror the guard already present in SetValue and MapValue.
@jpbempel
jpbempel force-pushed the dd/fix/di-synchronized-collection-deadlock branch from dfebb2f to 80f278e Compare July 31, 2026 09:06
@jpbempel
jpbempel added this pull request to the merge queue Jul 31, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Jul 31, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-31 13:33:45 UTC ℹ️ Start processing command /merge


2026-07-31 13:33:49 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-07-31 13:46:50 UTCMergeQueue: The build pipeline contains failing jobs for this merge request

Build pipeline has failing jobs for 8105e33:

⚠️ Do NOT retry failed jobs directly (why?).

What to do next?

  • Investigate the failures and when ready, re-add your pull request to the queue!
  • If your PR checks are green, try to rebase/merge. It might be because the CI run is a bit old.
  • Any question, go check the FAQ.
Details

Since those jobs are not marked as being allowed to fail, the pipeline will most likely fail.
Therefore, and to allow other builds to be processed, this merge request has been rejected and the pipeline got canceled.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 31, 2026
@tylfin
tylfin added this pull request to the merge queue Jul 31, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Jul 31, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-31 13:36:20 UTC ℹ️ Start processing command /merge


2026-07-31 13:36:23 UTC ❌ MergeQueue

PR already in the queue with status in_progress

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 31, 2026
@jpbempel

Copy link
Copy Markdown
Member

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Jul 31, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-31 14:56:33 UTC ℹ️ Start processing command /merge


2026-07-31 14:56:38 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-07-31 15:47:57 UTC ℹ️ MergeQueue: This merge request was merged

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 19d6a6e into master Jul 31, 2026
596 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the dd/fix/di-synchronized-collection-deadlock branch July 31, 2026 15:47
@github-actions github-actions Bot added this to the 1.65.0 milestone Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: debugger Dynamic Instrumentation type: bug fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants