Release 0.8.0 with replay-safe prioritized transport

This commit is contained in:
sergeych committed 2026-10-21 16:35:20 +04:00
1 parent 237d404bcd
commit 009d62a155
31 files changed
+1188 -130

No files matched your search

+46 -9
View File
@@ -1,4 +1,4 @@
# Numeric transport priority — 0.7.2-SNAPSHOT
# Numeric transport priority — protocol 2
```kotlin
import net.sergeych.kiloparsec.call
@@ -8,13 +8,15 @@ remote.call(command, arguments, priority = 100)
remote.push(notification, arguments, priority = 0)
```
Larger signed `Int` values go first. Equal priorities preserve FIFO. Existing
Larger signed `Int` values go first within the allowed nonce range. Equal
priorities use nonce order for encrypted packets and FIFO for unnumbered packets. Existing
call/push entry points remain unchanged and use zero unless invoked inside an
explicit priority call context. The overloads are extension functions, preserving
existing RemoteInterface implementers. A coroutine context carries the selected
value through the encrypted RPC wrappers; it becomes serialized outer-block data.
The adapter owns one outgoing writer. Waiting for a response does not occupy the
The transport owns a nonce-aware outgoing writer and submits one block at a time
to the adapter, preserving the selected order through the carrier. Waiting for a response does not occupy the
writer: calls retain independent request IDs and response waiters. Both transport
responses and errors copy the request's priority, including encrypted RPC errors
returned inside an outer response. Pushes have no normal response.
@@ -30,10 +32,8 @@ existing length prefix, WebSocket messages and UDP data blocks provide that
boundary. The old decoder consumes the legacy body and ignores trailing bytes.
Do not use this EOF-terminated extension to concatenate unframed Block objects or
embed them directly in a positional collection; wrap each block in its own bytes.
The handshake remains version 1 because the original wire format is retained.
Old peers interoperate but do not honor/inherit priority. New peers can decode
both formats. BiPack @Extendable is deliberately not added: it would insert a new
The priority trailer itself retains its legacy encoding. The secure handshake
requires protocol 2; this does not enable communication with protocol-1 peers. BiPack @Extendable is deliberately not added: it would insert a new
field-count prefix before existing fields and break old readers.
## Adapter behavior
@@ -45,7 +45,8 @@ it does not deserialize/copy/decrypt the payload or guess from the last four byt
or legacy devices. Built-in ProxyDevice-based adapters override it to enter their
writer directly. The old output channel remains available, bridged into that writer;
new transports should call send so pending senders are visible to priority ordering.
Custom adapters retain their existing FIFO behavior until they implement priority.
Transport callers now receive nonce-aware scheduling even with a custom FIFO
adapter. Direct adapter users retain the adapter-level scheduling behavior.
Queue admission is bounded to 128 waiting blocks and 32 MiB (plus the block
currently being written). Overflow fails explicitly rather than allocating an
@@ -56,7 +57,43 @@ batch-collection delay. WebSocket writes flush after each block to avoid excessi
look-ahead in the engine's FIFO. Socket/TLS buffers and network loss can still delay
control behind bytes already submitted; priority is not a preemption guarantee.
## Verification
## Nonce-aware admission and fairness
`OutQueue.push(message, nonce, priority)` records scheduling metadata locally; no
new wire fields are added. `pop()` finds the smallest queued nonce and selects the
highest-priority message no more than 2730 positions ahead of it. This is two
thirds of the 4096-position receive replay window. Gaps count as nonce distance,
not as transmitted packets. The bound prevents an old encrypted message from
being starved until it becomes stale. It leaves headroom for external reordering,
but does not promise acceptance after arbitrary network delay or reordering.
A transport's preparation mutex covers nonce allocation, encryption, final block
encoding and queue admission. Encryption remains in the secure-layer callbacks.
All encrypted paths (identity/finished, calls, pushes and replies) use this path.
The mutex is released before waiting for transmission. Full queues reject
admission immediately; there is no capacity wait while holding this mutex.
Cancellation may leave a nonce gap but never reuses a nonce.
The transport writer serializes carrier submissions, so a downstream adapter
cannot reorder these submissions again. Public direct writes to an adapter's
output channel bypass this transport contract.
Large-message preparation is not preemptible by priority. A local JVM probe with
concurrent 1 MiB and 1 KiB payloads measured preparation including mutex waiting
at medians of about 87 ms for both sizes (maximum about 118 ms). This includes
encoding and allocation, not just cipher time; it is a local observation, not a
throughput or latency guarantee.
## Current verification
Focused tests cover replay-window boundaries and bitmap wraparound, concurrent
nonce admission in both directions, duplicate/stale delivery, local send failure
recovery, cancellation/capacity/close, and an encrypted 4098-message backlog whose
old low-priority message remains acceptable. The saturated encrypted-link test
continues to pass. Kotlin JVM tests, JS/Wasm compilation, TypeScript tests and
TypeScript-to-JVM interoperability were checked for this change.
## Earlier priority-release verification
Focused JVM tests cover signed numeric ordering, stable ties, queued cancellation,
bounded admission, close, absent/partial trailers, unchanged legacy bodies, and