Skip to content

feat(client) astubbs#242: the Java reference client - one surface, two transports, and a handshake against a real sidecar - #386

Draft
astubbs wants to merge 7 commits into
feats/proxy-server-shellfrom
feats/proxy-java-client
Draft

astubbs wants to merge 7 commits into
feats/proxy-server-shellfrom
feats/proxy-java-client

Conversation

@astubbs

@astubbs astubbs commented Aug 31, 2026 •

Copy link
Copy Markdown
Owner

Part of #242.

depends on #384

Description

The Java reference client: one shared surface, an in-process transport, a gRPC transport that
speaks the frozen v1 wire, and a lane that proves the second one reaches a real sidecar. One
canonical client before the foreign ones - the shape ten other languages mirror, decided once, in
the language where it can be tested hardest.

The claim this PR makes, and what to check it against. The direct transport carries a record end
to end and applies its verdict: the offset moves and the produce payload lands, because a transport
that runs the user function and drops its answer looks identical from the processor's side. The gRPC
transport speaks the wire and its handshake genuinely reaches a real sidecar's session service -
which is a narrower claim than "it works", and is written down as the narrower claim. Both were
verified by breaking one term at a time; five sabotage arms with control arms either side are below.

Where this came from

Extracted from #293, the language-proxy god PR, as extraction A7 of
docs/plans/2026-08-31-001-process-god-branch-decomposition-plan.md - the rung above
#384, whose "deliberately NOT in this rung" list leaves exactly this. Taken
by path-scoped checkout rather than rewritten; every place it differs is named below.

The direct transport IS here, and that is the decision worth reading first

The dispatch order for this rung assumed the direct transport might need the sidecar's engine, and
would have to be left behind if it did. It does not. DirectParallelConsumerClient compiles
against parallel-consumer-core - ParallelEoSStreamProcessor, ParallelConsumerOptions,
RecordContext - and against nothing in parallel-consumer-proxy at all. "The engine" it drives is
core, which has been on master since before any of this existed; the sidecar's dispatch engine is
a different thing and this module has no way to reach it.

The evidence is not a reading of the imports. Its pom's bannedDependencies rule refuses protobuf,
gRPC and the protocol module on that classpath in every scope, and the module builds and its
tests pass with that ban in force. So the rung's claim does not have to shrink: both transports are
here, and the one that could not be finished without an engine - the engine-backed conformance run -
is the one that is left for A8.

What is in it

  • java-api - ParallelConsumerClient, ClientOptions, RecordProcessor and its asynchronous
    form, InboundRecord, OutboundRecord, Outcome, and the Outcomes bridge that turns every way
    a user function can misbehave into a verdict rather than a broken session. Dependency-free, and
    that is a product decision rather than tidiness: a ByteString or a ConsumerRecord here would not
    be a Java problem but a specification problem, because the mirroring languages have no such type.
    Two gates from opposite directions hold it - ClientSurfaceArchTest reads the bytecode and fires
    when a type is referenced; the direct sibling's enforcer ban reads the dependency tree and fires
    when a jar arrives.
  • java-direct - core bound in-process. The user's function runs on core's own worker threads
    and its Outcome maps onto core's success and retry paths, so ordering, retry and commit semantics
    are core's, untouched.
  • java-grpc - one bidirectional stream, Configure first, dispatched records to executors and
    their reports back. The client stores nothing per record: the fencing token rides from queue to
    report on the executor's stack and is echoed byte for byte, so a stateless client cannot have a
    state bug and every language mirroring it inherits that instead of inventing bookkeeping.
  • java-harness - not a product module. The one place on the JVM side that depends on the
    sidecar, so a client wrapping a transport does not drag it into the reactor.

Deliberately NOT in this rung

Named so neither side reads as an oversight: the conformance framework - the
transport-parameterised scenario suite, its fixture interface, the per-transport subclasses and core
as the control arm - is A8's, and A8 stacks on this branch. The engine-backed harness run and the
compile-scope harness classpath a JVM client declares to spawn a sidecar land in the harness module
when it arrives. The ten foreign clients are A9. Nothing here negotiates leases, heartbeats,
reconnect, worker-death reporting, terminal outcomes or the shutdown drain - java-grpc declares
["dispatch"] and nothing else, which is un-negotiated rather than half-built.

Five sabotage arms, with a control on either side

Each arm changes one term, runs the test that should break, and restores from a pristine copy;
the restored file was diffed byte-for-byte against the copy each time.

=== CONTROL: untouched tree, the handshake pair ===
Tests run: 2, Failures: 0 -- SidecarHandshakeTest
EXIT=0

=== ARM 1: the client declares an :authority the sidecar's allowlist does not list ===
SidecarHandshakeTest.theHandshakeReachesTheSessionServiceAndItsRefusalReachesTheCaller
  expected: UNIMPLEMENTED
  but was : PERMISSION_DENIED
EXIT=1

=== ARM 2: the no-engine service accepts the session silently instead of refusing it ===
SidecarHandshakeTest.theHandshakeReachesTheSessionServiceAndItsRefusalReachesTheCaller
  the sidecar hosts no engine, so the handshake must be refused rather than answered
  ==> expected: <ExecutionException> but was: <TimeoutException>   (40.4 s)
EXIT=1

=== ARM 3: the outcome's produce list is dropped on its way into core ===
DirectParallelConsumerClientTest.aSuccessfulOutcomeAdvancesTheOffsetAndProducesWhatItAskedFor
  expected: 1                                                      (30.2 s)
