Conversation
Connections from old JDBC drivers lack getClientInfo, so every parseDBInfoFromConnection call threw and caught an AbstractMethodError. #11412 muted the log line but not the throw. Add AbstractMethodGuard: a per-call-site object that treats AbstractMethodError and UnsupportedOperationException as "not supported" (returns null) and lets everything else, SQLException included, propagate through a type parameter. A class is latched, so later calls skip the call, only when the error message names exactly the receiver class, so a wrapper delegating to a deficient driver is never latched. Both HotSpot message formats (JDK 8 and 11+) are recognised. Anything unattributable keeps today's behaviour. The JDBC call site now narrows its catch from Throwable to SQLException, so unexpected failures reach the outer handler and stay visible. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
🟢 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. |
Benchmark the guard against the status quo (throw and catch on every call), a wrapper that cannot be latched, and the working path, using a real AbstractMethodError built at setup. Replace the lazily created ClassValue with an eager final field, and gate the per-class lookup with a plain anyLatched flag. This removes the creation race (a lost update could discard every latch so far) and gives safe publication through the final field. A plain flag measured faster than a volatile one on the working path. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Make isLatched package-private (only tests and the benchmark use it) and have invokeOrNull call it so the check has a single definition. Move CLIENT_INFO_GUARD next to the other private statics in JDBCDecorator. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replace AbstractMethodGuard with two abstract types meant to be held in static final fields, one per call site: - Latch: a one-way, call-site-wide latch, for failures that are the same for everyone (e.g. a field missing from the classes on the classpath). - ClassLatch: a per-class latch keyed through an overridable keyOf, for failures that recur for every instance of a class. The per-class state is an eager final ClassValue behind a plain anyLatched flag, so it is safely published and cannot lose latches to a creation race. Subclasses implement get in an ordinary try/catch, so checked exceptions need no generics tricks, and change the state only through protected helpers (latch, unlatch, latchIfNamed). A protected higher-order handleAbstractMethod covers the common case: AbstractMethodError is latched only when its message names the key class, and UnsupportedOperationException is swallowed without latching. JDBCDecorator now holds a static final ClassLatch for getClientInfo; behaviour is unchanged. The benchmark is renamed to ClassLatchBenchmark, gives each arm its own method, and adds a comparison against a dedicated-subclass form. Its results are provisional: depth-50 latched arms showed per-fork JIT modes and a cleaner run with more forks is planned. 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>
Drop the defaultValue hook from ClassLatch. The public methods are now tryGetOrNull (skip if the target is null or 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. handleAbstractMethod returns null on an unsupported call. 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>
Replace the nested ClassLatch.Call with a top-level ThrowingFunction next to the other functional interfaces (TriFunction, TriConsumer), so other toolbox types can share it. Behaviour is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Annotate ClassLatch.handleAbstractMethod with @StrategyConsumer and its function parameter with @strategy, as ConcurrentHashtable does, and note in its Javadoc that callers should pass a method reference or a non-capturing lambda. Documentation and tooling markers only; behaviour is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Name the helpers after the JVM errors they handle, as handleNoSuchField does: handleAbstractMethod (AbstractMethodError), handleNoSuchMethod (NoSuchMethodError) and handleNoSuchOrAbstractMethod (both), and cross-link their Javadocs. The two errors are easy to confuse, so the combined helper is the documented default. A NoSuchMethodError names the declared type, not the receiver, and can come from a call made inside one receiver's implementation, so it latches the target's key and never the whole call site. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
Record a five-fork run on Zulu 17 (M1) in the benchmark's Javadoc: a latched class against the status quo, a wrapper that cannot latch, and the working path, at two stack depths, with the method-reference form compared to a dedicated subclass. The two forms are indistinguishable. Notes one unexplained result: at depth 50 a latched skip is slower than a call that runs. 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>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Narrowing the catch to SQLException broke existing tests: TestConnection's getClientInfo throws a bare Throwable, and JDBCInstrumentationV0Test and JDBCWrappedInterfacesTest expect the URL-derived DB info to survive any failure of getClientInfo. Restore the old resilience at the call site, so the latch is purely an optimization, and update ParseDBInfoClientInfoTest to assert that any failure still yields URL-based DBInfo. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Two distinct correctness issues exist in ClassLatch.java: a NoSuchMethodError can escape the catch block if a method reference is resolved before the helper is entered, and the latch incorrectly keys on the receiver class alone rather than the specific failing method, causing unrelated valid calls to be permanently suppressed.
🤖 Bits Code Review · Commit d176106 · @DataDog review to ask questions
| && message.length() > end | ||
| && message.charAt(end) == ' '; | ||
| } | ||
| if (message.startsWith(name) && message.length() > name.length() + 1) { |
There was a problem hiding this comment.
I kind of hate this, but it makes the latching mechanism safer.
ygree
left a comment
There was a problem hiding this comment.
The provided ClassLatch is a flexible and thoughtful abstraction for handling various errors. However, many of the provided methods are not used for the intended use case.
I will approve it assuming it will have more applications than just the JDBCDecorator use case.
|
Yeah, I'm trying to find the right balance for these toolbox PRs. And yes, we do have a few other cases for ClassLatch-es mostly more AbstractMethodError-s, so we'll have to see how this all pans out. |
… method-name check tryApplyOrNull previously only caught NoSuchMethodError, missing the BootstrapMethodError-wrapped form that some JVM versions raise for the same linkage failure, which let it escape the latch uncaught. handleAbstractMethod/latchIfNamed/isNamedIn now also take the method name the call site expects, since a receiver's own implementation can raise an AbstractMethodError naming the key class while blaming a different method it calls internally; without the check that error would incorrectly latch the class for a method it does implement. Extracts the duplicated compile() test helper into CompilingClassLoaders. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the per-call AbstractMethodError catch (JMS <=1.1 producers lack getDestination) with a ClassLatch so the error is paid once per class instead of once per call. Distinguishes a producer's legitimate null destination from a latched/unavailable method via isLatched, since both can otherwise look identical. Part of APMLP-1895; stacked on #12702 (ClassLatch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A latched target, and the call that fails, now yield fallback(target) instead of a bare null. Call sites with a slower alternative (an older API) can put it in the latch rather than re-checking isLatched after tryApplyOrNull, which also removes the ambiguity between a skipped call and an operation that legitimately returned null. The default is null, so existing latches are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the per-call AbstractMethodError catch (JMS <=1.1 producers lack getDestination) with a ClassLatch so the error is paid once per class instead of once per call. Distinguishes a producer's legitimate null destination from a latched/unavailable method via isLatched, since both can otherwise look identical. Part of APMLP-1895; stacked on #12702 (ClassLatch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
With an overridable fallback the result is no longer necessarily null when the operation is skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the per-call AbstractMethodError catch (JMS <=1.1 producers lack getDestination) with a ClassLatch so the error is paid once per class instead of once per call. Distinguishes a producer's legitimate null destination from a latched/unavailable method via isLatched, since both can otherwise look identical. Part of APMLP-1895; stacked on #12702 (ClassLatch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
What Does This Do
Adds
ClassLatch, a small abstract type for calls that fail the same way every time for a given class, and uses it forConnection.getClientInfo()inJDBCDecorator.parseDBInfoFromConnection. Once a class is known to lack the method, the call is skipped instead of throwing and catching anAbstractMethodErroron every call.A call site holds a
static finalsubclass and callstryApply:The catch around the call is unchanged: it still catches
Throwable, logs withEXCLUDE_TELEMETRY, and falls back to a URL-onlyDBInfo, so the latch is purely an optimization and the behavior is the same as before. (An earlier revision of this PR narrowed the catch toSQLException, which broke existing tests:TestConnection.getClientInfo()throws a bareThrowable, andJDBCInstrumentationV0TestandJDBCWrappedInterfacesTestexpect the URL-derived DB info to survive any failure. Narrowing it, so that an unexpected failure reaches the outer handler and shows up in Error Tracking, is a separate decision and would need those tests changed.)Motivation
Connections from old JDBC drivers and pool proxies lack
getClientInfo, so every call threw and caught anAbstractMethodError. #11412 muted the log line (telemetry dropped to zero from 1.63), but the throw and catch still happen on every call. See Error Tracking issue b449bd64, which was ~650k events/day from old tracers.The JVM does not fast-throw
AbstractMethodError(that optimisation covers only a fixed set of implicit exceptions), and it cannot cache the failure on the call site because the error comes from method selection, which depends on the receiver. The latch memoises per call site and receiver class, and skips the call entirely when it can prove the class lacks the method.Additional Notes
Design
tryApply(formerlytryApplyOrNull, renamed once the result was no longer necessarilynull) ispublic final: returnsnullfor a null target,fallback(target)for a latched one, otherwise callsapply. Anullresult always means "nothing available," whether skipped or genuinely absent, so the caller's fallback behaves the same either way;tryApplyOrDefaultis null-coalescing sugar over it. No wrapper result type — a null return needs no allocation. Subclasses writeapplyas an ordinarytry/catchand touch state only vialatch/unlatch/latchIfNamed.fallback(target)is an overridable hook: what a latched target yields instead of the operation, and what thehandle*helpers return when the call fails, so the failing call and every skipped call after it agree. It defaults tonull, which is what JDBC uses. Override it when there is a slower alternative (e.g. an older API, as in Convert JMSDecorator.getDestination to ClassLatch #12720's JMSgetQueue/getTopic), instead of re-checkingisLatchedat the call site to tell a skipped call from an operation that legitimately returnednull.handleAbstractMethod(target, methodName, fn)is the common-case helper, taking a newThrowingFunction<T, R, E>(alongsideTriFunction/TriConsumer) so a throwing method reference likegetClientInfocan be passed directly. BothAbstractMethodErrorandUnsupportedOperationExceptionyieldfallback(target), but onlyAbstractMethodErrorlatches, and only when its message names the key class andmethodName— a wrapper delegating to a deficient object still gets the fallback, but is never latched. Anything else propagates.handleNoSuchMethodandhandleNoSuchOrAbstractMethodare also provided but unused so far (the JDBC site useshandleAbstractMethod).handleNoSuchMethodlatches the target's key rather than the whole site, sinceNoSuchMethodErrorcan originate inside one receiver's implementation.keyOfpicks the class the latch is keyed on (default: the target's class) — override it when the target is a wrapper around the actually-deficient object.final ClassValuebehind a plain (non-volatile)anyLatchedflag: one flag read on the common path; a stale read just costs one extra failure, since each thread always sees its own write.AbstractMethodErrormessage formats, verified on real JVMs (8, 11, 17, 21, 25, GraalVM 21). An unparseable message means no latch, never a wrong one.Observed traffic. Last 3h of "Could not get client info from DB" (~98.8k events, mostly tracer <1.63): 83.5%
AbstractMethodError, 14.4%SQLFeatureNotSupportedException, 1.3% otherSQLException, 0.8%UnsupportedOperationException. ~85% of theAbstractMethodErrorevents have a non-Datadog top frame — likely a wrapper/pool proxy delegating to a deficient object (inferred from stack shape, not confirmed). This PR's latch isn't keyed on the leaf, so it only covers the direct-call minority; wrapped connections are unaffected.Not in this PR (tracked in APMLP-1894)
keyOffor pooled connections, to cover the dominant wrapped case.SQLFeatureNotSupportedException(needs an attribution approach first).Latch, the call-site-wide sibling; lands with its first user (Stop repeated NoSuchFieldError in Jackson 2.16 IAST interner lookup (quick fix) #12670).Benchmark.
ClassLatchBenchmark(JMH), Zulu 17.0.7 on an M1, single thread, 5 forks. Full table in the benchmark's Javadoc; JDK 8/x86 not measured. Error margins 0.1–3.4%.handleAbstractMethodLatch(Stop repeated NoSuchFieldError in Jackson 2.16 IAST interner lookup (quick fix) #12670) doesn't show this (~28 ns skip), so it's specific to this per-class path (ClassValue.get/keyOf) or to how this benchmark's recursion inlines it — not investigated further.Tests.
ClassLatchTest/HandleAbstractMethodTest/HandleNoSuchMethodTestcover latching,fallback(on the failing call, once latched, for an unattributed error, never for a realnullor a null target),keyOf, wrapper non-latching, both message formats, collisions, null target, and checked/unchecked propagation, including a realAbstractMethodErrorbuilt with the JDK compiler (JDK 8, 11, 17, 21).ParseDBInfoClientInfoTestconfirms URL-basedDBInfosurvives anygetClientInfofailure. Existing JDBC tests (JDBCInstrumentationV0Test,JDBCWrappedInterfacesTest,IastJDBCTest) pass on both regular and latest-dependency tasks; the Docker-backedRemoteJDBCInstrumentationV0Testwasn't run locally.CODEOWNERS. New files fall under the already-owned
/internal-api/src/*/*/datadog/trace/util/path — no change needed.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