Conversation
JsonParser216Helper reads package-private Jackson fields. On classpaths where they are missing (e.g. a mixed or repackaged Jackson) every getCurrentName() call threw and was reported. Rethrow the first NoSuchFieldError per class loader so it is still reported once, then assume interned field names. Assuming "interned" skips tainting the field name, trading a possible false negative for avoiding false positives from tainting shared interned strings. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb4e5331ef
ℹ️ 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".
This comment has been minimized.
This comment has been minimized.
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
| * first failure is rethrown so it is reported once; later calls assume interned names. | ||
| */ | ||
| @Test | ||
| void rethrowsFirstMissingFieldThenAssumesInterned() throws Exception { |
There was a problem hiding this comment.
non-blocking: The IAST design decision (unavailable fields => NameAdvice calls setCurrentName but skips taintName) is only asserted at the helper level (fetchInterner returns true). Nothing checks the actual advice behaviour, which is what the PR description asks IAST to sign off on. Consider a small test (e.g. a Groovy JsonParserInstrumentationTest case) using the missing-field loader that verifies taintString is called for values with the field name and not for the name itself. Otherwise a later change to NameAdvice could silently flip the trade-off.
There was a problem hiding this comment.
The trade-off is pinned from both sides today: test json parsing (tainted but field names) goes through the fetchInterner() == true branch and asserts no name taint (0 * _), so flipping that branch would fail it, and JsonParser216HelperTest asserts that a missing _symbols or _interner yields true, which routes into that same branch.
A true end-to-end test would need the agent to instrument classes from a child-first loader in that harness, which I haven't verified works. Since it's non-blocking, I'd leave it as a follow-up if IAST wants it.
Reply written by Claude (Claude Code) on behalf of dougqh.
| try { | ||
| return ByteQuadsCanonicalizer216Helper.fetchInterner(jsonParser._symbols); | ||
| } catch (NoSuchFieldError e) { | ||
| fieldsUnavailable = true; |
There was a problem hiding this comment.
non-blocking: The Javadoc and PR text say the first failure is reported "once". Since fieldsUnavailable is only set here in the catch, threads racing before the first failure completes each rethrow, so it is really "once per racing thread". The bounded cost is fine; I would just reword the Javadoc to "at least once, bounded by concurrency" (the Codex bot already raised the race, so not flagging it separately).
There was a problem hiding this comment.
Yes, agreed, I'll fix that. I'm not aiming for an exactly once guarantee.
I just want to prevent constantly throwing Errors forever.
There was a problem hiding this comment.
Reworded in 092eaf0: both helpers' Javadoc now say a failure is reported at least once, bounded by concurrency, and the PR description matches.
This also covers the Codex comment above about making the transition atomic: the goal is to stop repeated throws, and a thread that races the first failure pays at most one extra, so I've left the transition non-atomic.
Reply written by Claude (Claude Code) on behalf of dougqh.
| */ | ||
| @Test | ||
| void rethrowsFirstMissingFieldThenAssumesInterned() throws Exception { | ||
| ClassLoader loader = new MissingInternerClassLoader(); |
There was a problem hiding this comment.
nit: Only a missing _interner is simulated. The _symbols branch goes through the same catch, but a parametrized case would prove it. Also, a second MissingInternerClassLoader instance is not checked to rethrow once, which is what "per class loader" claims.
There was a problem hiding this comment.
Added both: a _symbols case (rethrowsFirstMissingSymbolsThenAssumesInterned) and eachClassLoaderRethrowsItsOwnFirstFailure, which runs two separate loaders with the same missing field. Both use one shared helper, assertRethrowsOnceThenAssumesInterned(className, field), rather than a parametrized test. The loader is now MissingFieldClassLoader, so it can rename either field.
Reply written by Claude (Claude Code) on behalf of dougqh.
| return ByteQuadsCanonicalizer216Helper.fetchInterner(jsonParser._symbols); | ||
| } catch (NoSuchFieldError e) { | ||
| fieldsUnavailable = true; | ||
| throw e; |
There was a problem hiding this comment.
nit: On the rethrown first call, NameAdvice aborts before setCurrentName, so the first field name after the failure is not tracked and its value is attributed to a stale or null name. Harmless, but worth a line in the Javadoc next to the trade-off note.
There was a problem hiding this comment.
Added to the JsonParser216Helper Javadoc, next to the trade-off note: the call that hits the failure is aborted by the advice's exception suppression before setCurrentName, so that one field name isn't tracked and the value after it may be attributed to no name or the previous one. Later calls aren't affected. The PR description has the same note.
Reply written by Claude (Claude Code) on behalf of dougqh.
|
I think I'm going to create a generic guard around this similar to what I'm doing for AbstractMethodError. |
Replace the shared static volatile flag with a Latch per field read: _symbols in JsonParser216Helper and _interner in ByteQuadsCanonicalizer216Helper. A classpath missing only one of the fields keeps using the other, and each field's first failure is rethrown and reported once. Adds Latch, a one-way call-site-wide latch (plain flag, hint semantics), with its tests, and a test for each missing field. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Latch has no user here; it moves to the Jackson NoSuchFieldError fix (#12670), which is where it is used. ClassLatch's Javadoc no longer links to it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Reword the Javadoc: a failure is reported at least once, bounded by concurrency, not exactly once; threads racing the first failure each rethrow. The aim is to stop throwing forever, not to report once. - Document that the call hitting the failure is aborted by the advice's exception suppression before setCurrentName, so that one field name is not tracked. - Test that a second class loader rethrows its own first failure (the latch state is per class loader). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Drop the defaultValue hook from Latch. The public methods are now tryGetOrNull (skip if latched, otherwise get; null means nothing is available) and tryGetOrDefault (null-coalescing sugar over it), so a call that yields nothing and a skipped call always agree. The protected hook stays get. A null return needs no allocation and no escape analysis, unlike a wrapper result type. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
handleNoSuchField latches on NoSuchFieldError and rethrows it, so the first failure is still reported. A missing field is the same for every receiver, so it belongs on the call-site-wide Latch. A field read throws nothing checked, so the read is a plain Function. Both Jackson field reads now go through it instead of repeating the try/catch. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9f470827b
ℹ️ 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".
…ATCH Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename INTERNER to INTERNER_LATCH and SYMBOLS to SYMBOLS_LATCH, so the fields say what they are. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rename the protected hook get to handle, so it reads the same across the Latch family and does not suggest a no-arg accessor. The public methods (tryGetOrNull, tryGetOrDefault) are unchanged. Naming only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Benchmark Latch against the status quo (read and catch on every call) and against a hand-rolled volatile and plain flag, for a real NoSuchFieldError built at setup, on the missing-field and working paths and at two stack depths. Results are added to its Javadoc once run. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Record a five-fork run on Zulu 17 (M1) in the benchmark's Javadoc: a latched skip against the status quo and against hand-rolled volatile and plain flags, on the missing-field and working paths at two stack depths. The latch matches a plain flag and a volatile flag costs more. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rename the protected hook handle to apply, and the public methods tryGetOrNull and tryGetOrDefault to tryApplyOrNull and tryApplyOrDefault. apply matches ThrowingFunction.apply, which the handleX helpers take, and no longer overlaps with the handleX helper names. Naming only; benchmark results are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ClassLatch is not on this branch; describe the contrast without naming it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
What Does This Do
Stops the Jackson 2.16 IAST instrumentation from throwing and reporting a
NoSuchFieldErroron every parsed JSON field name when the package-private Jackson fields it reads (UTF8StreamJsonParser._symbols,ByteQuadsCanonicalizer._interner) are missing.Each field read has its own
Latch(a small one-way, call-site-wide latch added here ininternal-api):JsonParser216Helperlatches the_symbolsread, andByteQuadsCanonicalizer216Helperlatches the_internerread.NoSuchFieldErrorfor each field is rethrown, so the instrumentation exception handler still reports it. This is not an exactly-once guarantee: threads that race the first failure each rethrow, so a failure is reported at least once, bounded by concurrency. The aim is to stop throwing forever, not to report exactly once.true("field names are interned") without throwing.Adds
Latch(plain flag, hint semantics; see its Javadoc) withLatchTestandLatchBenchmark, and extendsJsonParser216HelperTest(JUnit 5): interned names, non-interned names, a simulated missing_interner, a simulated missing_symbols, and a second class loader that must rethrow its own first failure (child-first class loader that renames the field in the bytecode).Design decision for IAST review
When the fields are missing we cannot tell whether names are interned, so we answer "interned".
NameAdvicethen records the current field name viasetCurrentNamebut does not taint the name string (taintNameis skipped). This is a deliberate trade-off:Stringtaints the one shared instance, so every occurrence of that name (across requests) would look tainted: a false-positive risk. Answering "not interned" would do exactly that.NameAdviceaborts before eithersetCurrentNameortaintName, so neither name tracking nor name taint works. This change adds current-name tracking and does not add name taint.The trade-off is also documented in the
JsonParser216HelperJavadoc. I'd like IAST to confirm that "assume interned" is the right default.Motivation
Instrumentation telemetry shows
NoSuchFieldErrorfromJsonParserInstrumentation$NameAdvice(about 2.9k events in 3 days, roughly 93% from one service on tracer 1.66.0, the rest a long tail on 1.56 to 1.63). I checked jackson-core 2.16.0 through 2.21.x:_symbolsand_internerexist in all of them, and theStreamWriteConstraintsgate first matches in 2.16.0 together with_interner. So a stock Jackson 2.16+ cannot produce this error, which points at a mixed or repackaged Jackson on the customer classpath.Additional Notes
Why a latch per field. A missing field fails the same way for every receiver at that site, so a single one-way flag per site is enough (no per-class state). One latch per field, rather than one shared flag, means a missing
_internerdoes not switch off the_symbolsread, and each field's first failure is reported separately, with a different top frame identifying which field is missing.API. The helpers call
handleNoSuchField(target, read)insideapply, thentryApplyOrNull/tryApplyOrDefault(symbols, Boolean.TRUE)at the call site.handleNoSuchFieldlatches onNoSuchFieldErrorand rethrows it, so the first failure is still reported; it takes a plainjava.util.function.Functionbecause a field read throws nothing checked.Latchhas no built-in default: a null result means nothing is available (skipped, or no value), and the caller chooses the fallback. For_internerthe boolean is computed inside the latch, so the result is never null; a null_interneris data ("not interned") and must not be confused with a skipped call.The failing call itself. The call that hits the failure is aborted by the advice's exception suppression before
setCurrentName, so that one field name is not tracked and a value read right after it may be attributed to no name, or to the previous one. Later calls are not affected. This is also in theJsonParser216HelperJavadoc.Flag is plain, not
volatile. The previous version used astatic volatile boolean.Latchuses a plain flag on purpose: a stale read only costs another failure, and a thread always sees its own write, so each thread pays for at most one extra failure after its own first. This is a per-field-name path, so the read cost matters.LatchBenchmark, added here, measures this exact shape: avolatileflag costs about 0.5 ns more than a plain one on the working path at depth 0 (4.04 against 3.55 ns), andLatchmatches a hand-rolled plain flag (see the benchmark note below).Benchmark.
LatchBenchmark(JMH; a realNoSuchFieldErroris built at setup, as inClassLatchBenchmark). One run on Zulu 17.0.7 (HotSpot), MacBook M1, single thread, 5 forks, on a laptop with normal background activity (load about 4); the full table is in the benchmark's Javadoc. JDK 8 and x86 are not measured. The numbers were measured on the commit before the hook was renamedapplyand the public methodstryApply*; that rename is naming only.Latch, skippedvolatileflag, skippedLatchvolatileflagA latched skip is about 1,600 times cheaper than the status quo at depth 0 and about 180 times at depth 50, and allocates nothing.
Latchis as cheap as a hand-rolled plain flag, so the abstraction costs nothing measurable, and avolatileflag costs more (about 0.5 ns on the working path and about 0.3 ns when skipping, at depth 0). At depth 50 only the skipped arms are reliable (28 to 30 ns, consistent across forks): the working-path arms span 31 to 39 ns, and the plain flag, which is the same logic asLatch, came out slowest with one fork at 28.9M against 24.2M to 24.8M ops/s for the others, so I read that as JIT and recursion noise, not a difference between the designs.Null
_symbols. If the_symbolsread ever returned null, the answer is now "assume interned" where the old code would have thrown an NPE._symbolsis never null in stock Jackson.Class loaders and lambdas. The helpers are injected into the application class loader, now reference
datadog.trace.util.Latchfrominternal-api, and contain two small lambdas (parser -> parser._symbols,s -> s._interner != null); muzzle passes across the Jackson versions it checks.The design of
Latchand its per-class sibling is tracked in APMLP-1894 (the sibling is in Skip repeated AbstractMethodError from JDBC getClientInfo #12702).The root cause on the affected classpath is not confirmed: the telemetry
error.messageis empty, so the missing field name is unknown. This change bounds the cost of the failure and does not fix the classpath.The failure simulation is synthetic; I have not seen the customer's real classpath.
Only
NoSuchFieldErroris handled. Other linkage errors still propagate as before.Existing Groovy IAST tests in the module pass locally.
Contributor Checklist
type:and (comp:orinst:) labels in addition to any other useful labelsclose,fix, or any linking keywords when referencing an issueUse
solvesinstead, and assign the PR milestone to the issueJira ticket: APMLP-1894
🤖 Generated with Claude Code