Skip to content

Release the nagoya socket work as 3.1.0 - #50

Merged
pathscale merged 7 commits into
masterfrom
feat/nagoya-socket
Sep 21, 2026
Merged

pathscale merged 7 commits into
masterfrom
feat/nagoya-socket

Conversation

@pathscale

Copy link
Copy Markdown
Owner

Five commits that were already written, plus the version bump that lets anything
depend on them.

Why this is blocked today

The manifest said 3.0.3, and crates.io already holds a 3.0.3 with different
content. So this work had no version number that identified it, and the one
consumer that needs it, karen, had to reach the crate by path. That path
dependency is the last one in karen that cannot be argued away.

A new minor rather than a re-cut 3.0.3: two crates sharing one version number
would hand anyone who already resolved the old one a silent behaviour change.

What is here

  • nagoya-transport (src/libs/ws/transport.rs:93-100), gating pub mod nagoya and re-exporting NagoyaStream, which is what gives
    framed_json_neutral over a nagoya socket. Declared at Cargo.toml:70.
  • serve_with without spawn_local (src/libs/ws/server.rs:262,
    :176-181, src/libs/ws/session.rs:366), driving connections on
    FuturesUnordered so a nagoya reactor can drive it. The published serve_with
    spawn_locals and panics outside a tokio LocalSet.

What this does not do

It does not remove tokio from this crate. Hyper's upgrader still
spawn_locals onto TokioExecutor (src/libs/ws/server.rs:179,
src/libs/ws/transport.rs:25), so the ws path still needs it.

The honest claim is: the nagoya-transport path is tokio-free, the ws path is
not. karen is clean only because it takes default-features = false and drops
ws, which drops tungstenite, rustls, hyper, reqwest and webpki-roots with it.
Please do not let this land described as "endpoint-libs no longer needs tokio".

After merge

Publishing 3.1.0 lets karen replace path = "../endpoint-libs" with
version = "=3.1.0", and unblocks agentcode and moe-pgo when their own tokio
removal starts. All three currently either carry a path dependency or sit on the
2.1 line.

Linear history, no merge commits.

meh added 6 commits September 17, 2026 16:42
`framed_json` is bound on `tokio::io::AsyncRead + AsyncWrite`, so a consumer
that does not run tokio cannot reach the framed transport at all, even though
`Transport` itself is a blanket impl over `Stream + Sink` and names no runtime.
The seam was already neutral; only the one implementation behind it was not.

So this adds `framed_json_neutral`, the same transport over
`futures::io::AsyncRead + AsyncWrite`. That trait pair is the neutral one:
tokio adapts to it through `tokio-util`'s `Compat`, and Nagoya through
`nagoya::io::Compat`, so a caller can name a byte stream without naming a
runtime. The tokio path is untouched and remains the default.

`tokio_util`'s `Framed` is what the tokio path gets for free and there is no
`futures-util` equivalent, so the buffering is written out here: the same two
buffers, one accumulating what has been read but not yet framed, one holding
what has been encoded but not yet written. An oversized length prefix is
refused before the body is buffered, which is what keeps a peer from naming
four gigabytes and being believed.

`encode` and `decode` are shared with the tokio path rather than reimplemented,
so the bytes are identical by construction. A test asserts that anyway, because
the format is normative for non-Rust peers and there are now two
implementations that could drift.
`framed_json_neutral` asks for futures-io, which was the right seam and not yet a
reachable one: Nagoya's socket implements Nagoya's own `Stream`, whose `read` and
`write_all` are `async fn`, and `nagoya::io::Compat` runs the other way, presenting
a futures-io stream to a Nagoya consumer. The doc comment claiming that Compat
closed the gap was wrong and is corrected here.

`NagoyaStream` closes it instead, and is thin because `TcpStream` already exposes
`poll_read`, `poll_write`, and `poll_flush` publicly with exactly the futures-io
signature. So this delegates rather than bridges: no boxed future, no second
buffer, no duplicate readiness logic. Had those methods been private, a wrapper
would have had to poll an `async fn` borrowing `&mut self` from inside `poll_read`,
which is not soundly possible.

