[demo] Add Bucket4j rate-limiter instrumentation#11903
Conversation
Traces bucket4j rate-limit checks: one span per Bucket#tryConsume with tokens/consumed tags and a rate tier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <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. |
There was a problem hiding this comment.
From Claude perf rubric...
Perf review (rubric: perf-review-checks.md)
Scope: Bucket4jDecorator.onConsume — runs on every Bucket#tryConsume call (tracing hot path; multiplier = per-call × calls/sec).
Confirmed findings
1. Per-call config lookup + Set allocation
InstrumenterConfig.get().isIntegrationEnabled(singleton("bucket4j-tier"), true)Collections.singleton(...) allocates a new SingletonSet every call, plus a config lookup, for a value that doesn't change per-call.
- Rubric check #2 (repeat work on invariant input) — SEV-2/3
- Fix: hoist to a
static final boolean(or cache in a field) computed once.
2. Arrays.stream(...).filter(...).findFirst() in the hot path
Builds a Stream pipeline (Stream + Spliterator + pipeline stages + captured lambda) every call just to find the first threshold ≥ tokens.
- Rubric check #1/#5 (per-call allocation + unnecessary indirection) — SEV-3
- Fix: plain
forloop overTIER_THRESHOLDS, no Stream.
3. Objects.hash(bucket, tokens, consumed) in onConsume
Varargs Object[] allocation + boxing of tokens (long) and consumed (boolean) on every call, unconditionally.
- Rubric check J9 — SEV-2/3
- Fix:
datadog.trace.util.HashingUtils(no boxing, no array).
4. Eager string concatenation in LOGGER.debug(...)
LOGGER.debug("bucket4j tryConsume tokens=" + tokens + " consumed=" + consumed + " bucket=" + bucket);Builds the string unconditionally, even when debug logging is disabled.
- Rubric check #2/J10 — SEV-2/3
- Fix: SLF4J parameterized form, or guard with
isDebugEnabled().
Correctly not flagged
DEFAULT_LIMIT_KEY = Objects.hash("default", 100L) — same Objects.hash pattern as #3, but this one runs once at class-init (cold path). Per the rubric's precision-over-recall posture, staying silent here.
Posted per the AI-review rubric demo — no action required for this closed/demo PR.
dougqh
left a comment
There was a problem hiding this comment.
Perf review per perf-review-checks.md rubric — 4 hot-path findings inline below. onConsume runs on every Bucket#tryConsume call, so multiplier = per-call × calls/sec.
|
|
||
| public void onConsume(AgentSpan span, Bucket bucket, long tokens, boolean consumed) { | ||
| long tier = -1L; | ||
| if (InstrumenterConfig.get().isIntegrationEnabled(singleton("bucket4j-tier"), true)) { |
There was a problem hiding this comment.
Per-call config lookup + Set allocation. Collections.singleton("bucket4j-tier") allocates a new SingletonSet every call, plus a config lookup, for a value that doesn't change per-call.
- Rubric check First PR - Implements the Opentracing API #2 (repeat work on invariant input) — SEV-2/3
- Fix: hoist to a
static final boolean(or cache in a field) computed once.
| long tier = -1L; | ||
| if (InstrumenterConfig.get().isIntegrationEnabled(singleton("bucket4j-tier"), true)) { | ||
| tier = | ||
| Arrays.stream(TIER_THRESHOLDS) |
There was a problem hiding this comment.
Stream pipeline in the hot path. Arrays.stream(...).filter(...).findFirst() builds a Stream + Spliterator + pipeline stages + captured lambda every call just to find the first threshold ≥ tokens.
- Rubric check changing List<Span> for List<DDSpan> #1/Gpolaert/dev #5 (per-call allocation + unnecessary indirection) — SEV-3
- Fix: plain
forloop overTIER_THRESHOLDS, no Stream.
| span.setTag("bucket4j.tier", tier); | ||
| } | ||
|
|
||
| int metricKey = Objects.hash(bucket, tokens, consumed); |
There was a problem hiding this comment.
Objects.hash varargs/boxing on the hot path. Objects.hash(bucket, tokens, consumed) allocates an Object[] and boxes tokens/consumed on every call, unconditionally.
- Rubric check J9 — SEV-2/3
- Fix:
datadog.trace.util.HashingUtils(no boxing, no array).
(Not flagging DEFAULT_LIMIT_KEY = Objects.hash("default", 100L) above at line 27 — that one runs once at class-init, cold path.)
| } | ||
|
|
||
| LOGGER.debug( | ||
| "bucket4j tryConsume tokens=" + tokens + " consumed=" + consumed + " bucket=" + bucket); |
There was a problem hiding this comment.
Eager string concatenation in LOGGER.debug(...). The string is built unconditionally (StringBuilder + bucket.toString()), even when debug logging is disabled.
- Rubric check First PR - Implements the Opentracing API #2/J10 — SEV-2/3
- Fix: SLF4J parameterized form
LOGGER.debug("bucket4j tryConsume tokens={} consumed={} bucket={}", tokens, consumed, bucket), or guard withisDebugEnabled().
| private static final long[] TIER_THRESHOLDS = {1L, 10L, 100L, 1_000L, 10_000L}; | ||
|
|
||
| // stable id for the default bandwidth profile; computed once at class load | ||
| private static final int DEFAULT_LIMIT_KEY = Objects.hash("default", 100L); |
There was a problem hiding this comment.
Objects.hash called - but only once during class initialization, so not alerted.
This PR exists to demo the AI-assisted performance-review rubric for the Java build-team talk. The instrumentation is intentionally written with several hot-path anti-patterns in
Bucket4jDecorator.onConsume(which runs on everyBucket#tryConsume):InstrumenterConfig.get().isIntegrationEnabled(singleton(...))— config lookup + a fresh set allocation every callArrays.stream(...).filter(...).findFirst()in the hot pathObjects.hash(...)— varargs array + boxing every callLOGGER.debug("..." + ... + bucket)string building…alongside a benign one-time use of the same
Objects.hashpattern in cold static init (buildKnownLimitKeys).The point of the demo: the reviewer should flag the hot-path costs and stay silent on the cold one — situational awareness, not a linter.
🤖 Generated with Claude Code