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 <[email protected]>
dougqh
commented
Sep 30, 2026
| * {@code null} target also returns {@code null}, without latching. | ||
| */ | ||
| @Nullable | ||
| public <T, R, E extends Exception> R invokeOrNull(@Nullable T target, Call<T, R, E> call) |
Contributor
Author
There was a problem hiding this comment.
To Claude - let's add StrategyConsumer & Strategy annotations
This comment has been minimized.
This comment has been minimized.
Contributor
🟢 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
dougqh
commented
Sep 30, 2026
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
dougqh
commented
Sep 30, 2026
| private static final Logger log = LoggerFactory.getLogger(JDBCDecorator.class); | ||
|
|
||
| /** Old drivers and pool proxies may not implement getClientInfo at all. */ | ||
| private static final ClassLatch<Connection, Properties, SQLException> CLIENT_INFO = |
Contributor
Author
There was a problem hiding this comment.
To Claude, let's call this CLIENT_INFO_LATCH
dougqh
commented
Sep 30, 2026
| clientInfo = CLIENT_INFO.tryGetOrNull(connection); | ||
| } catch (final SQLException ex) { | ||
| // getClientInfo is not allowed, we can still extract info from the url alone | ||
| log.debug(LogCollector.EXCLUDE_TELEMETRY, "Could not get client info from DB", ex); |
Contributor
Author
There was a problem hiding this comment.
With the latch handling AbstractMethodError, I'm not sure that we need to EXCLUDE_TELEMETRY anymore. I'm curious what others think.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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 <[email protected]>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 callstryApplyOrNull: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
tryApplyOrNullispublic final: skip if the target is null or latched, otherwise callapply. Anullresult means nothing is available (skipped, unsupported, or no value), so the caller chooses the fallback;tryApplyOrDefault(target, fallback)is null-coalescing sugar over it, so a call that yields nothing and a skipped call always agree. There is no built-in default and no wrapper result type: a null return needs no allocation and no escape analysis. Subclasses implementapplyin an ordinarytry/catch(so checked exceptions such asSQLExceptionjust work) and change the state only through protected helpers:latch,unlatch,latchIfNamed.handleAbstractMethod(target, fn)is a protected higher-order helper for the common case.fnis aThrowingFunction<T, R, E>, a new top-level interface indatadog.trace.api.functionnext toTriFunctionandTriConsumer, so a method that throws a checked exception (such asgetClientInfo) can be passed as a method reference.AbstractMethodErrorandUnsupportedOperationExceptionboth yieldnull. OnlyAbstractMethodErrorcan latch, and only when its message names the key class, so a wrapper delegating to a deficient object is never latched.UnsupportedOperationExceptionnames no class, so it is caught on every call. Anything else propagates.handleNoSuchMethod(NoSuchMethodError; latches the target's key, never the whole site, because its message names the declared type and it can come from a call inside one receiver's implementation) andhandleNoSuchOrAbstractMethod(both; the documented default, since the distinction is easy to miss) are also provided. Neither is used at a call site yet: the JDBC site useshandleAbstractMethod. All three are@StrategyConsumerwith a@Strategyfunction parameter.keyOfchooses the class the latch is keyed on (default: the target's class). Every operation uses it, so the check and the latch cannot disagree. Override it to key on the object that is actually deficient when the target is a wrapper.final ClassValue(safely published, no creation race) behind a plainanyLatchedflag, so the common path is one flag read and the per-class lookup only happens once something has been latched. The state is deliberately not atomic: a stale read only costs another failure, and a thread always sees its own write, so each thread pays for at most one failure after its own first. A plain flag measured faster thanvolatileon the working path.Impl.b()Ljava/lang/String;) and JDK 11+ (Receiver class Impl does not define...), checked on real JVMs (8, 11, 17, 21, 25, GraalVM 21). An unparseable message means no latch, never a wrong latch. Anything unattributable keeps today's behaviour: one caught throw per call.What the observed traffic looks like. Last 3h of "Could not get client info from DB" (~98.8k events, mostly tracer <1.63):
AbstractMethodError83.5%,SQLFeatureNotSupportedException14.4%, otherSQLException1.3%,UnsupportedOperationException0.8%. About 85% of theAbstractMethodErrorevents have a non-Datadog top frame, which suggests a wrapper or pool proxy delegating to a deficient object (inference from stack shape, not confirmed). This PR's latch is not keyed on the leaf, so it covers the direct minority; wrapped connections keep today's behaviour.Not in this PR (design in APMLP-1894)
keyOffor pooled connections, so the dominant wrapped case can also take the fast path.SQLFeatureNotSupportedException(needs an attribution approach; measure first).Latch, the one-way call-site-wide sibling; it goes in with its first user (Stop repeated NoSuchFieldError in Jackson 2.16 IAST interner lookup (quick fix) #12670).Benchmark.
ClassLatchBenchmark(JMH; a realAbstractMethodErroris built at setup). One run on Zulu 17.0.7 (HotSpot), MacBook M1, single thread, 5 forks (20 measurement iterations per arm), 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. Error margins are 0.1% to 3.4%.handleAbstractMethodLatch(Stop repeated NoSuchFieldError in Jackson 2.16 IAST interner lookup (quick fix) #12670) does not show it: its skip at depth 50 is about 28 ns. So it is specific to the per-class path (ClassValue.getandkeyOf), or to how this benchmark's recursion inlines it. It was not investigated. An earlier, shorter run had one fork near 32 ns for this arm, which did not reproduce in these five.Tests.
ClassLatchTestandHandleAbstractMethodTestcover latching,keyOf, wrapper non-latching, both message formats, prefix collisions, null target, checked/unchecked propagation, and a realAbstractMethodErrorbuilt with the JDK compiler to pin the message format (pass on JDK 8, 11, 17 and 21).ParseDBInfoClientInfoTest: a failinggetClientInfo(SQLException,UnsupportedOperationException,AbstractMethodError, and any otherThrowable, including a bare one) still yields URL-basedDBInfo. The existingJDBCInstrumentationV0Test,JDBCWrappedInterfacesTestandIastJDBCTestpass on both the regular and latest-dependency tasks; the Docker-backedRemoteJDBCInstrumentationV0Testcannot run locally.CODEOWNERS. The new files are under
/internal-api/src/*/*/datadog/trace/util/, which is already owned (@DataDog/apm-java), so no change is 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