Skip to content

Add an independent Java adapter - #24

Merged
jbwinters merged 11 commits into
mainfrom
task/java-adapter
Oct 28, 2026
Merged

jbwinters merged 11 commits into
mainfrom
task/java-adapter

Conversation

@jbwinters

@jbwinters jbwinters commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Adds an independent Java 21 implementation of Core's pinned jacquard-host-v0 protocol in java/: strict JSON and v0 value codecs, the host session state machine, a bounded process carrier for jacquard host worker, a low-level Host with a closed operation registry, and a replayer for the pinned conformance kit. It has no third-party runtime dependencies (JUnit 5 at test scope only), builds with Maven, and on Unix runs the system mkfifo.

  • The kit is a byte-identical fourth copy under java/src/main/resources, tracked by scripts/core_pin.py, which also generates Compat.java.
  • CI runs the offline suite on Temurin 21 and 25, and live-examples runs the live suite against the pinned Core with live tests required (65 tests, none skipped).
  • Kit replay passes all 21 recordings, both through the stand-in worker and against Core 0.3.0.

Java differs from the other adapters in ways docs/LIMITS.md and java/README.md record:

  • Process tree, not process group: Java cannot start a process in its own group, so on expiry or cancellation the host kills the worker and a snapshot of its descendants (through ProcessHandle, so a blocked stdin write cannot hold the kill). A descendant that was already reparented survives.
  • Output pipes: on Unix the host creates the worker's stdout and stderr as FIFOs and holds their read ends, because the JDK's own pipes drain at exit under the stream lock, without bound. On other systems the JDK's pipes are used.
  • Reads: reads never block; they are polled through available() on the caller's thread.
  • Writes: writes run on one stdin thread in 4096-byte chunks behind a gate, which can stay blocked only if an escaped descendant holds stdin and never reads.

Every wait, and FIFO setup, observes the one lifetime deadline, the Cancel, and the caller's interrupt. Frame encoding stops at max_frame_bytes, and text validation is charged against the frame. No handler starts once the call is cancelled, interrupted, or past its lifetime. Kit replay keeps at most one frame beyond the recording. Each of these has a regression test that fails on the code before it; see docs/EVIDENCE.md.

The SDK layer follows in a separate PR.

A Maven project in java/ (org.jacquardlang:jacquard-host, Java 21+, no
runtime dependencies) implementing jacquard-host-v0: strict codecs, the
session state machine, a bounded process carrier for `jacquard host
worker`, a Host with a closed operation registry, and replay of the
pinned conformance kit, carried as class-path resources.

Java cannot start the worker in its own process group and cannot
interrupt a pipe read. The carrier therefore reads stdout and stderr only
after available() reports bytes, on the caller's thread, rechecking the
lifetime deadline, cancellation, and the interrupt status in every wait;
writes run on one stdin thread in gated 4096-byte chunks; and expiry
kills a ProcessHandle snapshot of the worker's process tree. What that
misses is documented in java/README.md.

core_pin.py generates Compat.java and keeps the Java kit copy in sync.
NOTICE names the new copy, java/NOTICE stays identical to it, and the
source archive carries the Java project.
A java job runs the offline tests on Java 21 and 25, and the shared
pinned-Core job runs the live tests on Java 21 with
JACQUARD_REQUIRE_LIVE set, so a misconfigured job fails rather than
skips.
LIMITS.md records the process-tree kill, what the JDK's own reaping and
pipe handling hide, the uninterruptible stdin write, cooperative handler
cancellation, and the protocol-level scope. EVIDENCE.md records the
offline and live results on Java 21 and 25 against jacquard-core-0.3.0.
Process.destroyForcibly() closes the worker's stdin on the calling thread,
which waits behind a write the stdin thread has blocked on a pipe a child
holds; the kill never reached the child. Signal the worker through its
ProcessHandle instead. Regression tests keep the worker alive at expiry and
at cancellation. Refuse null arguments before a worker starts, bound the
live fixture install, extend the leak checks to failure paths, and correct
the docs on in-flight chunks, retained threads, and worst-case cleanup.
The JDK drains a process's output pipes on its reaper thread when the
process exits, under the stream lock available() and close() also take, into
an unbounded buffer, for as long as a descendant keeps writing. A reparented
descendant flooding stdout or stderr held the caller for seconds past the
deadline and grew the heap by hundreds of MiB. On Unix the host now creates
both pipes as FIFOs with mkfifo in a private directory, holds their read
ends, and redirects the worker's output to them, so no host read waits on a
JDK lock and a flood stays in the pipe until disposal closes it. Regression
tests flood stderr at the worker's exit, at expiry, and at cancellation, and
flood stdout.
@jbwinters
jbwinters merged commit a21cdd2 into main Oct 28, 2026
23 of 24 checks passed
@jbwinters
jbwinters deleted the task/java-adapter branch October 28, 2026 00:06
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