Background: the cluster RPC fabric (the rt-* modules, built on JeroMQ) was intentionally disabled for the 3.0 release -- not because it was broken, but because there wasn't a strong enough business case to keep maintaining it at the time. It's needed again for upcoming requirements. Four planning docs (CLUSTER-RPC-PLAN.md, RT-TRANSPORT-COMPARISON.MD, CLUSTER-CAPACITY.md, APP_NODE_IMPLEMENTATION_PLAN.md) worked through renovating the existing JeroMQ transport vs. replacing it with a Jetty WebSocket transport. This issue tracks executing the WebSocket path, which is what those docs converge on.
Why WebSocket over renovating JeroMQ: in how this codebase actually uses ZMQ (DEALER/ROUTER as a glorified TCP socket with length-prefixed framing and routing-identity envelopes -- not PUB/SUB, not multi-peer fanout, not inproc://-as-IPC), WebSocket is a strict superset plus a free HTTP layer: auth via the existing session-secret/OIDC pipeline, TLS via the existing Jetty SSL config, standard close codes, L7 proxy/mesh compatibility, ping/pong liveness, and observability that already works for the REST API. JeroMQ's threading model fights Java's concurrency primitives directly (documented as the "threading nightmare"), is pinned to an old version because upstream 0.6.0 hangs under our usage pattern, and the effort/risk estimates for renovating it in place are worse on every axis (RT-TRANSPORT-COMPARISON.MD §4-6).
Plan (phases, condensed from APP_NODE_IMPLEMENTATION_PLAN.md §5 -- see that doc for full detail and the resolved open-questions log in §6):
- Shared safety net. Timeouts + leak assertions on the existing (currently
@Test(enabled = false)) JeroMQ integration tests. Sunk cost regardless of anything else in this plan.
- WS transport prototype.
cluster-ws/cluster-ws-guice stubs; one static wss://host/cluster/v1 endpoint, registered once at bootstrap in deployment-jetty. Round-trip a single sync invocation against the existing (deprecated) dispatchers first, to validate the transport in isolation before touching dispatch. Decision point: if this round-trip doesn't land cleanly, fall back to the JeroMQ renovation path in JEROMQ.md instead of continuing.
- Invocation semantics parity. Multiplexing by request id, async /
CompletionStage invocations, best-effort cancellation (interrupt server-side, at-most-once delivery client-side), per-call timeouts, a per-peer session cache with lazy dial + coalesced concurrent dials, a configurable graceful-drain timeout (default 30s), and a 10 MiB per-frame cap. No outbox, no reconnect-with-buffering -- K8s SRV-gated discovery plus dial-on-demand plus registry health-pruning already cover what buffering would have bought us, and buffering would actively conflict with the health-pruning contract.
ElementRegistry-based dispatch. Replace the three deprecated LocalInvocationDispatcher implementations with one that resolves through ElementRegistry, via a new DeploymentId (deterministic hash of a deployment's own id) as an O(1) key into activeDeployments, with a merge-scan fallback across ElementRuntimeService.getActiveRuntimes() when wildcarded. ApplicationId-based scoping is dropped as legacy.
ClusterElementLocator + addressing. New ElementAddress type in sdk-cluster, built from sdk-cluster's existing Path type via composition (not subclassing -- Path is final), reusing ElementServiceKey's #-separated encoding for service+name. Explicit per-method @RemotelyInvokable opt-in (default-deny) -- @ElementService alone does not make a method remotely callable.
- Auth. No new auth scheme needed. The WS upgrade handshake carries the existing
SessionSecretHeader, validated through the existing SessionDao/SessionService chain against the shared MongoDB (every instance already has DB access, so the database is already the shared trust anchor). User-initiated cross-instance calls forward the caller's real session; pure control-plane calls (health/discovery) use a provisioned per-instance system/service User + minted Session.
- Discovery. Plug the existing SRV-backed
InstanceDiscoveryService implementations into the new WS registry.
- Coexistence + cutover.
elements.rt.transport config flag, default jeromq for one release then ws, both exercised in CI during the transition.
- Retire JeroMQ. Delete the six
rt-*-jeromq modules and related config; keep JEROMQ.md for historical reference.
- Observability + tuning. Per-peer connection metrics, idle-timeout / ping-interval tuning, published
/cluster/v1 frame-format docs.
- Retire
app-node. Fold cluster-node hosting into jetty-ws behind a new operational-mode CLI flag; delete the separate app-node executable (already confirmed dead/broken code, unmaintained for a long time).
Independent side-quest, land regardless of transport outcome: replace the unimplemented InstanceMetadataContext.getInstanceQuality(): double score (no production implementation exists today -- the only impl in the tree is a test mock returning a random double) with concrete available/total resource accounting: a ResourceBudget/ResourceClaim SPI, local optimistic debit between the registry's ~30s refresh cycles, sorting candidates by absolute available headroom (not ratio), and a configured (not auto-detected) total per instance. Full design in CLUSTER-CAPACITY.md. This lives in rt-server-cluster, is transport-independent, and should land early since it removes a known-broken piece from production sooner.
Naming. New modules drop the rt-server-/rt- prefix (cluster-ws, cluster-ws-guice, cluster-node-ws); the URL path becomes /cluster/v1/... (not /rt/v1/...). rt-*-jeromq modules keep their names through retirement to avoid rename churn on code that's going away anyway.
Suggested feature name: "Elemental Conduit" -- a conduit carries something between two points, which is exactly what this is (cluster RPC transport), and it pairs naturally with the existing "Elements" branding. Alternates considered: "Elemental Current", "Elemental Circuit".
References: CLUSTER-RPC-PLAN.md, RT-TRANSPORT-COMPARISON.MD, CLUSTER-CAPACITY.md, APP_NODE_IMPLEMENTATION_PLAN.md (all at repo root).
Background: the cluster RPC fabric (the
rt-*modules, built on JeroMQ) was intentionally disabled for the 3.0 release -- not because it was broken, but because there wasn't a strong enough business case to keep maintaining it at the time. It's needed again for upcoming requirements. Four planning docs (CLUSTER-RPC-PLAN.md,RT-TRANSPORT-COMPARISON.MD,CLUSTER-CAPACITY.md,APP_NODE_IMPLEMENTATION_PLAN.md) worked through renovating the existing JeroMQ transport vs. replacing it with a Jetty WebSocket transport. This issue tracks executing the WebSocket path, which is what those docs converge on.Why WebSocket over renovating JeroMQ: in how this codebase actually uses ZMQ (DEALER/ROUTER as a glorified TCP socket with length-prefixed framing and routing-identity envelopes -- not PUB/SUB, not multi-peer fanout, not
inproc://-as-IPC), WebSocket is a strict superset plus a free HTTP layer: auth via the existing session-secret/OIDC pipeline, TLS via the existing Jetty SSL config, standard close codes, L7 proxy/mesh compatibility, ping/pong liveness, and observability that already works for the REST API. JeroMQ's threading model fights Java's concurrency primitives directly (documented as the "threading nightmare"), is pinned to an old version because upstream 0.6.0 hangs under our usage pattern, and the effort/risk estimates for renovating it in place are worse on every axis (RT-TRANSPORT-COMPARISON.MD§4-6).Plan (phases, condensed from
APP_NODE_IMPLEMENTATION_PLAN.md§5 -- see that doc for full detail and the resolved open-questions log in §6):@Test(enabled = false)) JeroMQ integration tests. Sunk cost regardless of anything else in this plan.cluster-ws/cluster-ws-guicestubs; one staticwss://host/cluster/v1endpoint, registered once at bootstrap indeployment-jetty. Round-trip a single sync invocation against the existing (deprecated) dispatchers first, to validate the transport in isolation before touching dispatch. Decision point: if this round-trip doesn't land cleanly, fall back to the JeroMQ renovation path inJEROMQ.mdinstead of continuing.CompletionStageinvocations, best-effort cancellation (interrupt server-side, at-most-once delivery client-side), per-call timeouts, a per-peer session cache with lazy dial + coalesced concurrent dials, a configurable graceful-drain timeout (default 30s), and a 10 MiB per-frame cap. No outbox, no reconnect-with-buffering -- K8s SRV-gated discovery plus dial-on-demand plus registry health-pruning already cover what buffering would have bought us, and buffering would actively conflict with the health-pruning contract.ElementRegistry-based dispatch. Replace the three deprecatedLocalInvocationDispatcherimplementations with one that resolves throughElementRegistry, via a newDeploymentId(deterministic hash of a deployment's own id) as an O(1) key intoactiveDeployments, with a merge-scan fallback acrossElementRuntimeService.getActiveRuntimes()when wildcarded.ApplicationId-based scoping is dropped as legacy.ClusterElementLocator+ addressing. NewElementAddresstype insdk-cluster, built fromsdk-cluster's existingPathtype via composition (not subclassing --Pathisfinal), reusingElementServiceKey's#-separated encoding for service+name. Explicit per-method@RemotelyInvokableopt-in (default-deny) --@ElementServicealone does not make a method remotely callable.SessionSecretHeader, validated through the existingSessionDao/SessionServicechain against the shared MongoDB (every instance already has DB access, so the database is already the shared trust anchor). User-initiated cross-instance calls forward the caller's real session; pure control-plane calls (health/discovery) use a provisioned per-instance system/serviceUser+ mintedSession.InstanceDiscoveryServiceimplementations into the new WS registry.elements.rt.transportconfig flag, defaultjeromqfor one release thenws, both exercised in CI during the transition.rt-*-jeromqmodules and related config; keepJEROMQ.mdfor historical reference./cluster/v1frame-format docs.app-node. Fold cluster-node hosting intojetty-wsbehind a new operational-mode CLI flag; delete the separateapp-nodeexecutable (already confirmed dead/broken code, unmaintained for a long time).Independent side-quest, land regardless of transport outcome: replace the unimplemented
InstanceMetadataContext.getInstanceQuality(): doublescore (no production implementation exists today -- the only impl in the tree is a test mock returning a random double) with concreteavailable/totalresource accounting: aResourceBudget/ResourceClaimSPI, local optimistic debit between the registry's ~30s refresh cycles, sorting candidates by absolute available headroom (not ratio), and a configured (not auto-detected)totalper instance. Full design inCLUSTER-CAPACITY.md. This lives inrt-server-cluster, is transport-independent, and should land early since it removes a known-broken piece from production sooner.Naming. New modules drop the
rt-server-/rt-prefix (cluster-ws,cluster-ws-guice,cluster-node-ws); the URL path becomes/cluster/v1/...(not/rt/v1/...).rt-*-jeromqmodules keep their names through retirement to avoid rename churn on code that's going away anyway.Suggested feature name: "Elemental Conduit" -- a conduit carries something between two points, which is exactly what this is (cluster RPC transport), and it pairs naturally with the existing "Elements" branding. Alternates considered: "Elemental Current", "Elemental Circuit".
References:
CLUSTER-RPC-PLAN.md,RT-TRANSPORT-COMPARISON.MD,CLUSTER-CAPACITY.md,APP_NODE_IMPLEMENTATION_PLAN.md(all at repo root).