Skip to content

bench: first 4-runtime results + disable Reactor row (upstream kafka-clients 4.x incompat)#133

Merged
eschizoid merged 4 commits into
mainfrom
bench/results-2026-05-16
May 17, 2026
Merged

bench: first 4-runtime results + disable Reactor row (upstream kafka-clients 4.x incompat)#133
eschizoid merged 4 commits into
mainfrom
bench/results-2026-05-16

Conversation

@eschizoid

Copy link
Copy Markdown
Owner

Real numbers from the Testcontainers harness

Three of four runtimes produced clean JMH measurements across the full `workMicros` sweep:

Runtime `workMicros=0` `workMicros=100` `workMicros=1000`
KPipe 553,704 ± 54,924 500,903 ± 95,758 495,803 ± 41,069
Raw `KafkaConsumer` + VT 570,890 ± 38,369 526,568 ± 44,444 498,016 ± 51,323
Confluent Parallel Consumer 107,877 ± 8,892 112,985 ± 2,653 66,454 ± 2,963
Reactor Kafka

Records/sec, higher is better. Each cell is `Score ± error` across forks=2 × iter=5.

Findings

  • KPipe ≈ raw `KafkaConsumer + VT` across the full sweep, within error bars. Framework overhead on the hot path is statistically invisible.
  • Both Loom-based runtimes are 4.4×–7.5× faster than Confluent Parallel Consumer. The gap widens at `workMicros=1000` where Confluent's 100-worker thread pool starts blocking on the simulated I/O wait.
  • Confluent allocates least (~34 B/op) and is consistent across the sweep. KPipe allocates most (~923–1530 B/op) due to `Result` wrappers + per-record VT spawn; at the throughput shown above the JVM's young gen handles it without throughput dip.

Reactor Kafka — verified upstream gap

Reactor Kafka 1.3.23 (latest stable on Maven Central) AND 1.4.0-M1 / 1.4.0-RC1 from the Spring milestone repo ALL still pin `kafka-clients:3.6.0`. Running against our 4.2.0 classpath crashes at first fetch with:

```
java.lang.NoSuchMethodError: 'void org.apache.kafka.clients.consumer.ConsumerRecord.(...)'
at reactor.kafka.receiver.ReceiverRecord.(ReceiverRecord.java:48)
```

The `ConsumerRecord` ctor shape was removed in kafka-clients 4.x. Reactor Kafka has not shipped a 4.x-compatible build yet.

The `@Benchmark` methods for Reactor are now commented out in both `ParallelProcessingBenchmark` and `ParallelProcessingLatencyBenchmark` so the bench doesn't waste 6+ min timing out on Reactor every cell. The `ReactorInvocationContext` and the `reactor-kafka` dep stay compiled — re-enabling is one-line when upstream catches up.

Per discussion: removed the `exclude(kafka-clients)` workaround on the `reactor-kafka` dependency. Doesn't change runtime behaviour (Gradle conflict-resolves to `4.2.0` regardless), and the comment now explains the situation honestly instead of hiding it.

Files

  • `benchmarks/results/2026-05-16.md` — environment, JMH config, full throughput + allocation + GC tables, Pitfalls section.
  • `benchmarks/results/2026-05-16.json` — raw JMH JSON, 64 KB, committed for reproducibility.

…eam incompat)

Three of four runtimes produced clean JMH measurements across the full workMicros sweep
against the Testcontainers Kafka 4.2.0 broker. Headline (records/sec, ± error from
forks=2 × iter=5):

                            workMicros=0       =100         =1000
  KPipe                    553,704 ± 54,924   500,903   495,803
  Raw KafkaConsumer + VT   570,890 ± 38,369   526,568   498,016
  Confluent PC             107,877 ±  8,892   112,985    66,454

  KPipe ≈ raw within error → framework overhead is statistically invisible.
  Both Loom runtimes 4.4×–7.5× ahead of Confluent PC across the sweep.

