SEDA bus · Java · 1.3.1
The original — and the only port with guaranteed delivery.
seda-bus-java is where the design started, and it's the version embedded in
service-bus and 1M5. It carries features none of the other six ports have: optional
AtLeastOnce/ExactlyOnce persistence, datatype channels, and a pull model
alongside the push-based consumer API.
Executors.newFixedThreadPool, daemon threadsra-common onlyra.common.Envelope, shared with service-bus and 1M5DynamicRoutingSlip — a LIFO stackjava.util.concurrent.Semaphore, one per channelAtMostOnce / AtLeastOnce / ExactlyOnceReject (the only port not defaulting to Block)How it implements the design
Everything the platform gives you, used directly.
The envelope, completion callback, service-level, and channel/consumer/bus interfaces all come
from ra-common, so the bus is a drop-in message bus for the wider stack. The
routing slip is a genuine LIFO stack (producers push routes and call ratchet();
completion pops) — the other six ports use a FIFO list for the same itinerary concept.
Guaranteed delivery is per-channel or per-envelope: AtLeastOnce
writes the envelope to the channel's on-disk store via an atomic temp-file move before
send returns, removes it on ack, and replays unprocessed envelopes in
filename-time order; ExactlyOnce adds a bounded dedup map so replay skips
duplicates. “Exactly once” means processing effectively once, not a distributed
two-phase commit. Datatype channels let a channel carry a class filter,
rejecting envelopes whose content isn't assignable to it — a feature none of the other
six ports have.
Concurrency permits are real java.util.concurrent.Semaphores, one per channel, held
by the worker pool — the JVM's own primitive, not a hand-rolled one. History matters here:
1.3.0 replaced a 100ms scan loop (which capped throughput at roughly 10
messages/sec/channel) with the current event-driven drain, and added retry/dead-letter and
persistence in the same pass.
Results
Never a cliff — because the JVM already pays the cost these ports had to fix by hand.
| Config | Sustained eps | 0.5× | 1.0× | 1.5× |
|---|---|---|---|---|
cap1 | 648,341 | 0.93 | 0.86 | 0.65 |
cap8 | 444,318 | 0.94 | 0.80 | 0.46 |
| Config | eps | vs. seq |
|---|---|---|
seq (1 producer) | 795,225 | — |
par (8 producers, 1 channel) | 450,484 | 0.57× |
chan (8 producers, 8 channels) | 2,028,409 | 2.55× |
Reading the numbers
The design choice that explains it
cap8 collapses hardest of any port at 1.5× overload (0.46,
versus 0.65 for its own cap1) — consistent with
ExecutorService-style shared-pool contention once eight stages genuinely
oversubscribe the pool.
But par's latency never shows the cliff several other ports pay for
— p50 1,659.5µs, p99 7,307.2µs, p999/max within one order of magnitude, despite
a design similar to what Rust and C++ had before their fixes. The leading explanation:
the JVM's ExecutorService/LockSupport spin briefly before making an
OS-level park/wake call, while Rust's raw std::sync::mpsc and C++'s original single
mutex parked immediately with no spin phase. Rust's fix (parking_lot, which adds
the same spin-before-park behavior) and C++'s fix (a two-lock queue, a different mechanism
entirely) both closed most of the gap to Java's behavior here — without either port
copying Java's approach directly. Java simply never had the problem to begin with.
The full cross-language results, including every anomaly explained
Source
Read the code, or the rest of the family.
seda-bus-java on GitHub
— source, its own DESIGN.md, CHANGELOG.md, and test suite.