Skip to content

Take endpoint-libs 3.2 in blitz-control-protocol (0.5.3) - #24

Merged
pathscale merged 17 commits into
masterfrom
feat/qa-inventory-root
Sep 23, 2026
Merged

pathscale merged 17 commits into
masterfrom
feat/qa-inventory-root

Conversation

@pathscale

Copy link
Copy Markdown
Owner

3.2 removed transport::framed_json along with the tokio flavour of the transport. framed_json_neutral replaces it but takes a futures_io stream, and every caller in this workspace holds a tokio one.

So framed_json stays as this crate's own function, same name and shape, wrapping framed_json_neutral over tokio_util::compat. client.rs, server.rs and ps-qa are untouched by the change and the codec on the wire is unchanged.

Gated on server or client, the two features that put tokio in the graph, with tokio-util joining both. karen takes this crate with default features off precisely to get the tokio-free build; leaving the shim ungated would have put tokio back into it.

0.5.3 so a consumer resolving from the registry rather than by path can pick the fix up.

The resolved endpoint-libs moves 3.0.0 to 3.2.1. The requirement was already ^3 and needed no edit: the lock was stale, which is what made this look like a pin problem rather than a removed function.

Verified: cargo check --all-targets and cargo clippy clean.

meh added 16 commits September 23, 2026 11:59
Inspect, Act, Relaunch and Quit cannot name a second URL. Navigate
carries one; the document core refuses it because a fetch belongs to
the embedder. 0.5.2 so chuzz can path-dep this checkout until it
publishes.
… wire

keywords, summary, preview, structure and content, each reporting the nodes
and bytes it dropped as a ratio so a caller can tell whether widening helps.
Summary is extractive: sentences the page contains, checkable against it. The
keyword ranker carries keyword_margin, terms against an unrelated page of the
same kind, because a raw score without a null is decoration.

Response is level-shaped (Condensation), so a caller asking for keywords is
not handed the page's text; the cache holds every level per settle key so
widening is a second cheap request rather than a second settle. Keyed on URL
and settle revision, never the fetch.

DocumentControl answers it too, so the document core and the headless host
agree.
3.2 removed `framed_json` along with the tokio flavour of the transport.
`framed_json_neutral` replaces it and takes a `futures_io` stream, which every
caller in this workspace does not have: they hold tokio `UnixStream`s and a
tokio duplex in the round-trip test.

So the `compat` bridge lives in one place. `framed_json` stays as this crate's
own function with the same name and the same shape, wrapping
`framed_json_neutral` over `tokio_util::compat`, and `client.rs` and `ps-qa` are
untouched by the change. The codec on the wire is unchanged.

It is gated on `server` or `client`, the two features that put tokio in the
graph, and `tokio-util` joins both. A build that only reads the semantic tree
has no socket to frame, and karen takes this crate with default features off
precisely to get that build; leaving the shim ungated would have put tokio back
into it.

The resolved endpoint-libs also moves 3.0.0 to 3.2.1. The requirement was
already `^3` and needed no edit: the lock was simply stale, which is what made
this look like a version pin problem rather than a removed function.
Missed in the previous pass: `server.rs` imported `framed_json` from
endpoint-libs directly, which 3.2 removed along with the tokio flavour of the
transport. It uses the shim in `lib.rs` now, the same one `client.rs` and ps-qa
already use, so the compat bridge stays in one place.

0.5.3, because a consumer resolving this from the registry rather than by path
needs a release to pick the fix up.
Groundwork for taking tokio out of the control socket. The two tokio channels in
this crate's public API are not carrying queues: a watch retains one revision
and a oneshot carries one answer, so what was being used in both cases was a
retained slot and a wake. nagoya ships both halves, an RwLock and a Notify, and
deliberately ships neither channel.

Latest replaces the watch. A reader that falls behind skips the revisions it
missed rather than queueing them, which is the property the event stream wanted:
inspection stays bounded when a client stalls or an animation presents faster
than the client consumes.

Once replaces the oneshot, including the part that matters most: a sender going
away calls abandon, so a caller waiting on an answer that will never come gets
None instead of parking. A control request whose runtime went away has to fail,
not hang the client.

Both register the waiter with enable before reading the slot, which is the order
nagoya documents on Notified::enable: notify_waiters stores no permit, so a
waiter that reads first can lose a transition landing between the read and the
first poll. Once keeps its value and its finality flag under one lock rather
than two, because two cannot be read together: a receiver could find the slot
empty, lose a race to send, then read a finality flag that is now true and
report None while the value sits there.

Four tests, one per property, including the race enable exists for.
The socket path was world-reachable between bind and chmod, and it was not a
theoretical window. A Unix socket is created by bind already listening, so a
mode applied afterwards is always late, and the directory under /tmp had
whatever create_dir_all gives.

The access control moves to the directory, set to 0700 before anything inside it
exists. Ordering stops mattering: a process that cannot traverse the directory
cannot reach the socket whatever the socket's own mode says. The socket keeps
0600 too, because defence resting on one check is defence resting on nobody ever
relaxing the directory.

The transport is nagoya's. TcpListener::bind takes an Addr and Addr::Path is a
Unix socket, which is why there is no UnixListener in nagoya to look for: one
socket type, the address decides the family. Binding moved onto the thread that
polls it, since nagoya cannot adopt a raw fd and a socket only makes progress
while its own reactor is polled.

Connections are held in a FuturesUnordered rather than spawned. nagoya's TaskSet
never removes a finished task's entry, so a set fed by unbounded connection
churn grows one entry per connection ever accepted, and a QA harness reconnecting
in a loop is exactly that churn.

The watch and the oneshot in the public API are gone. ControlBridge returns an
Arc<Once<DebugResponse>>; start_with_events takes an Arc<Latest<DebugEvent>>.
Latest carries a revision and each connection holds its own cursor, which is
what watch tracks internally: two readers sit at different places in the stream
and one catching up must not move the other. That is also what preserves the
rule the old borrow_and_update kept, that an action is not answered with a paint
from before it.

Shutdown is a Flag rather than a oneshot, because Drop is not async and cannot
move a sender out to fire it.

Tests still use tokio and are converted next.
The server path is nagoya end to end and the crate's server feature no longer
reaches tokio at all: cargo tree -e normal -i tokio prints nothing for it.

Binding moved back into start_inner, which is where it belongs. Doing it on the
server thread was a race the tests caught at once: start returned with a path
that nothing was listening on yet, so a caller that connected immediately got
ENOENT. Listener::bind needs no reactor handle, so the socket is listening
before start returns and the thread only registers it, which is the part that
genuinely has to happen where the polling does.

The seven socket tests are the reason to trust any of this, so they were
converted rather than dropped: each drives a real server over a real socket. A
test client needs a reactor of its own, because a nagoya socket only makes
progress while the reactor it was created on is polled, and the server is
polling its own on another thread. One shared Reactor::start covers all of them.

Once gains a synchronous fill. The bridge closure is not async, so a host that
already holds the answer has to reply without awaiting; contention is only
against a receiver taking the value, so try_write either succeeds at once or
means the answer was already delivered.
The client speaks to a nagoya socket now, like the server: Addr::path for the
address, timeout and sleep from nagoya, and std Instant for deadline
arithmetic, which never needed a runtime clock in the first place.

The inspector connection gets a process-wide reactor rather than one per
connection. A library does not own the caller's thread and cannot assume one is
polling anything, and a thread per connection would be a thread per command in a
QA sweep.

With both halves converted, neither server nor client pulls tokio: cargo tree
-e normal -i tokio prints nothing for either feature. tokio and tokio-util move
behind tokio-framing, which exists only for the framed_json shim that ps-qa and
chuzz still need for their own tokio streams. Depending on this crate no longer
puts tokio in a graph that had got rid of it.

The connect-retry test is kept and converted, because the race it covers is real:
a descriptor can be visible before its socket is bound, and the client has to
retry rather than fail. The duplex framing test stays on tokio behind the same
feature, since the pipe it needs has no nagoya equivalent and the codec it
checks is the same one either way.
framed_json was a bridge for consumers holding tokio streams, and after the
server and the client both moved there were none left in this workspace. ps-qa's
silent-host fixture was the last, and it binds a control socket, so it binds the
same nagoya one the real server does rather than keeping a tokio listener alive
to be bridged.

The neutral framing and NagoyaStream are re-exported here instead. A consumer
that serves or fakes this protocol names both halves of a connection through
this crate rather than depending on endpoint-libs and nagoya to do it.

The duplex round-trip test goes with the shim. It existed to exercise the tokio
bridge, and the codec it checked is covered by the six socket tests that drive a
real server over a real socket.

tokio and tokio-util are gone from the manifest entirely, dependencies and dev
dependencies alike. The only mentions left in the crate are three comments
explaining what is no longer there.
Latest gains set_now, the synchronous twin of set. A paint lands in a window
event handler, which is not async and has no runtime to await on, so the only
way to record one was to have an async setter in a sync callback.

try_write either succeeds at once or means a reader holds the slot for an
instant, and dropping that revision is correct rather than unfortunate: this
keeps only the newest value anyway, so a caller that loses the race is
overwritten by the next paint a frame later.
The server thread returned silently when its reactor or the listener
registration failed, so an app launched with --blitz-control advertised
nothing and gave no reason. Both failures now go to stderr.
The inspector connection is a nagoya socket, and ps-qa awaited it from
`#[tokio::main]`. Two runtimes: the request was written and the reply's
readiness was never seen by the executor polling it. Its 43 `tokio::time`
sleeps and deadlines had the same problem, since nagoya drives no tokio timer.

main runs `nagoya::block_on`, sleeps are `nagoya::sleep`, deadlines are plain
`std::time::Instant` arithmetic, and the one test that joined the silent host
with the client does it with `futures::join!` under `nagoya::block_on`. tokio
is no longer a dependency of ps-qa.
`Once::fill` gave up when `try_write` failed, on the reasoning that contention
meant the answer was already delivered. It does not: `recv` holds the write
lock while it looks at an empty slot, so a reply filled in that window was
discarded and the receiver waited for one that never came, the same hang as a
reply that was never sent. The receiver holds the lock only to read two fields,
so fill now retries until it gets it.
The test module named MessageStream and TransportStream directly and then
imported `super::*`, which re-exports both. Clippy with -D warnings rejects
the duplicates, which failed the Linux protocol job.
The transport is nagoya end to end now; the feature comment still named tokio.
ps-qa now runs on nagoya and no longer depends on tokio.
@pathscale
pathscale force-pushed the feat/qa-inventory-root branch from 3597889 to d66cfb6 Compare September 23, 2026 04:59
nagoya, ps-blitz, ps-anyrender and blitz-control-protocol were required at a
patch floor. With no lockfile the newest release resolves regardless, so the
floor pinned nothing useful and only had to be chased on every release.
@pathscale
pathscale merged commit 2a5ab50 into master Sep 23, 2026
3 checks passed
@pathscale
pathscale deleted the feat/qa-inventory-root branch September 23, 2026 11:45
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