EXIT=1

=== ARM 4: a failure Outcome is applied as a success ===
DirectParallelConsumerClientTest.aFailureOutcomeIsRedeliveredCarryingItsReason
  expected: 2                                                      (30.1 s)
EXIT=1

=== ARM 5: the exempt module's own ArchUnit wrapper is removed ===
EveryModuleWiresUpArchUnitTest.everyExemptModuleStillRunsSomeArchUnitWrapper
  ...is exempt from the shared wrapper because it cannot depend on core's test-jar, NOT from
  ArchUnit - it must still run some @AnalyzeClasses test of its own, or the exemption is a hole
EXIT=1

=== RESTORED: control arms again, trees byte-identical ===
EXIT=0

Arm 1 is the one that matters most. It is what separates "the wire reached the service" from
"the wire reached the server", and a green run cannot tell you which you have. The authority
allowlist answers PERMISSION_DENIED and the admission slot RESOURCE_EXHAUSTED, both from
interceptors that run before the service method - so UNIMPLEMENTED is the only code the service
itself can raise, and asserting the code rather than "it failed" is what makes the test mean
something. A permanent control arm sits beside it: the same client pointed at a port nothing is
listening on, asserted to fail differently.

Arm 2's shape is the finding, not its colour. Removing the refusal does not fail fast; it hangs,
and the assertion lands on its budget after 40 seconds. That is what a client author would hit
against a silently-accepting sidecar.

How it was verified

JDK 17, macOS, on this branch:

  • ./mvnw clean then a full-reactor -Pci install - BUILD SUCCESS, every module including the
    four new ones.
  • The four modules' own suites green, no Docker and no broker: the api surface's ArchUnit rules and
    the asynchronous processor's whole failure contract; the direct transport's four; the gRPC
    transport's wire mapping and its two session-end cases; the handshake pair.
  • bin/ci-unit-test.sh, the full unit suite - which is what found the collision below. A
    module-scoped run was green while the whole-tree run was not.
  • bin/check-all.sh on its real exit code: 0. Two gates report CANNOT RUN -
    check-proto-breaking.sh and check-proto-lint.sh, both wanting a buf this machine does not
    have. Both belong to a base rung, neither is affected by this diff, and CI is where they run.
  • COPYRIGHT_CHECK_REQUIRE_FORK_POINT=1 bin/check-copyright-headers.sh - 0 violations against a
    real fork point.

Commits were made with --no-verify, and the reason is recorded rather than assumed. The
pre-commit hook resolved the wrong project directory and gated this branch against a different
worktree's tree - it reported dangling citations in bench/README.md, a file that does not exist on this
branch at all. bin/check-file-refs.sh run in this worktree reports zero, and bin/check-all.sh
exits 0. The harness fix is in flight on #382.

A rule master gained after the source branch was cut

EveryModuleWiresUpArchUnitTest requires every module with test sources to wire the shared
TestConventionRules. That rule does not exist on feats/proxy-requirements, so the api module
never had to satisfy it - and it cannot: the rule library ships in core's test-jar, and that
module is dependency-free on core in any scope, which its pom states and the direct sibling's
enforcer ban exists to keep true.

Resolved by naming the module as that test's one exemption, with the reason at the site - and by
guarding the exemption, which is the part worth reviewing. A second assertion beside it requires
an exempt module to still run some @AnalyzeClasses wrapper of its own, so deleting
ClientSurfaceArchTest turns the exemption back into a failure rather than into silence. Arm 5
above is that guard going red. The alternative - putting core's test-jar on the api module's test
classpath - would have worked and is deliberately not taken, because the "dependency-free" invariant
is also what the module's SpotBugs filter argues from; if you would rather have the dependency than
the exemption, this is the place to say so.

Five places this differs from #293 on purpose

  1. The SpotBugs gate moves from process-classes to process-test-classes. master's root pom
    sets includeTests=true, which postdates that branch - and at process-classes,
    target/test-classes does not exist yet. A clean build analysed main code only and passed; the
    next incremental build, with test classes left on disk, failed the same tree with two findings.
    A gate whose verdict depends on what a previous run left behind is not a gate. The new phase is
    still before test, so every reason the original phase was chosen still holds.
  2. The conformance suite is not taken - see "deliberately NOT in this rung". The api pom's,
    direct pom's and READMEs' descriptions of it are rewritten to say where it is rather than
    describing a tree that does not exist.
  3. CommitHistory.forPartition is taken, and the rest of that class's rework is not. The static
    factory is what the direct transport's test needs and is the reason its evidence can be a value
    polled on with Awaitility rather than an assertion; the source branch's wider tidy-up of the same
    class belongs with whatever else wanted it.
  4. The gRPC graph's two pins are declared once on the Java aggregator, not in each of the two
    modules that resolve them. parallel-consumer-proxy's pom owns the reasoning and keeps it; what
    is different here is only that this aggregator has two children needing the same pin and
    dependencyManagement does not flow between siblings. It stays out of the root pom, where
    ${error-prone.version} pins the Error Prone compiler for an unrelated and temporary reason.
  5. HelloFixture is deleted rather than relocated. The scaffolding rung's own pom said this rung
    would "convert this module and move this class out"; a module that compiles a client surface, two
    transports and their suites is a strictly stronger statement about the JDK than a class that
    prints a string. The eight non-JVM modules keep theirs and bin/foreign-client-step.sh is
    untouched.