`poll_close` flushes and stops there, because Nagoya exposes no half-close. That is
said outright rather than implied, since a peer waiting to see end-of-stream before
the drop needs a half-close on the Nagoya side first.

The test sends a `WireMessage` both ways over a Unix socket on Nagoya's reactor and
executor, with no tokio runtime started. A compile would not have shown this: the
adapter's whole job is readiness, so the message has to travel.

The feature is off by default and additive; the tokio path is untouched, and the
dependency resolves from the registry: 0.1.9 is the published release that already
speaks AF_UNIX and carries `TaskSet`.
Connections are polled in place with FuturesUnordered so a nagoya
reactor can run the accept loop. spawn_local required a tokio
LocalSet even when the listener was not tokio.
serve_with already drives connections with FuturesUnordered so a nagoya
reactor can run the accept loop. Request handlers in the session still
spawn_local'd, which is why Karen wrapped that loop in a LocalSet.

Poll those bodies on the session task the same way: a slow hook cannot
stall the read loop, one thread, no tokio task spawn. TCP listen is
unchanged.
Connections are polled in place with FuturesUnordered, matching serve_with.
A LocalSet remains only because the hyper upgrader still spawn_locals onto
TokioExecutor. nago-wss is the tokio-free replacement for that backend.
The five commits on this branch add a feature and change runtime behaviour, and
the manifest still said 3.0.3, which crates.io already holds with different
content. Anything depending on this work therefore had no version to name it
and had to reach the crate by path.

A new minor rather than a re-cut 3.0.3: two crates sharing one version number
would give anyone who already resolved the old one a silent behaviour change.
@pathscale

Copy link
Copy Markdown
Owner Author

Local gate, run before asking for review rather than leaning on CI.

cargo build                                          ok
cargo build --no-default-features \
  --features ws-core,framed-transport,nagoya-transport  ok
cargo test                                           58 passed, 0 failed
cargo test --no-default-features \
  --features ws-core,framed-transport,nagoya-transport  98 passed, 0 failed
cargo fmt --all -- --check                           clean
cargo clippy --no-default-features \
  --features ws-core,framed-transport,nagoya-transport -- -D warnings   clean

The feature is genuinely proven end to end, not just compiled:

test a_wire_message_crosses_a_nagoya_unix_socket_in_both_directions ... ok

A wire message across a real nagoya unix socket in both directions, plus the
libs::ws::transport::nagoya doctest compiling.

One thing a reviewer should know

A plain cargo test does not test this feature, and does not say so.
Under default features tests/nagoya_transport.rs compiles to nothing:

Running tests/nagoya_transport.rs
running 0 tests
test result: ok. 0 passed; 0 failed

That reads as a pass. The 40-test difference between the two runs above is the
measure of it. tests/transport_seam.rs behaves the same way and still reports
0 with this feature set.

Not introduced by this branch and not fixed here, because doing so would widen
a release into a test-harness change. Raising it because the feature this PR
exists to ship is one of the things a default run silently does not check, so
whatever gates this crate should name the feature set explicitly.

The workflow fired on every push to master and every pull request. The gate is
the one an author runs locally before asking for review; a tick that arrives
without being asked for is a tick people stop reading.

workflow_dispatch keeps it runnable from the Actions tab when somebody wants it.
The runner is unchanged.
@pathscale

Copy link
Copy Markdown
Owner Author

Added 3f3f26d: CI is manual now.

rust.yml fired on every push to master and every pull request, including this
one. It is workflow_dispatch only, still runnable from the Actions tab, runner
unchanged. The run this PR triggered was cancelled.

Folded in here rather than split out because this repository keeps one open pull
request at a time. It is a trigger change and nothing else; no job, step or
runner was touched.

@pathscale
pathscale merged commit 405dc5b into master Sep 21, 2026
@pathscale
pathscale deleted the feat/nagoya-socket branch September 21, 2026 22:01
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