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>
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 <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>
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 <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>
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.
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 callstryGetOrNull:The catch around the call narrows from
ThrowabletoSQLException, still logged withEXCLUDE_TELEMETRYand still falling back to a URL-onlyDBInfo. Unexpected failures now reach the outer handler, which logs withoutEXCLUDE_TELEMETRY, so a new failure shape becomes visible in Error Tracking instead of being muted.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
tryGetOrNullispublic final: skip if the target is null or latched, otherwise callget. Anullresult means nothing is available (skipped, unsupported, or no value), so the caller chooses the fallback;tryGetOrDefault(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 implementgetin 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.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 realAbstractMethodErrorbuilt at setup; Zulu 17.0.7, M1, single thread; results in its Javadoc). The numbers are provisional: a cleaner run with more forks is planned.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) still yields URL-basedDBInfo; an unexpected exception falls back toDBInfo.DEFAULT.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