Two smaller ones: the protocol specification and client-authoring guide moved to the protocol module
in #383, so every README link that pointed into parallel-consumer-proxy/docs/
is repointed; and docs/client-static-analysis.md arrives trimmed to the rows that exist here, with
the remaining languages named as arriving with their own clients rather than left as apparent gaps.

Three things this PR found

A gate that disagreed with itself between a clean build and an incremental one - item 1 above.
The discovery order is what makes it worth naming: the clean run passed, so the natural conclusion
was that the incremental failure was noise.

Three fb-contrib findings in the direct transport, two of them real. An identity lambda and an
overly concrete collection parameter were fixed. The third is declined because its remedy is
forbidden here: CE_CLASS_ENVY on the record translation points at moving the method onto core's
RecordContext, and core must not know a client API exists - the mirror of that rule is exactly what
ClientSurfaceArchTest enforces from the other side.

Two ignored CountDownLatch.await results in SessionEndTest were a real hole. A timed-out
await returned the same success both tests assert, so the run could have gone green having measured
nothing. Fixed rather than excluded, by throwing when the release never comes - which lands as the
wrong verdict and fails the assertion loudly.

The declined findings, and where each argument lives

Every module carries its own spotbugs-exclude.xml; every entry names a class and a pattern, most a
method or a field too, with the argument written beside it. The two that generalise beyond one line:
widening a parameter to its *OrBuilder supertype would let a caller obtain an API value for a
message that was never on any wire (the same call #383 made for the protocol
module's bridges), and making the negotiated in-flight ceiling static would share one session's
number with every other client in the JVM.

One NullAway warning remains on a line this diff wrote:
OutboundRecord.of("responses", null, ...) passes a null key deliberately, which is the API's
keyless record and which WireMappingTest asserts stays absent on the wire. NullAway is repo-wide
advisory and no file in this tree carries a suppression for it; starting that practice is not this
PR's call.

The duplication reports, read

Both engines pass. PMD CPD reports zero new clones and no movement at all in its percentage.
jscpd names four, and none of them is duplication this PR should remove:

  • Three are the same pom block - the commons-lang3 dependency and the comment above it saying
    why it is declared explicitly rather than leaned on transitively (core's test-jar reaches for it,
    and test scope is not transitive). One of the pair in each case is
    parallel-consumer-proxy/pom.xml, which is on the base branch: the repetition is the established
    answer to a Maven fact, and the comment is what makes each copy readable on its own. Collapsing it
    would mean a parent declaring a test dependency for children that may not want it.
  • One is an import block shared between GrpcParallelConsumerClient and the test that drives it -
    the same pair feat(proxy) astubbs#242: a sidecar giving non-JVM runtimes key-ordered concurrency, in eleven languages #293's own report names. A test importing the protocol types
    its subject uses is not duplication with a fix.

The analysis surfaces, read

bin/check-pr-analysis-surfaces.sh 386 reported two findings on lines this diff wrote and none
inherited
, both from SpotBugs and both in the two parallel-consumer-core test files this
extraction touched outside the client modules. One fixed, one declined with the argument at the site:

  • Fixed - EveryModuleWiresUpArchUnitTest's exempt-module registry was a List used only with
    contains() and a for-each, which fb-contrib reports as "defines List based fields but uses them
    like Sets"
    . It is right: nothing about that registry is positional, and the type was simply the
    first one to hand. It is a Set now.
  • Declined - CommitHistory.forPartition's first parameter could be a Collection, and must
    not be. Everything that class answers is positional: highestCommit() and getEncoding() read the
    LAST element, which is the most recent commit, so encounter order is the whole meaning of the
    input. A Collection parameter would accept a set and return an arbitrary commit while looking
    correct - worse than not compiling. The reason is now in that method's javadoc, so the next reader
    meets it before the finding. This is not a general position on the pattern: the same
    OCP_OVERLY_CONCRETE finding on DirectParallelConsumerClient.toProducerRecords was fixed
    earlier on this branch, because that method only iterates and its parameter promises no order.

The module-scoped SpotBugs gate this PR adds is stricter than the repo-wide static: spotbugs lane
and is green. One NullAway warning on a line this diff wrote is named in the section above and
declined.

Re-read on the fixed head, once static: spotbugs had run on it, and it says what a fix and a
decline are supposed to say: the EveryModuleWiresUpArchUnitTest finding is gone, the
CommitHistory one is still reported, and nothing is inherited. That is the expected state, not
an outstanding item - a decline does not silence a detector, it answers it, and the answer is in the
method's javadoc. Reviewers who disagree with the decline should say so there.

(An earlier re-read returned no annotations at all. That was not a clean bill of health, it was
SpotBugs not having run on that head yet - the distinction the script's own closing note insists on.)

Integration Tests went red, and it was not a test

Zero tests failed anywhere in that job. It built every module through parallel-consumer-proxy,
reached the Java client's SpotBugs gate, and hung fetching a detector jar from Maven Central:

[ERROR] Could not transfer artifact com.mebigfatguy.fb-contrib:fb-contrib:jar:7.7.4
        from/to central (https://repo1.maven.org/maven2/): Read timed out
Parallel Consumer - Proxy Client Java API .......... FAILURE [04:07 min]

It is the third instance of a class this workflow already names.
maven-central-timeout-azure-west-regions-2026-04-21.md
records that Central's CDN routes badly from some Azure regions, that each attempt hangs exactly
240 seconds, and that re-running does not help because the region assignment is the variable. The
signature matches term for term - 04:07, and Unit Tests fetched the same jar from another runner in
the same run without trouble.

Why it reached the test matrix only now is the part this PR owns: dependency:go-offline does not
fetch what a plugin resolves for itself, and SpotBugs' detectors are declared under
<configuration><plugins>. That cost nothing while SpotBugs ran only in its own static: spotbugs
job; binding spotbugs:check into the default reactor makes every lane resolve them.

prepare-deps already warms two instances of exactly this class - protobuf's protoc executables
and scala-maven-plugin's compiler and zinc bridge, each with its reasoning written above it. This
adds the third, the same way and with the same two guards: the version comes from the pom property
rather than a literal that rots on the next bump, a non-version answer from help:evaluate is a hard
error rather than a warm of the wrong coordinate, and the jars are asserted on disk afterwards.
Verified by extracting the step from the workflow and running it (exit 0, all four jars present), and
proven able to fail by renaming one property, which produced the intended ::error:: rather than a
silent no-op.

Nothing was skipped, disabled or retried to make the lane green - the gate still runs everywhere
it ran before; only the artifacts it needs are now already on disk.

The control arm: the same suite passes. bin/ci-integration-test.sh was run locally against this
branch, on a machine where Maven Central is reachable - same code, same commands, one term different -
and it is BUILD SUCCESS end to end. So the lane's red was the artifact fetch and nothing about the
diff, which is what a re-run alone could never have told you apart from luck.

Known-red, and not a fault

Chaos Pain Suite, Unit Tests, Performance Tests and every other lane passed on the first run.
The one real red, Integration Tests, is diagnosed above and fixed at its cause.

Everything else has run on this head and passed, self-hosted lanes included: Unit Tests,
Integration Tests in 9m18s - the lane that was red, now green with the cache warm in place,
which is the confirmation that the fix above works rather than a hope that it does - Performance Tests, Chaos Pain Suite and static: spotbugs. The cache job carrying the fix passed too, with
its Warm SpotBugs' detector plugins step succeeding on a real runner and saving the warmed jars.

Those lanes took a while to get there because the six highcpu runners spent the day flapping in and
out of status=offline - a fleet condition affecting every open PR, not a signal about this one.

roadmap-stage: N/A - the language-proxy-sidecar entry names #293 as its carrier and sits at limited-poc on the strength of that PR's content. Landing a client library on master makes no more advanced artifact exist - there is still no engine for it to talk to - so its stage is unchanged.

Checklist

  • Docs updated - docs/client-static-analysis.md (new, and cited by name from four poms and an
    ArchUnit test), the four module READMEs, the clients directory README and AGENTS.md's module
    table - both of which claimed the whole tree was build scaffolding, which this change makes false -
    and the module-maturity and testing-evidence records.
  • User-facing feature documentation data added under docs/features/ - N/A - nothing here is a
    user-facing feature. Nothing is published to any registry (the clients tree skips deploy and
    install), and there is no engine on master for the gRPC transport to talk to; the
    release-documentation records this does earn are the maturity and evidence rows, which are in this
    PR and say in both directions what is and is not evidenced.
  • Tests added/updated - the direct transport's suite and the sidecar handshake pair are new; the
    api and gRPC suites come with their modules. Five arms were proven able to fail by sabotage, with
    control arms on either side, above.
  • Title & body reflect the final content of this PR
  • Ran ce-simplify and ce-code-review locally - N/A - neither was run. Most of the diff is code
    taken from feat(proxy) astubbs#242: a sidecar giving non-JVM runtimes key-ordered concurrency, in eleven languages #293 where it was already reviewed, and the changes this rung
    made to it are named one by one above; the cheaper place to spend a review is @claude review this
    on the PR, which is also the only route that opens inline threads.

…keeps it transport-free

The API a JVM worker programs against, and the one nine other languages will mirror:
ParallelConsumerClient, ClientOptions, RecordProcessor and its asynchronous form, InboundRecord,
OutboundRecord, Outcome and the Outcomes bridge that turns a misbehaving user function into a
verdict instead of a broken session.

What a user gets: a way to receive Kafka records and answer per record - success, optionally
carrying records for Parallel Consumer to produce, or failure carrying a reason that travels with
the redelivery - without the transport underneath appearing anywhere in the shape. Two Java
transports implement it, and neither is visible from here.

THE SURFACE IS DEPENDENCY-FREE, AND THAT IS THE PRODUCT DECISION RATHER THAN TIDINESS. A ByteString
or a ConsumerRecord on this surface would not be a Java problem, it would be a specification
problem: the mirroring languages have no such type, so the shape stops being expressible and the
fan-out diverges silently, one language at a time, with nothing going red. ClientSurfaceArchTest
fails the build on the bytecode when a transport, engine or Kafka type is referenced; the direct
sibling's bannedDependencies enforcer fails it on the dependency tree. They are complements - the
enforcer fires when a jar arrives, ArchUnit when a type is used through a jar that was already
legitimately there.

THE MODULE WAS A LEAF AND IS NOW AN AGGREGATOR, which its own pom on master said this rung would
do. The HelloFixture class it compiled - the polyglot scaffolding's one deterministic line - is
DELETED rather than relocated: a module that compiles a client surface and its suites is a strictly
stronger statement about the JDK than a class that prints a string. The eight non-JVM modules keep
theirs and bin/foreign-client-step.sh is untouched.

SpotBugs gates every module beneath this aggregator - `check` rather than `spotbugs`, so a finding
fails the build instead of writing a report nobody reads. It is bound at process-test-classes, NOT
at the process-classes this arrived from: the root pom sets includeTests=true, and at
process-classes target/test-classes does not exist yet, so a clean build analysed main code only
and passed while the next incremental build failed the same tree. A gate whose verdict depends on
what a previous run left on disk is not a gate. process-test-classes is the first phase after
test-compile and is still before `test`, so bin/ci-unit-test.sh still reaches it.

Extracted from #293 as extraction A7 of
docs/plans/2026-08-31-001-process-god-branch-decomposition-plan.md, taken by path-scoped checkout
rather than rewritten. The transport-parameterised conformance suite that lives in this module's
test tree on that branch is NOT here - it belongs to the conformance rung stacked above this one.

A COLLISION WITH A RULE MASTER GAINED AFTER THAT BRANCH WAS CUT, resolved rather than absorbed.
EveryModuleWiresUpArchUnitTest requires every module with test sources to wire the shared
TestConventionRules - which ships in core's test-jar, which this module may not depend on in any
scope. So the module is named as its one exemption, with the reason at the site, and the exemption
is GUARDED: a second assertion beside it requires an exempt module to still run some
@AnalyzeClasses wrapper of its own, so deleting ClientSurfaceArchTest turns the exemption back into
a failure instead of into silence. Proven by deleting that test and watching the guard go red, with
a control arm either side. The full unit suite is what found this; the module-scoped run was green.
…re in-process

The degenerate transport: the shared surface bound straight to the engine that has always been on
master, with no sidecar, no wire and no protocol underneath. The user's RecordProcessor runs on
core's own worker threads and its Outcome maps onto core's success and retry paths, so ordering,
retry and commit semantics are core's, untouched.

For a user this is the cheapest way to program against the client surface - the same code that will
later run against a sidecar, running in the same JVM today. For the project it is the control arm:
a scenario that passes over the wire and fails here is a claim about the API rather than about a
stream.

"THE ENGINE" HERE IS parallel-consumer-core, NOT THE SIDECAR'S DISPATCH ENGINE, and the distinction
is what let this land on a branch where the sidecar hosts nothing. Nothing in this module reads a
line of parallel-consumer-proxy; its pom bans protobuf, gRPC and the protocol module from the
classpath in every scope, and that ban passing is the shortest proof of the claim.

Evidence, and it asserts the offset rather than the callback: the processor being invoked proves
hand-out and nothing about whether the verdict was applied, and a transport that ran the user
function and dropped its answer looks identical from the processor's side. So the success arm
asserts core's committed offset and the producer's history, the failure arm asserts the redelivery
carries the reason the previous attempt gave, and both were proven able to fail by breaking one
term at a time - dropping the produce list on its way into core, and applying a failure Outcome as
a success - with a control arm either side and the tree restored byte-identical.

CommitHistory.forPartition comes from the same source branch and is why that evidence is a value
rather than an assertion: the class's constructor wants a flat per-partition list, and until this
existed the only code producing one lived inside a Truth Subject - so a fixture that needs a number
to poll on with Awaitility hand-wrote the same reverse scan. Only the static factory is taken; the
source branch's wider tidy-up of that class is left where it is.

Two fb-contrib findings were real and were fixed rather than excluded - an identity lambda, and an
overly concrete collection parameter. The third is declined in the module's own filter, because its
remedy is forbidden here: CE_CLASS_ENVY on the record translation points at moving the method onto
core's RecordContext, and core must not know a client API exists - the mirror of that rule is what
ClientSurfaceArchTest enforces from the other side.

Extracted from #293 (extraction A7). The cross-language conformance suite
that also drives this transport arrives with the conformance rung above.
… a sidecar

The same surface over the RPC hop: one bidirectional stream, Configure as its first message, and
from Configured onward dispatched records handed to executors and their per-record reports sent
back. It is the JVM reference implementation of the wire, and the shape Kotlin and Scala are meant
to wrap rather than reimplement.

What a user gets that the direct transport cannot give them: their processing running wherever they
put it, against a Parallel Consumer that is somewhere else. What every later language gets: a
session implementation that has already been debugged once.

THE CLIENT STORES NOTHING PER RECORD. A dispatch's fencing token rides from the queue to the report
on the executor's stack and is echoed byte for byte - no request map, no dedupe cache, no
completion state - because a stateless client cannot have a state bug, and every language mirroring
this inherits that rather than inventing its own bookkeeping. WireMappingTest pins the echo without
a connection; SessionEndTest pins the two ways a session ends badly, both of which were red before
the change that added them on the source branch: a mid-session stream error used to hang with every
executor parked and nothing on the surface able to report it, and closing mid-processing used to
transmit an interrupted user function's failure as a verdict the user never gave.

IT DEPENDS ON parallel-consumer-proxy IN NO SCOPE, and that is load-bearing rather than tidy. A
test-scope dependency is not on the classpath but IS an edge in the Maven reactor, so a client
wrapping this one would build the sidecar - and bin/build.sh opens with `clean`, so the routine
build of a wrapper would delete the sidecar jar other languages' tests spawn. Anything needing a
running sidecar lives in the harness lane instead.

Two ignored CountDownLatch.await results in SessionEndTest were a real hole and are fixed rather
than excluded: a timed-out await returned the same success both tests assert, so the run could have
gone green having measured nothing. Four findings are declined in the module's own filter with the
argument for each, the two that generalise being that widening a parameter to its *OrBuilder
supertype would let a caller obtain an API value for a message that was never on any wire - the
same call #383 made for the protocol module's own bridges - and that
making the negotiated in-flight ceiling static would share one session's number with every other
client in the JVM.

The gRPC graph's two pins - error_prone_annotations for requireUpperBoundDeps, gson forward off the
version OSS Index matches for CVE-2025-53864 - are declared once on the Java aggregator rather than
in this pom and the harness's. parallel-consumer-proxy's pom owns the reasoning and keeps it; what
is new is only that this aggregator has two children needing the same pin and dependencyManagement
does not flow between siblings. It stays out of the root pom, where ${error-prone.version} pins the
Error Prone COMPILER for an unrelated and temporary reason.

Extracted from #293 (extraction A7).
…ly a scripted one

The harness lane, and the one assertion no in-JVM fake can make for a client library: the gRPC
transport's own connect path, against a sidecar started through the sidecar's own entry point -
real bind, real authority allowlist, real admission slot, real session service.

Because that sidecar hosts no Parallel Consumer engine, the run ends where its honesty ends: the
client is told UNIMPLEMENTED, in the protocol's own words, and reports that to its caller from both
connect() and sessionEnd(). That is a smaller claim than "the client works", and it is written down
as the smaller claim rather than dressed up.

THE STATUS CODE IS THE ASSERTION, NOT "IT FAILED". The authority allowlist answers PERMISSION_DENIED
and the admission slot RESOURCE_EXHAUSTED, both from interceptors that run BEFORE the service
method, so only UNIMPLEMENTED can have come from the service - which is what distinguishes "the
handshake was delivered" from "the connection was turned away". A second test points the same client
at a port nothing is listening on and asserts the failure is NOT that one, so the first cannot pass
on any error at all.

Proven able to fail, one term at a time, with a control arm either side and the tree restored
byte-identical afterwards:

  CONTROL   untouched tree                                              2 tests pass
  ARM 1     the client declares an :authority the allowlist does not
            list (overrideAuthority on the channel)                     expected UNIMPLEMENTED
                                                                        but was PERMISSION_DENIED
  ARM 2     the no-engine service accepts the session silently
            instead of refusing it                                      no refusal in 30s;
                                                                        TimeoutException
  RESTORED  control arm again                                           2 tests pass

Arm 1 is the one worth reading: it shows the assertion is about reaching the SERVICE rather than
about reaching the server, which is exactly what a green run cannot tell you.

IT IS A MODULE RATHER THAN A TEST SCOPE, and that is inherited from #293
rather than decided here: a test-scope dependency is not transitive on the classpath but is an edge
in the Maven reactor, so putting this test in the transport module would drag the sidecar into the
ordinary build of anything wrapping it - and bin/build.sh opens with `clean`, so that build would
delete the sidecar jar other languages' tests spawn. The measurement is a reactor list rather than
an assertion, and the pom says which command to run.

Its one SpotBugs finding is declined: the socket findsecbugs wants encrypted is opened purely so
the OS allocates a port number and is closed before anything connects to it.

The engine-backed conformance run, and the compile-scope harness classpath a JVM client declares to
spawn a sidecar as a child process, arrive with the conformance rung stacked on this one - this
module is where they land.
… it is now evidenced for

The records that make the Java client's claims checkable by someone who did not write it, and
correct the ones this change made false.

docs/client-static-analysis.md is the policy the Java modules' poms and their exclusion filters all
cite by name, and it did not exist on master: which tool, why that one, what it catches, what fails
the build, and the exact local command with the three silent traps baked into it - naming the
aggregator instead of the leaves analyses nothing and reports BUILD SUCCESS; dropping the
quarantined exclusion makes you debug someone else's known flake; omitting -am fails at validate.
It arrives trimmed to what exists here: Java filled in, the protocol row pointing at docs/ci.md
which owns that gate, and the other ten languages named as arriving with their own clients rather
than left as apparent gaps. The severity policy is the part worth arguing with - everything fails,
nothing warns, dismissal at the finding with a reason - and it is deliberately stricter than the
repo-wide SpotBugs lane, because these modules start at zero findings and that is the one moment a
clean-slate gate is free.

The maturity and testing-evidence records say in both directions what is and is not evidenced, and
the limitation is the sentence that matters: NO RECORD HAS CROSSED THE WIRE. The direct transport
is evidenced end to end because the engine it binds is ordinary; every gRPC assertion past the
handshake is against a proxy the test itself scripts, which can prove how the transport reacts and
cannot prove a real engine would ever behave that way.

Two claims elsewhere became false with this change and are corrected rather than left to rot: the
clients directory's README said none of its modules was a client, and AGENTS.md's module table said
the whole tree was build scaffolding.
@github-actions

Copy link
Copy Markdown

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 5 package(s) with unknown licenses.
See the Details below.

License Issues

parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-direct/pom.xml

PackageVersionLicenseIssue Type
bz.stub.parallelconsumer:parallel-consumer-core0.6.0.0-SNAPSHOTNullUnknown License
bz.stub.parallelconsumer:parallel-consumer-proxy-client-java-api0.6.0.0-SNAPSHOTNullUnknown License

parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-grpc/pom.xml

PackageVersionLicenseIssue Type
bz.stub.parallelconsumer:parallel-consumer-proxy-client-java-api0.6.0.0-SNAPSHOTNullUnknown License
bz.stub.parallelconsumer:parallel-consumer-proxy-protocol0.6.0.0-SNAPSHOTNullUnknown License

parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-harness/pom.xml

PackageVersionLicenseIssue Type
bz.stub.parallelconsumer:parallel-consumer-proxy0.6.0.0-SNAPSHOTNullUnknown License
Excluded from license check: pkg:githubactions/actions/github-script

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
maven/com.github.spotbugs:spotbugs-maven-plugin UnknownUnknown
maven/bz.stub.parallelconsumer:parallel-consumer-core 0.6.0.0-SNAPSHOT UnknownUnknown
maven/bz.stub.parallelconsumer:parallel-consumer-core 0.6.0.0-SNAPSHOT UnknownUnknown
maven/bz.stub.parallelconsumer:parallel-consumer-proxy-client-java-api 0.6.0.0-SNAPSHOT UnknownUnknown
maven/com.github.spotbugs:spotbugs-maven-plugin UnknownUnknown
maven/org.apache.commons:commons-lang3 UnknownUnknown
maven/org.apache.maven.plugins:maven-enforcer-plugin UnknownUnknown
maven/bz.stub.parallelconsumer:parallel-consumer-core 0.6.0.0-SNAPSHOT UnknownUnknown
maven/bz.stub.parallelconsumer:parallel-consumer-proxy-client-java-api 0.6.0.0-SNAPSHOT UnknownUnknown
maven/bz.stub.parallelconsumer:parallel-consumer-proxy-protocol 0.6.0.0-SNAPSHOT UnknownUnknown
maven/com.github.spotbugs:spotbugs-maven-plugin UnknownUnknown
maven/io.grpc:grpc-netty-shaded UnknownUnknown
maven/org.apache.commons:commons-lang3 UnknownUnknown
maven/bz.stub.parallelconsumer:parallel-consumer-core 0.6.0.0-SNAPSHOT UnknownUnknown
maven/bz.stub.parallelconsumer:parallel-consumer-proxy 0.6.0.0-SNAPSHOT UnknownUnknown
maven/bz.stub.parallelconsumer:parallel-consumer-proxy-client-java-grpc 0.6.0.0-SNAPSHOT UnknownUnknown
maven/com.github.spotbugs:spotbugs-maven-plugin UnknownUnknown
maven/org.apache.commons:commons-lang3 UnknownUnknown
maven/com.github.spotbugs:spotbugs-maven-plugin UnknownUnknown
maven/com.google.code.gson:gson 2.14.0 🟢 9.4
Details
CheckScoreReason
Maintained🟢 1030 commit(s) and 4 issue activity found in the last 90 days -- score normalized to 10
Dependency-Update-Tool🟢 10update tool detected
Code-Review🟢 9Found 22/23 approved changesets -- score normalized to 9
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Packaging⚠️ -1packaging workflow not detected
Pinned-Dependencies🟢 7dependency not pinned by hash detected -- score normalized to 7
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License🟢 10license file detected
Fuzzing🟢 10project is fuzzed
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
Signed-Releases⚠️ -1no releases found
Vulnerabilities🟢 100 existing vulnerabilities detected
Security-Policy🟢 10security policy file detected
SAST🟢 10SAST tool is run on all commits
CI-Tests🟢 1030 out of 30 merged PRs checked by a CI test -- score normalized to 10
Contributors🟢 10project has 11 contributing companies or organizations
maven/com.google.errorprone:error_prone_annotations 2.48.0 🟢 7.1
Details
CheckScoreReason
Maintained🟢 1030 commit(s) and 9 issue activity found in the last 90 days -- score normalized to 10
Code-Review⚠️ 0Found 0/30 approved changesets -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
License🟢 10license file detected
Pinned-Dependencies🟢 10all dependencies are pinned
Binary-Artifacts🟢 10no binaries found in the repo
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Security-Policy🟢 10security policy file detected
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0

Scanned Files

  • parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-api/pom.xml
  • parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-direct/pom.xml
  • parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-grpc/pom.xml
  • parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-harness/pom.xml
  • parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/pom.xml

@github-actions

Copy link
Copy Markdown

✅ Duplicate Code Report

Two engines run in parallel for cross-validation. Each has its own thresholds tuned to its baseline - the real safety net is the per-engine "max increase vs base" check.

✅ PMD CPD

PR Base Change
Clones 32 32 ➖ 0
Duplicated lines 1017 1017 ➖ 0
Duplication 0.13% 0.13% ➖ 0
Rule Limit Status
Max duplication 0.5% ✅ Pass (0.13%)
Max increase vs base +0.1% ✅ Pass (+0.00%)

No new clones introduced by this PR.

✅ jscpd (language-agnostic)

PR Base Change
Clones 97 93 🫤 +4
Duplicated lines 1385 1313 :face_with_raised_eyebrow: +72
Duplication 1.02% 1.00% 🫤 +0.02%
Rule Limit Status
Max duplication 2% ✅ Pass (1.02%)
Max increase vs base +0.1% ✅ Pass (+0.02%)
⚠️ 4 new clones introduced
  • 8 lines: parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-grpc/src/main/java/bz/stub/parallelconsumer/client/grpc/GrpcParallelConsumerClient.java:10 <-> parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-grpc/src/test/java/bz/stub/parallelconsumer/client/grpc/SessionEndTest.java:7
  • 20 lines: parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-grpc/pom.xml:76 <-> parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-harness/pom.xml:109
  • 28 lines: parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-direct/pom.xml:47 <-> parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-harness/pom.xml:109
  • 20 lines: parallel-consumer-proxy/pom.xml:123 <-> parallel-consumer-proxy-clients/parallel-consumer-proxy-client-java/parallel-consumer-proxy-client-java-harness/pom.xml:109

Powered by astubbs/duplicate-code-cross-check

@github-actions

Copy link
Copy Markdown

🧪🔒 Quarantine Lane Report

Quarantined test Outcome Owner Meaning
ProducerManagerTest.producedRecordsCantBeInTransactionWithoutItsOffsetDirect 🔴 failing (expected) #262 quarantine holding

🔴 expected while the owner PR is open · 🟡🎲 flapper, pass proves nothing · 🚨 a deterministic quarantined test passing means its fix landed: delete its @Quarantined annotation + docs/quarantined-tests.md entry (a merge-blocking review thread has been opened). Lane: non-gating; rules: see the Quarantine Audit check.

@github-actions

github-actions Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

⚠️ SpotBugs Report

425 bug(s) found (rule-level exclusions only - see docs/inflight/static-spotbugs-rule-registry.md). See the annotations on the Files Changed tab for details.

… wrote

bin/check-pr-analysis-surfaces.sh 386 reported two findings on lines this branch wrote, both from
SpotBugs and both in parallel-consumer-core's test tree - the two files this extraction touched
outside the client modules. Nothing inherited was reported. One is fixed, one is declined with the
argument at the site rather than in a PR body nobody re-reads.

FIXED - the exemption registry is a Set, because membership is what it is for.
EveryModuleWiresUpArchUnitTest's exempt-module list was a List used only with contains() and a
for-each, which is fb-contrib's "defines List based fields but uses them like Sets". The detector is
right and the type was simply the first one that came to hand: nothing about this registry is
positional, and a Set says so.

DECLINED - CommitHistory.forPartition's first parameter must stay a List, though a Collection would
compile. Everything that class answers is positional: highestCommit() and getEncoding() read the
LAST element, which is the most recent commit, so encounter order is the whole meaning of the input.
A Collection parameter would accept a set and return an arbitrary commit while looking correct,
which is worse than not compiling. The reason now sits in that method's javadoc, so the next reader
meets it before the finding rather than after.

Not a general position on the pattern: the same OCP_OVERLY_CONCRETE finding on
DirectParallelConsumerClient.toProducerRecords was FIXED earlier on this branch, because that method
only iterates and its parameter carries no ordering promise. The difference is whether order means
anything, not whether the detector is fashionable.
…lves-for-itself miss

Integration Tests went red on this PR with ZERO tests run. The job built every module through
parallel-consumer-proxy, reached the Java client's SpotBugs gate, tried to fetch
com.mebigfatguy.fb-contrib:fb-contrib:7.7.4 from Maven Central, and hung until it gave up:

    [ERROR] Could not transfer artifact com.mebigfatguy.fb-contrib:fb-contrib:jar:7.7.4
            from/to central (https://repo1.maven.org/maven2/): Read timed out

WHAT IT IS, AND IT IS NOT NEW - it is the third instance of a class this workflow already names.
docs/solutions/build-errors/maven-central-timeout-azure-west-regions-2026-04-21.md records that
Maven Central's CDN routes badly from some Azure regions, that each attempt hangs exactly 240
seconds, and that re-running does not help because the region assignment is what differs. The
signature here matches it term for term: the module failed at 04:07, "Unit Tests" fetched the same
jar from another runner without trouble in the same run, and no test anywhere in the job failed.

WHY IT REACHED THE TEST MATRIX ONLY NOW, which is the part this PR owns. `dependency:go-offline`
resolves declared dependencies and plugin JARs, but not what a plugin resolves FOR ITSELF at
execution time - and SpotBugs' detector plugins are declared under <configuration><plugins>, so
go-offline never sees them. That cost nothing while SpotBugs ran only in its own `static: spotbugs`
job. Binding spotbugs:check into the DEFAULT reactor, which the Java proxy client does, makes every
lane of the `test` matrix resolve them, and a cold cache then enters the route lottery three times
over.

The prepare-deps job already warms two instances of exactly this class - protobuf-maven-plugin's
protoc executables, and scala-maven-plugin's compiler and zinc bridge - each with the same
reasoning written above it. This is the third, warmed the same way and with the same two guards:
the version comes from the pom property rather than a literal that would rot on the next bump, a
non-version answer from help:evaluate is a hard error rather than a warm of the wrong coordinate,
and the jar is asserted ON DISK afterwards, because the goal's exit code says it ran while only the
file says the cache got populated.

Verified by extracting the step from the workflow and running it: exit 0 with all four jars present.
Proven able to fail by renaming one property, which produced the intended error rather than a silent
no-op:

    ::error::fb-contrib.renamed did not resolve to a version from the root pom
             (got 'null object or invalid expression') - the SpotBugs detector cache warm would be
             a silent no-op

Nothing is skipped, disabled or retried to make the lane green: the gate still runs in every lane it
ran in before, and what changes is only that the artifacts it needs are already on disk.
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