Reactor Kafka row CRASHED at first record fetch with NoSuchMethodError on
ConsumerRecord.<init> — Reactor Kafka 1.3.23 (latest stable on Maven Central) AND
1.4.0-M1 / 1.4.0-RC1 on the Spring milestone repo ALL pin `kafka-clients:3.6.0` whose
ConsumerRecord ctor shape was removed in `kafka-clients:4.x`. No upstream Kafka-4.x build
exists yet. Disabling the Reactor @benchmark methods (in both throughput and latency
companion); the ReactorInvocationContext stays compiled so re-enabling is one-line when
upstream catches up.

Also dropped the `exclude kafka-clients` workaround on the reactor-kafka dependency —
removing it does not change runtime behaviour (Gradle conflict-resolves to 4.2.0 anyway)
and the comment now explains the situation honestly.

Snapshot doc: `benchmarks/results/2026-05-16.md` with environment, JMH config, full
throughput + allocation + GC tables, and a "Pitfalls" section calling out the Reactor
limitation. Raw JSON committed alongside for reproducibility.
@codecov

codecov Bot commented May 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.60%. Comparing base (38d1671) to head (939c4a3).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #133      +/-   ##
============================================
- Coverage     70.68%   70.60%   -0.08%     
  Complexity      623      623              
============================================
  Files            68       68              
  Lines          2521     2521              
  Branches        323      323              
============================================
- Hits           1782     1780       -2     
- Misses          575      578       +3     
+ Partials        164      163       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

eschizoid added 3 commits May 16, 2026 16:26
Reactor Kafka shipped 1.3.25 on 2025-11-06 with the fix for issue #420 — the
ReceiverRecord constructor now calls a ConsumerRecord ctor that exists in both
kafka-clients 3.x AND 4.x (avoids the deprecated Long-checksum ctor that was
removed in 4.0). The dependency's POM still pins kafka-clients:3.9.1 but the
new binary is forward-compatible.

Verified via:
  * Bytecode inspection of the rebuilt fat-jar: ReceiverRecord now invokes
    `ConsumerRecord.<init>(String, int, long, long, TimestampType, int, int,
    Object, Object, Headers, Optional)` — the new 11-arg signature that exists
    in kafka-clients 4.x.
  * Smoke test (`workMicros=0`, single iteration, single fork) produced a clean
    score of ~300k ops/s — Reactor now consumes records end-to-end instead of
    crashing with NoSuchMethodError at first fetch.

Re-enables the `reactor` @benchmark methods in both throughput and latency
companion, and the build.gradle.kts comment now records the working version
rather than describing the broken one.

Note: the initial smoke didn't appear to work because the fat JMH jar was
caching the old (1.3.23) ReceiverRecord bytecode despite the dependency bump.
A clean `rm -rf benchmarks/build` + `./gradlew :benchmarks:jmhJar
--rerun-tasks` rebuilt the jar with the correct 1.3.25 bytecode. Mention this
in METHODOLOGY (next commit) so anyone bumping a transitive dep knows to
force-clean.
… cache gotcha

Post-run update to the 2026-05-16 snapshot's Pitfalls section: the original Reactor
NoSuchMethodError was resolved by bumping to 1.3.25 (which avoids the deprecated
ConsumerRecord ctor removed in kafka-clients 4.x). Smoke-tested at workMicros=0 and got
~300k ops/s. Full Reactor sweep is pending a fresh run.

Also records the JMH fat-jar caching gotcha: after a transitive version bump, force-clean
benchmarks/build and add --rerun-tasks to jmhJar. Otherwise the fat-jar can keep the old
bytecode and the smoke test will appear to still fail.
@eschizoid eschizoid merged commit bd45016 into main May 17, 2026
1 of 2 checks passed
@eschizoid eschizoid deleted the bench/results-2026-05-16 branch May 17, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant