// Single-connection memcached client.
//
// The client keeps one [`Connection`] open and moves requests over it one at a
// time: write the encoded request, then pull one complete response off the
// decoder. [`Client::execute`] keeps that request/response pairing obvious for
// a single command, while [`Client::pipeline`] writes a whole batch before it
// reads any of the replies.
///|
/// A client bound to a single connection.
pub struct Client {
conn : &Connection
decoder : Decoder
}
///|
/// Wrap an open connection.
pub fn Client::new(conn : &Connection) -> Client {
{ conn, decoder: Decoder::new() }
}
///|
/// Wrap an open connection with a decoder that accepts `limit` buffered bytes
/// before it declares the stream out of sync.
pub fn Client::with_limit(conn : &Connection, limit : Int) -> Client {
{ conn, decoder: Decoder::with_limit(limit) }
}
///|
/// Send `request` and return the response it produces.
///
/// The reply is read whole before it is returned, so a response split across
/// several packets is reassembled transparently.
///
/// `request` has to be one the server answers; a command whose reply is
/// suppressed by `noreply` has nothing to read, and waiting for it would block
/// until the connection died. Those go through [`Client::send`], and a batch of
/// them through [`Client::pipeline`].
pub fn Client::execute(self : Client, request : Request) -> Response raise {
validate_request(request)
guard request.expects_reply() else {
raise ProtocolError::Malformed(
"this command has no reply to read; use 'send' for it",
)
}
self.conn.write(request.to_bytes().view())
self.read_response()
}
///|
/// Pull one whole response off the connection, reading until it is complete.
///
/// A read may deliver half a response or several of them, so the decoder is
/// asked first and only fed when it is still waiting for the rest. A read that
/// returns no byte means the peer hung up mid-response: there is nothing left to
/// frame, so the stream is reported as broken rather than as a short reply.
fn Client::read_response(self : Client) -> Response raise {
while true {
match self.decoder.next() {
Some(response) => return response
None => {
let chunk = self.conn.read(READ_CHUNK_SIZE)
if chunk.length() == 0 {
raise TransportError::Io("server closed the connection mid-response")
}
self.decoder.feed(chunk.view())
}
}
}
abort("unreachable")
}
///|
/// Write `request` without reading an answer back.
///
/// Only accepts the commands that produce no reply: `quit`, and a mutation
/// that carries `noreply`. Anything else has to go through
/// [`Client::execute`], otherwise its reply would stay in the stream and be
/// mistaken for the answer to the next command.
pub fn Client::send(self : Client, request : Request) -> Unit raise {
validate_request(request)
guard !request.expects_reply() else {
raise ProtocolError::Malformed(
"this command has a reply to read; use 'execute' for it",
)
}
self.conn.write(request.to_bytes().view())
}
///|
/// Run `requests` as one pipeline: write every request, then read one reply back
/// for each request the server answers.
///
/// The result holds one element per request, in the order the requests were
/// given: `Some(response)` for a request that has a reply, and `None` for one
/// whose reply the protocol suppresses, which is a mutation carrying `noreply`.
/// Pairing is therefore the index, and the question it asks is
/// [`Request::expects_reply`], so the caller never counts replies by hand.
///
/// Writing the whole batch before reading any of it is where the round-trip
/// saving comes from: the server works on the requests behind the one whose
/// reply is travelling back. Nothing is validated halfway either - every request
/// is checked before the first byte goes out, so a batch holding a bad request
/// leaves the connection untouched.
///
/// `quit` is refused wherever it appears in a batch. The server stops answering
/// once it reads one, so a request behind it would be dropped with nothing to
/// report the loss, and closing is [`Client::quit`]' job anyway.
///
/// A `Remote` failure is itself a whole response line, so the framing is intact
/// and the server still answers every remaining request; the replies after it
/// are read and dropped before the error is raised, because leaving them in the
/// stream would hand them to the next call as its own answers. Their content is
/// lost along the way, so a batch whose every reply has to be accounted for is
/// better run as one [`Client::execute`] per request.
///
/// A failure part way through the batch leaves the connection spent, and the
/// batch stops there. Either the write itself failed, in which case the requests
/// that did go out are answered by the server regardless, or a reply could not be
/// framed at all, in which case the loop stopped before reading the rest. Both
/// leave replies on the wire that no call here will take back: a later call on
/// the same [`Client`] would read them as its own answers, and
/// [`Client::resync`] only clears the decoder, never the wire. The owner has to
/// drop it and build a new one.
///
/// An empty batch writes nothing, reads nothing and returns nothing: building
/// the list request by request is a normal way to use this, and the empty list
/// has no command to send.
pub fn Client::pipeline(
self : Client,
requests : Array[Request],
) -> Array[Response?] raise {
// The batch is a snapshot, because validation and the writes below have to see
// the same requests: `Array` is mutable, so a batch edited between the two
// passes would let a request through unchecked and then put it on the wire.
let batch = requests.copy()
validate_pipeline(batch)
// Whether the server answers a request is decided once, next to the write it
// belongs to, and that flag list is the only thing read back afterwards. A
// second pass over the batch would have to reach the same answer again, and
// nothing here would hold it to that.
let wanted : Array[Bool] = []
for request in batch {
self.conn.write(request.to_bytes().view())
wanted.push(request.expects_reply())
}
self.read_replies(wanted)
}
///|
/// How many replies `wanted` still asks the server for from `from` on.
fn remaining_reply_count(wanted : Array[Bool], from : Int) -> Int {
let mut count = 0
let mut index = from
while index < wanted.length() {
if wanted[index] {
count = count + 1
}
index = index + 1
}
count
}
///|
/// Read one response per `true` in `wanted`, and return a list as long as
/// `wanted`: `Some(response)` where the flag is set, `None` where the server was
/// asked not to answer. Pairing a reply with its request is therefore the index,
/// and a silent request costs no read at all.
///
/// A `Remote` failure is a response of its own: the stream keeps its framing and
/// the server still owes one reply per remaining request, so those replies are
/// read and dropped before the error is raised, which leaves the connection
/// ready for the next call. Any other failure says the bytes cannot be framed at
/// all, so reading on would only pile up whatever follows a stream that is
/// already lost; that error is raised as it came, and the batch ends with replies
/// still unread on the wire, which makes the connection spent - see
/// [`Client::pipeline`] for what the caller has to do about it.
fn Client::read_replies(
self : Client,
wanted : Array[Bool],
) -> Array[Response?] raise {
let responses : Array[Response?] = []
for _ in wanted {
responses.push(None)
}
let mut slot = 0
while slot < wanted.length() {
if wanted[slot] {
let reply = self.read_response() catch {
err =>
match err {
Remote(_, _) => {
self.discard_replies(remaining_reply_count(wanted, slot + 1))
raise err
}
_ => raise err
}
}
responses[slot] = Some(reply)
}
slot = slot + 1
}
responses
}
///|
/// Read and drop `count` responses.
///
/// This is how a pipeline keeps its stream framed once one of its replies had to
/// be reported as a failure: the server still sends one reply per remaining
/// request, and those bytes would otherwise be waiting in the stream to be read
/// as the answers of some later call.
///
/// A failure while dropping is swallowed. The replies are of no use to whoever
/// is about to hear why the pipeline stopped, and that first error is the one
/// that explains the batch - the same reason [`Client::quit`] swallows a failed
/// close. Dropping also stops at that failure, because a stream that just
/// refused to produce a reply has nothing left worth reading.
fn Client::discard_replies(self : Client, count : Int) -> Unit {
let mut index = 0
while index < count {
let _ = self.read_response() catch { _ => return () }
index = index + 1
}
}
///|
/// `get` for one or more keys.
pub fn Client::get(
self : Client,
keys : Array[String],
) -> Array[RetrievedValue] raise {
expect_values(self.execute(Request::Get(keys~, with_cas=false)))
}
///|
/// `gets` for one or more keys, including each item's CAS token.
pub fn Client::gets(
self : Client,
keys : Array[String],
) -> Array[RetrievedValue] raise {
expect_values(self.execute(Request::Get(keys~, with_cas=true)))
}
///|
/// `gat`: fetch `keys` and reset the expiry of every hit to `exptime`.
pub fn Client::gat(
self : Client,
keys : Array[String],
exptime : Int,
) -> Array[RetrievedValue] raise {
expect_values(self.execute(Request::Gat(keys~, exptime~, with_cas=false)))
}
///|
/// `gats`: like [`Client::gat`], but each block carries its CAS token.
pub fn Client::gats(
self : Client,
keys : Array[String],
exptime : Int,
) -> Array[RetrievedValue] raise {
expect_values(self.execute(Request::Gat(keys~, exptime~, with_cas=true)))
}
///|
/// Run a storage command with explicit flags, expiry and CAS token.
pub fn Client::store(
self : Client,
op : StorageOp,
key : String,
value : Bytes,
flags : Int,
exptime : Int,
cas : UInt64?,
) -> Status raise {
expect_status(
self.execute(
Request::Storage(op~, key~, flags~, exptime~, value~, cas~, noreply=false),
),
)
}
///|
/// `set`: store `value` under `key`, ignoring any existing value.
///
/// `flags` and `exptime` default to `0`; pass either to override.
pub fn Client::set(
self : Client,
key : String,
value : Bytes,
flags? : Int = 0,
exptime? : Int = 0,
) -> Status raise {
self.store(StorageOp::Set, key, value, flags, exptime, None)
}
///|
/// `add`: store `value` under `key` only if the key does not exist yet.
///
/// `flags` and `exptime` default to `0`; pass either to override.
pub fn Client::add(
self : Client,
key : String,
value : Bytes,
flags? : Int = 0,
exptime? : Int = 0,
) -> Status raise {
self.store(StorageOp::Add, key, value, flags, exptime, None)
}
///|
/// `replace`: store `value` under `key` only if the key already exists.
///
/// `flags` and `exptime` default to `0`; pass either to override.
pub fn Client::replace(
self : Client,
key : String,
value : Bytes,
flags? : Int = 0,
exptime? : Int = 0,
) -> Status raise {
self.store(StorageOp::Replace, key, value, flags, exptime, None)
}
///|
/// `append`: append `value` to the item stored under `key`.
///
/// `flags` and `exptime` default to `0`; pass either to override.
pub fn Client::append(
self : Client,
key : String,
value : Bytes,
flags? : Int = 0,
exptime? : Int = 0,
) -> Status raise {
self.store(StorageOp::Append, key, value, flags, exptime, None)
}
///|
/// `prepend`: prepend `value` to the item stored under `key`.
///
/// `flags` and `exptime` default to `0`; pass either to override.
pub fn Client::prepend(
self : Client,
key : String,
value : Bytes,
flags? : Int = 0,
exptime? : Int = 0,
) -> Status raise {
self.store(StorageOp::Prepend, key, value, flags, exptime, None)
}
///|
/// `cas`: store `value` under `key` only if its CAS token is still `cas`.
///
/// `flags` and `exptime` default to `0`; pass either to override.
pub fn Client::cas(
self : Client,
key : String,
value : Bytes,
cas : UInt64,
flags? : Int = 0,
exptime? : Int = 0,
) -> Status raise {
self.store(StorageOp::Cas, key, value, flags, exptime, Some(cas))
}
///|
/// `delete`: remove `key`.
pub fn Client::delete(self : Client, key : String) -> Status raise {
expect_status(self.execute(Request::Delete(key~, noreply=false)))
}
///|
/// `incr`: add `delta` to the counter stored under `key`.
pub fn Client::incr(
self : Client,
key : String,
delta : UInt64,
) -> UInt64 raise {
expect_counter(self.execute(Request::Incr(key~, delta~, noreply=false)))
}
///|
/// `decr`: subtract `delta` from the counter stored under `key`.
pub fn Client::decr(
self : Client,
key : String,
delta : UInt64,
) -> UInt64 raise {
expect_counter(self.execute(Request::Decr(key~, delta~, noreply=false)))
}
///|
/// `touch`: reset the expiry of `key` to `exptime` without fetching it.
pub fn Client::touch(
self : Client,
key : String,
exptime : Int,
) -> Status raise {
expect_status(self.execute(Request::Touch(key~, exptime~, noreply=false)))
}
///|
/// `version`: the version string the server reports.
pub fn Client::version(self : Client) -> String raise {
expect_version(self.execute(Request::Version))
}
///|
/// `stats`: the general statistics of the server, one entry per `STAT` line.
pub fn Client::stats(self : Client) -> Array[StatEntry] raise {
expect_stats(self.execute(Request::Stats(sub=None)))
}
///|
/// `stats `: one section of the server statistics, such as `items`,
/// `slabs` or `settings`.
///
/// A section nests its dimension into the entry name, so `stats items` yields
/// entries named `items:1:number` and friends.
///
/// `section` may be several words separated by single spaces, which is how the
/// server spells the sections that take an argument: `stats detail on`,
/// `stats cachedump `.
pub fn Client::stats_of(
self : Client,
section : String,
) -> Array[StatEntry] raise {
expect_stats(self.execute(Request::Stats(sub=Some(section))))
}
///|
/// `flush_all`: invalidate every item after `delay` seconds.
///
/// `None` flushes immediately.
pub fn Client::flush_all(self : Client, delay : Int?) -> Status raise {
expect_status(self.execute(Request::FlushAll(delay~, noreply=false)))
}
///|
/// `verbosity `: change how much the server logs.
pub fn Client::verbosity(self : Client, level : Int) -> Status raise {
expect_status(self.execute(Request::Verbosity(level~, noreply=false)))
}
///|
/// `cache_memlimit `: cap the memory the server uses for items.
pub fn Client::cache_memlimit(self : Client, megabytes : Int) -> Status raise {
expect_status(self.execute(Request::CacheMemlimit(megabytes~, noreply=false)))
}
///|
/// `slabs reassign `: move a slab page from class `src` to class
/// `dst`. A `src` of `-1` lets the server pick the source slab itself.
pub fn Client::slabs_reassign(
self : Client,
src : Int,
dst : Int,
) -> Status raise {
expect_status(self.execute(Request::SlabsReassign(src~, dst~, noreply=false)))
}
///|
/// `slabs automove `: set the slab automover - `0` off, `1` on,
/// `2` aggressive.
pub fn Client::slabs_automove(self : Client, mode : Int) -> Status raise {
expect_status(self.execute(Request::SlabsAutomove(mode~, noreply=false)))
}
///|
/// `quit`: ask the server to hang up, then release the local channel.
///
/// The server sends no reply to `quit`, so nothing is read back. The channel is
/// closed even when the command cannot be written, so a failed send does not
/// leak it; the send error is re-raised afterwards. A failure of that closing
/// is swallowed, because the send error is the one that explains why the
/// connection is being given up in the first place.
pub fn Client::quit(self : Client) -> Unit raise {
self.send(Request::Quit) catch {
err => {
self.conn.close() catch {
_ => ()
}
raise err
}
}
self.conn.close()
}
///|
/// Drop every byte the decoder still holds.
///
/// This is the client's handle on [`Decoder::resync`]: once a stream has lost
/// its framing ([`ProtocolError::Desynchronised`]) and is known to have
/// restarted - typically after a reconnect - the buffered fragment has to go
/// before the next [`Client::execute`]. Calling it on a healthy stream
/// discards bytes that were never consumed.
pub fn Client::resync(self : Client) -> Unit {
self.decoder.resync()
}
///|
fn expect_values(response : Response) -> Array[RetrievedValue] raise {
match response {
Values(values) => values
_ => raise ProtocolError::Malformed("expected a retrieval response")
}
}
///|
fn expect_status(response : Response) -> Status raise {
match response {
Status(status) => status
_ => raise ProtocolError::Malformed("expected a status line")
}
}
///|
fn expect_counter(response : Response) -> UInt64 raise {
match response {
Counter(value) => value
_ => raise ProtocolError::Malformed("expected a counter value")
}
}
///|
fn expect_version(response : Response) -> String raise {
match response {
Version(text) => text
_ => raise ProtocolError::Malformed("expected a version response")
}
}
///|
fn expect_stats(response : Response) -> Array[StatEntry] raise {
match response {
Stats(entries) => entries
_ => raise ProtocolError::Malformed("expected a stats response")
}
}
///|
/// How many bytes one read is asked for while a response is being assembled.
///
/// This is a fetch size, not a cap: [`Decoder`] holds the bytes until a whole
/// response is framed, so what may accumulate is bounded by the decoder's own
/// limit (`DEFAULT_BUFFER_LIMIT` by default), never by this number.
const READ_CHUNK_SIZE : Int = 4096
///|
/// Longest key memcached accepts, in bytes.
const MAX_KEY_LENGTH : Int = 250
///|
/// Whether `code` may stand inside a word of a command line.
///
/// The server splits the command line on spaces, so a word may hold anything
/// that is neither a space nor a control character. A key is one such word, and
/// so is every word of a `stats` section.
fn is_word_byte(code : Int) -> Bool {
code > 0x20 && code != 0x7F
}
///|
/// Reject a key that cannot be carried in a command line.
///
/// A key is one word of the command line, so it must be non-empty, no longer
/// than the bytes the server accepts for a key, and free of the bytes that
/// would end the word early. The length is measured in UTF-8 bytes, which is
/// the unit the server counts.
///
/// Every rejection names the key and, where there is a number to give, its
/// byte count: the key comes from application data, so the caller has to be
/// able to find it there. A byte the command line cannot carry is rendered the
/// same way `quote` renders peer data, so a tab or a delete stays visible
/// instead of reaching the message as itself.
fn validate_key(key : String) -> Unit raise ProtocolError {
let bytes = @utf8.encode(key.view())
let shown = quote(key.view())
guard bytes.length() > 0 else { raise Malformed("a key cannot be empty") }
guard bytes.length() <= MAX_KEY_LENGTH else {
raise Malformed(
"key '\{shown}' is \{bytes.length()} bytes, more than the \{MAX_KEY_LENGTH} bytes a key may hold",
)
}
for byte in bytes {
guard is_word_byte(byte.to_int()) else {
raise Malformed(
"key '\{shown}' holds a blank or control byte a command line cannot carry: '\{byte.to_char().escape(quote=false)}'",
)
}
}
}
///|
/// Reject a `stats` section that cannot be written into a command line.
///
/// A section is one or more words separated by single spaces, and the words
/// are what the server reads, so a space may only appear between two of them:
/// an empty word (leading, trailing or doubled space, or no section at all)
/// would leave the command line with a hole in it. Every other word byte is
/// the same one a key may hold, and every rejection quotes the section so that
/// the offending byte can be found in the message.
///
/// The length cap is borrowed from keys: memcached sets no separate limit on a
/// section, but the command line is read as one run of words, so a section no
/// longer than a key is certainly accepted and a longer one is refused as a
/// conservative guard against a runaway section.
fn validate_subcommand(section : String) -> Unit raise ProtocolError {
let bytes = @utf8.encode(section.view())
let shown = quote(section.view())
guard bytes.length() > 0 && bytes.length() <= MAX_KEY_LENGTH else {
raise Malformed(
"stats section is \{bytes.length()} bytes, outside the 1..\{MAX_KEY_LENGTH} bytes this client allows: '\{shown}'",
)
}
// A word holds at least one byte, so every space must have a word byte on
// both sides of it; a space that does not leaves an empty word behind. The
// same scan records that and rejects the bytes a command line cannot carry.
let mut after_space = true
let mut empty_word = false
for byte in bytes {
let is_space = byte.to_int() == 0x20
guard is_space || is_word_byte(byte.to_int()) else {
raise Malformed(
"stats section has a byte a command line cannot carry: '\{shown}'",
)
}
empty_word = empty_word || (is_space && after_space)
after_space = is_space
}
guard !(empty_word || after_space) else {
raise Malformed("stats section has an empty word: '\{shown}'")
}
}
///|
/// Reject a retrieval whose key list cannot be written out.
///
/// The command line carries the keys one after another, so an empty list would
/// encode a bare `get` with no key at all.
fn validate_keys(keys : Array[String]) -> Unit raise ProtocolError {
guard keys.length() > 0 else {
raise Malformed("a retrieval needs at least one key")
}
for key in keys {
validate_key(key)
}
}
///|
/// Reject a batch a pipeline cannot run.
///
/// Every request is held to the same rule as a single one, for the same reason
/// [`Client::execute`] checks first: encoding only says what a request looks
/// like on the wire, so a request that could not be expressed as a command line
/// has to be refused before the batch starts, not after part of it went out.
///
/// `quit` is refused wherever it sits in the batch. The server stops answering
/// once it reads one, so every request behind it would be dropped without the
/// reply that would have reported it, and a `quit` at the very end would close
/// the connection behind [`Client`]' back. [`Client::quit`] owns that job.
fn validate_pipeline(requests : Array[Request]) -> Unit raise ProtocolError {
for request in requests {
guard !(request is Quit) else {
raise Malformed(
"a pipeline cannot hold 'quit': the server stops answering behind it, so a request after it would be dropped with no reply to report it; use 'Client::quit' to close",
)
}
validate_request(request)
}
}
///|
/// Reject a request that the text protocol cannot express.
///
/// Encoding only reports what a request looks like on the wire, so the client
/// checks the invariants that the command line relies on before writing.
fn validate_request(request : Request) -> Unit raise ProtocolError {
match request {
Storage(key~, ..) => validate_key(key)
Get(keys~, ..) => validate_keys(keys)
Gat(keys~, ..) => validate_keys(keys)
Delete(key~, ..) => validate_key(key)
Incr(key~, ..) => validate_key(key)
Decr(key~, ..) => validate_key(key)
Touch(key~, ..) => validate_key(key)
Stats(sub~) =>
// The section is a run of words of the command line, so each word obeys
// the same rules as a key; an empty word would leave a hole behind.
if sub is Some(section) {
validate_subcommand(section)
}
FlushAll(delay~, ..) =>
if delay is Some(seconds) {
guard seconds >= 0 else {
raise Malformed("flush_all delay cannot be negative: \{seconds}")
}
}
Verbosity(level~, ..) =>
if level < 0 {
raise Malformed("verbosity level cannot be negative: \{level}")
}
CacheMemlimit(megabytes~, ..) =>
if megabytes < 0 {
raise Malformed(
"cache_memlimit megabytes cannot be negative: \{megabytes}",
)
}
SlabsReassign(src~, dst~, ..) => {
// `-1` asks the server to pick the source slab itself; anything below
// is not a slab class at all.
guard src >= -1 else {
raise Malformed("slab source class cannot be below -1: \{src}")
}
// Only the source may be left to the server: the target names the class
// the page moves into, so it has to be one.
guard dst >= 0 else {
raise Malformed("slab target class cannot be negative: \{dst}")
}
}
SlabsAutomove(mode~, ..) => {
guard 0 <= mode && mode <= 2 else {
raise Malformed("slabs automove mode must be 0, 1 or 2: \{mode}")
}
}
Version => ()
Quit => ()
}
}