Skip to content

Skip repeated AbstractMethodError from JDBC getClientInfo - #12702

Draft
dougqh wants to merge 9 commits into
masterfrom
dougqh/abstract-method-guard
Draft

dougqh wants to merge 9 commits into
masterfrom
dougqh/abstract-method-guard

Conversation

@dougqh

@dougqh dougqh commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

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 for Connection.getClientInfo() in JDBCDecorator.parseDBInfoFromConnection. Once a class is known to lack the method, the call is skipped instead of throwing and catching an AbstractMethodError on every call.

A call site holds a static final subclass and calls tryGetOrNull:

private static final ClassLatch<Connection, Properties, SQLException> CLIENT_INFO =
    new ClassLatch<Connection, Properties, SQLException>() {
      @Override
      protected Properties get(Connection connection) throws SQLException {
        return handleAbstractMethod(connection, Connection::getClientInfo);
      }
    };

clientInfo = CLIENT_INFO.tryGetOrNull(connection);

The catch around the call narrows from Throwable to SQLException, still logged with EXCLUDE_TELEMETRY and still falling back to a URL-only DBInfo. Unexpected failures now reach the outer handler, which logs without EXCLUDE_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 an AbstractMethodError. #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

  • tryGetOrNull is public final: skip if the target is null or latched, otherwise call get. A null result 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 implement get in an ordinary try/catch (so checked exceptions such as SQLException just 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. fn is a ThrowingFunction<T, R, E>, a new top-level interface in datadog.trace.api.function next to TriFunction and TriConsumer, so a method that throws a checked exception (such as getClientInfo) can be passed as a method reference. AbstractMethodError and UnsupportedOperationException both yield null. Only AbstractMethodError can latch, and only when its message names the key class, so a wrapper delegating to a deficient object is never latched. UnsupportedOperationException names no class, so it is caught on every call. Anything else propagates.
  • keyOf chooses 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.
  • State is an eager final ClassValue (safely published, no creation race) behind a plain anyLatched flag, 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 than volatile on the working path.
  • Both HotSpot message formats are parsed: JDK 8 (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): AbstractMethodError 83.5%, SQLFeatureNotSupportedException 14.4%, other SQLException 1.3%, UnsupportedOperationException 0.8%. About 85% of the AbstractMethodError events 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)

Benchmark. ClassLatchBenchmark (JMH, a real AbstractMethodError built 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.

  • A latched class costs about 5 ns instead of about 4.5 µs, and allocates nothing (the status quo allocates 896 B at depth 0, 2,256 B at depth 50).
  • A wrapper, which cannot be latched, performs like the status quo.
  • On the working path the latch adds about 0.5 ns at depth 0. At depth 0 the method-reference form and a dedicated-subclass form are indistinguishable on the latched path.
  • At depth 50 the latched arms showed two stable per-fork JIT modes (about 85 ns and about 32 ns), so their means are not reported and the ranking of the two forms at that depth is not established. The cause was not investigated.
  • JDK 8 and x86 are not measured.

Tests. ClassLatchTest and HandleAbstractMethodTest cover latching, keyOf, wrapper non-latching, both message formats, prefix collisions, null target, checked/unchecked propagation, and a real AbstractMethodError built with the JDK compiler to pin the message format (pass on JDK 8, 11, 17 and 21). ParseDBInfoClientInfoTest: a failing getClientInfo (SQLException, UnsupportedOperationException, AbstractMethodError) still yields URL-based DBInfo; an unexpected exception falls back to DBInfo.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

Jira ticket: APMLP-1894

🤖 Generated with Claude Code

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 dougqh added type: bug fix Bug fix tag: no release notes Changes to exclude from release notes inst: jdbc JDBC instrumentation tag: ai generated Largely based on code generated by an AI or LLM labels 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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To Claude - let's add StrategyConsumer & Strategy annotations

@datadog-prod-us1-3

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Sep 30, 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.00 s 13.95 s [-0.3%; +1.0%] (no difference)
startup:insecure-bank:tracing:Agent 13.00 s 13.10 s [-1.5%; -0.1%] (maybe better)
startup:petclinic:appsec:Agent 17.59 s 17.40 s [+0.2%; +2.0%] (maybe worse)
startup:petclinic:iast:Agent 17.50 s 17.04 s [-1.7%; +7.1%] (no difference)
startup:petclinic:profiling:Agent 17.23 s 17.39 s [-2.0%; +0.2%] (no difference)
startup:petclinic:sca:Agent 16.89 s 17.44 s [-7.4%; +1.1%] (no difference)
startup:petclinic:tracing:Agent 16.68 s 16.65 s [-0.8%; +1.2%] (no difference)

Commit: b4045082 · 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.

dougqh and others added 4 commits September 30, 2026 12:23
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>
Comment thread internal-api/src/main/java/datadog/trace/util/ClassLatch.java
dougqh and others added 4 commits September 30, 2026 15:05
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>
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 =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To Claude, let's call this CLIENT_INFO_LATCH

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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inst: jdbc JDBC instrumentation tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes type: bug fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant