Expand description
Connections, framing and buffers: the seam between the loop and the commands.
yo-reactor knows how to run a batch and nothing about what a command is.
dispatch knows how to run a command and nothing about where the bytes came
from. This module is the piece in between, and it is the piece a server is
missing until it exists: the read buffer a command’s arguments point into,
the framing that says where one command ends and the next begins, the reply
buffer that holds an answer until the batch is done, and the state a
connection keeps between the two.
§Two halves
Wire is a pair rather than a thing. The connection half is the front,
and it is in a module of its own that cannot name a Server: the buffers,
the decoder pool, the framing, the sessions and the queue of framed work.
The other half is the server, which is the databases and the numbers INFO
reports. The line matters because it is the line a second thread runs along:
a front belongs to the thread that accepted its connections and is reached by
nothing else, and the server is what the threads come to share. Everything
that needs both is a method on Wire and there are three of them, which are
running a command, answering a client that blocked and forgetting a client
that has gone.
§What a piece of work is
Cmd is three numbers: which connection, which decoder holds the
arguments, and where in that connection’s buffer they point. It is Copy
and twenty four bytes, so it crosses an intake lane without touching the
heap, and it carries no borrow, which is what lets the reactor hold sixty
four of them while the engine owns the bytes they name.
The decoders are pooled. Framing takes one out of the pool per command,
run puts it back, and a connection with a half read command keeps hold of
one so that a bulk arriving in ten reads is decoded once rather than ten
times. In the steady state the pool is as large as the deepest batch and
nothing here allocates at all.
§One write per connection
Replies accumulate in the connection’s Out and go out
in Wire::flush, which is one call to the sink per connection touched by
the batch and never one per reply. That is the syscall shape 04 section 2
asks for, and it is the one aki got wrong: its HGETALL profile spent 69.7
percent of its time in write syscalls.
§What is not here
Sockets. Sink is where the bytes go and the io_uring reactor implements
it later, which keeps this module testable without a network and keeps the
ring out of the crate that parses the protocol.
The hash the first walk computes warms the bucket and is then thrown away,
because yo-kv’s commands take keys rather than hashes. The prefetch is the
part that is worth a cache miss; hashing a short key twice is a few
nanoseconds, and removing the second one means a hashed form of every
command method, which is a change to make with a benchmark rather than on
the way past.
use yo_resp::engine::{Recorder, Wire, pump};
use yo_reactor::Reactor;
let mut r = Reactor::inline(Wire::new(Recorder::new()));
let conn = r.engine_mut().accept();
r.engine_mut().feed(conn, b"*3\r\n$3\r\nSET\r\n$1\r\nk\r\n$1\r\nv\r\n*2\r\n$3\r\nGET\r\n$1\r\nk\r\n");
let mut batch = Vec::new();
assert_eq!(pump(&mut r, &mut batch), 2);
assert_eq!(r.engine().sink().sent(conn), b"+OK\r\n$1\r\nv\r\n");Structs§
- Cmd
- One framed command, waiting to run.
- Recorder
- A sink that keeps everything, for tests and for a driver with no socket.
- Wire
- The engine: connections on one side, the command layer on the other.
Traits§
- Sink
- Where replies go.
Functions§
- pump
- Run everything that is framed, in batches, and write the replies.
Type Aliases§
- ConnId
- Which connection. An index, reused after a connection closes.