use std::sync::Arc;
use std::time::Duration;
use rainier_support::{Error, ErrorKind, Result};
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stored {
Stored,
NotStored,
}
#[derive(Debug)]
pub struct MemcachedConnection {
stream: BufReader<TcpStream>,
}
impl MemcachedConnection {
pub async fn connect(address: &str) -> Result<Self> {
let stream =
TcpStream::connect(address).await.map_err(|e| unreachable_error("connect", e))?;
let _ = stream.set_nodelay(true);
Ok(Self { stream: BufReader::new(stream) })
}
pub async fn get(&mut self, key: &str) -> Result<Option<Vec<u8>>> {
self.write_all(format!("get {key}\r\n").as_bytes()).await?;
let header = self.read_line().await?;
if header == "END" {
return Ok(None);
}
let mut fields = header.split(' ');
if fields.next() != Some("VALUE") {
return Err(protocol_error(&header));
}
let length: usize = fields
.nth(2)
.and_then(|bytes| bytes.trim().parse().ok())
.ok_or_else(|| protocol_error(&header))?;
let mut body = vec![0u8; length + 2];
self.stream.read_exact(&mut body).await.map_err(|e| unreachable_error("read", e))?;
body.truncate(length);
let terminator = self.read_line().await?;
if terminator != "END" {
return Err(protocol_error(&terminator));
}
Ok(Some(body))
}
pub async fn set(&mut self, key: &str, value: &[u8], expiry: u32) -> Result<()> {
match self.store("set", key, value, expiry).await? {
Stored::Stored => Ok(()),
Stored::NotStored => Err(protocol_error("NOT_STORED in reply to set")),
}
}
pub async fn add(&mut self, key: &str, value: &[u8], expiry: u32) -> Result<Stored> {
self.store("add", key, value, expiry).await
}
async fn store(&mut self, verb: &str, key: &str, value: &[u8], expiry: u32) -> Result<Stored> {
let mut command = format!("{verb} {key} 0 {expiry} {}\r\n", value.len()).into_bytes();
command.extend_from_slice(value);
command.extend_from_slice(b"\r\n");
self.write_all(&command).await?;
match self.read_line().await?.as_str() {
"STORED" => Ok(Stored::Stored),
"NOT_STORED" | "EXISTS" => Ok(Stored::NotStored),
other => Err(protocol_error(other)),
}
}
pub async fn gets(&mut self, key: &str) -> Result<Option<(Vec<u8>, u64)>> {
self.write_all(format!("gets {key}\r\n").as_bytes()).await?;
let header = self.read_line().await?;
if header == "END" {
return Ok(None);
}
let fields: Vec<&str> = header.split(' ').collect();
if fields.first() != Some(&"VALUE") || fields.len() < 5 {
return Err(protocol_error(&header));
}
let length: usize = fields[3].trim().parse().map_err(|_| protocol_error(&header))?;
let cas: u64 = fields[4].trim().parse().map_err(|_| protocol_error(&header))?;
let mut body = vec![0u8; length + 2];
self.stream.read_exact(&mut body).await.map_err(|e| unreachable_error("read", e))?;
body.truncate(length);
let terminator = self.read_line().await?;
if terminator != "END" {
return Err(protocol_error(&terminator));
}
Ok(Some((body, cas)))
}
pub async fn compare_and_swap(
&mut self,
key: &str,
value: &[u8],
expiry: i64,
cas: u64,
) -> Result<Stored> {
let mut command = format!("cas {key} 0 {expiry} {} {cas}\r\n", value.len()).into_bytes();
command.extend_from_slice(value);
command.extend_from_slice(b"\r\n");
self.write_all(&command).await?;
match self.read_line().await?.as_str() {
"STORED" => Ok(Stored::Stored),
"EXISTS" | "NOT_FOUND" => Ok(Stored::NotStored),
other => Err(protocol_error(other)),
}
}
pub async fn delete_if(&mut self, key: &str, expected: &[u8]) -> Result<bool> {
let Some((value, cas)) = self.gets(key).await? else {
return Ok(false);
};
if value != expected {
return Ok(false);
}
Ok(self.compare_and_swap(key, b"", -1, cas).await? == Stored::Stored)
}
pub async fn delete(&mut self, key: &str) -> Result<bool> {
self.write_all(format!("delete {key}\r\n").as_bytes()).await?;
match self.read_line().await?.as_str() {
"DELETED" => Ok(true),
"NOT_FOUND" => Ok(false),
other => Err(protocol_error(other)),
}
}
pub async fn increment(&mut self, key: &str, by: u64, up: bool) -> Result<Option<u64>> {
let verb = if up { "incr" } else { "decr" };
self.write_all(format!("{verb} {key} {by}\r\n").as_bytes()).await?;
let reply = self.read_line().await?;
match reply.as_str() {
"NOT_FOUND" => Ok(None),
value => value.trim().parse().map(Some).map_err(|_| protocol_error(value)),
}
}
pub async fn flush_all(&mut self) -> Result<()> {
self.write_all(b"flush_all\r\n").await?;
match self.read_line().await?.as_str() {
"OK" => Ok(()),
other => Err(protocol_error(other)),
}
}
pub async fn version(&mut self) -> Result<String> {
self.write_all(b"version\r\n").await?;
let reply = self.read_line().await?;
reply
.strip_prefix("VERSION ")
.map(|version| version.trim().to_string())
.ok_or_else(|| protocol_error(&reply))
}
async fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
self.stream.get_mut().write_all(bytes).await.map_err(|e| unreachable_error("write", e))?;
self.stream.get_mut().flush().await.map_err(|e| unreachable_error("flush", e))
}
async fn read_line(&mut self) -> Result<String> {
let mut line = String::new();
let read =
self.stream.read_line(&mut line).await.map_err(|e| unreachable_error("read", e))?;
if read == 0 {
return Err(unreachable_message("the server closed the connection"));
}
let line = line.trim_end_matches(['\r', '\n']).to_string();
match line.as_str() {
"ERROR" => Err(protocol_error("the server did not recognise the command")),
other if other.starts_with("CLIENT_ERROR") || other.starts_with("SERVER_ERROR") => {
Err(protocol_error(other))
}
_ => Ok(line),
}
}
}
#[derive(Clone)]
pub struct MemcachedConnector {
address: String,
pool: Arc<Pool>,
}
struct Pool {
idle: Mutex<Vec<MemcachedConnection>>,
limit: usize,
}
impl MemcachedConnector {
pub fn open(url: impl Into<String>) -> Self {
Self::with_pool_size(url, 8)
}
pub fn with_pool_size(url: impl Into<String>, limit: usize) -> Self {
let url = url.into();
let address = url
.strip_prefix("tcp://")
.or_else(|| url.strip_prefix("memcached://"))
.unwrap_or(&url)
.trim_end_matches('/')
.to_string();
Self { address, pool: Arc::new(Pool { idle: Mutex::new(Vec::new()), limit: limit.max(1) }) }
}
pub fn description(&self) -> &str {
"memcached"
}
pub fn address(&self) -> &str {
&self.address
}
pub async fn acquire(&self) -> Result<MemcachedGuard<'_>> {
if let Some(connection) = self.pool.idle.lock().await.pop() {
return Ok(MemcachedGuard { connection: Some(connection), connector: self });
}
let connection = MemcachedConnection::connect(&self.address).await?;
Ok(MemcachedGuard { connection: Some(connection), connector: self })
}
pub async fn ping(&self) -> Result<bool> {
let mut guard = self.acquire().await?;
guard.connection().version().await?;
Ok(true)
}
async fn release(&self, connection: MemcachedConnection) {
let mut idle = self.pool.idle.lock().await;
if idle.len() < self.pool.limit {
idle.push(connection);
}
}
}
impl std::fmt::Debug for MemcachedConnector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemcachedConnector").field("pool_limit", &self.pool.limit).finish()
}
}
#[derive(Debug)]
pub struct MemcachedGuard<'a> {
connection: Option<MemcachedConnection>,
connector: &'a MemcachedConnector,
}
impl MemcachedGuard<'_> {
pub fn connection(&mut self) -> &mut MemcachedConnection {
self.connection.as_mut().expect("the guard holds its connection until dropped")
}
pub fn discard(mut self) {
self.connection = None;
}
}
impl Drop for MemcachedGuard<'_> {
fn drop(&mut self) {
if let Some(connection) = self.connection.take() {
let connector = self.connector.clone();
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move { connector.release(connection).await });
}
}
}
}
pub const MAX_RELATIVE_TTL: u64 = 60 * 60 * 24 * 30;
pub fn expiry_seconds(ttl: Option<Duration>) -> u32 {
match ttl {
None => 0,
Some(ttl) => match ttl.as_secs() {
0 => 1,
seconds => seconds.min(MAX_RELATIVE_TTL) as u32,
},
}
}
pub fn check_key(key: &str) -> Result<()> {
if key.is_empty() {
return Err(Error::internal("a Memcached key must not be empty"));
}
if key.len() > 250 {
return Err(Error::internal(format!(
"Memcached keys are limited to 250 bytes; this one is {}",
key.len()
)));
}
if key.bytes().any(|byte| byte <= b' ' || byte == 0x7f) {
return Err(Error::internal(
"a Memcached key must not contain spaces or control characters",
));
}
Ok(())
}
fn unreachable_error(operation: &str, error: std::io::Error) -> Error {
unreachable_message(&format!("{operation} failed: {error}"))
}
fn unreachable_message(detail: &str) -> Error {
Error::new(ErrorKind::ServiceUnavailable, format!("Memcached: {detail}"))
}
fn protocol_error(reply: &str) -> Error {
Error::new(
ErrorKind::ServiceUnavailable,
format!("Memcached returned an unexpected reply: {reply}"),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_url_scheme_is_optional_and_stripped() {
for url in ["127.0.0.1:11211", "tcp://127.0.0.1:11211", "memcached://127.0.0.1:11211/"] {
assert_eq!(MemcachedConnector::open(url).address(), "127.0.0.1:11211", "{url}");
}
}
#[test]
fn a_connector_opens_without_touching_the_network() {
assert_eq!(MemcachedConnector::open("127.0.0.1:1").description(), "memcached");
}
#[test]
fn the_pool_size_is_at_least_one() {
let connector = MemcachedConnector::with_pool_size("127.0.0.1:1", 0);
assert_eq!(connector.pool.limit, 1, "a pool of zero would never lend a connection");
}
#[test]
fn debug_does_not_disclose_the_url() {
let connector = MemcachedConnector::open("tcp://user:hunter2@127.0.0.1:11211");
assert!(!format!("{connector:?}").contains("hunter2"));
}
#[test]
fn no_ttl_means_never_expire() {
assert_eq!(expiry_seconds(None), 0);
}
#[test]
fn a_sub_second_ttl_becomes_one_second_not_never() {
assert_eq!(expiry_seconds(Some(Duration::from_millis(500))), 1);
}
#[test]
fn a_long_ttl_is_clamped_below_the_timestamp_threshold() {
let forty_days = Duration::from_secs(60 * 60 * 24 * 40);
assert_eq!(expiry_seconds(Some(forty_days)), MAX_RELATIVE_TTL as u32);
}
#[test]
fn an_ordinary_ttl_passes_through_in_seconds() {
assert_eq!(expiry_seconds(Some(Duration::from_secs(300))), 300);
}
#[test]
fn a_key_that_would_corrupt_the_connection_is_refused() {
for bad in ["", "has space", "has\nnewline", "has\ttab", "has\rreturn"] {
assert!(check_key(bad).is_err(), "{bad:?}");
}
assert!(check_key("app:user:1").is_ok());
}
#[test]
fn an_over_long_key_is_refused_with_its_length() {
let err = check_key(&"k".repeat(251)).unwrap_err();
assert!(err.message().contains("250"), "{}", err.message());
assert!(err.message().contains("251"), "{}", err.message());
}
#[tokio::test]
async fn connecting_to_nothing_is_a_503() {
let err = MemcachedConnection::connect("127.0.0.1:1").await.unwrap_err();
assert_eq!(err.status(), 503, "{}", err.message());
}
#[tokio::test]
async fn acquiring_from_nothing_is_a_503() {
let err = MemcachedConnector::open("127.0.0.1:1").acquire().await.unwrap_err();
assert_eq!(err.status(), 503);
}
async fn stub(script: &'static [u8]) -> (String, tokio::task::JoinHandle<Vec<u8>>) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap().to_string();
let handle = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buffer = vec![0u8; 4096];
let read = socket.read(&mut buffer).await.unwrap();
buffer.truncate(read);
socket.write_all(script).await.unwrap();
socket.flush().await.unwrap();
buffer
});
(address, handle)
}
#[tokio::test]
async fn get_reads_a_value_by_length() {
let (address, server) = stub(b"VALUE k 0 6\r\nab\r\ncd\r\nEND\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
assert_eq!(connection.get("k").await.unwrap(), Some(b"ab\r\ncd".to_vec()));
assert_eq!(server.await.unwrap(), b"get k\r\n");
}
#[tokio::test]
async fn get_reads_a_miss_as_none() {
let (address, _server) = stub(b"END\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
assert_eq!(connection.get("k").await.unwrap(), None);
}
#[tokio::test]
async fn set_sends_the_length_and_the_body() {
let (address, server) = stub(b"STORED\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
connection.set("k", b"hello", 300).await.unwrap();
assert_eq!(server.await.unwrap(), b"set k 0 300 5\r\nhello\r\n");
}
#[tokio::test]
async fn add_reports_a_collision_rather_than_failing() {
let (address, _server) = stub(b"NOT_STORED\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
assert_eq!(connection.add("k", b"v", 0).await.unwrap(), Stored::NotStored);
}
#[tokio::test]
async fn delete_reads_not_found_as_false() {
let (address, _server) = stub(b"NOT_FOUND\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
assert!(!connection.delete("k").await.unwrap());
}
#[tokio::test]
async fn increment_reads_the_new_value() {
let (address, server) = stub(b"7\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
assert_eq!(connection.increment("k", 3, true).await.unwrap(), Some(7));
assert_eq!(server.await.unwrap(), b"incr k 3\r\n");
}
#[tokio::test]
async fn decrement_uses_the_decr_verb() {
let (address, server) = stub(b"1\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
connection.increment("k", 2, false).await.unwrap();
assert_eq!(server.await.unwrap(), b"decr k 2\r\n");
}
#[tokio::test]
async fn increment_reads_a_missing_key_as_none() {
let (address, _server) = stub(b"NOT_FOUND\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
assert_eq!(connection.increment("k", 1, true).await.unwrap(), None);
}
#[tokio::test]
async fn version_strips_its_prefix() {
let (address, _server) = stub(b"VERSION 1.6.21\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
assert_eq!(connection.version().await.unwrap(), "1.6.21");
}
#[tokio::test]
async fn a_server_error_is_surfaced_rather_than_read_as_data() {
let (address, _server) = stub(b"SERVER_ERROR out of memory\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
let err = connection.get("k").await.unwrap_err();
assert!(err.message().contains("out of memory"), "{}", err.message());
}
#[tokio::test]
async fn an_unrecognised_command_is_an_error() {
let (address, _server) = stub(b"ERROR\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
assert!(connection.flush_all().await.is_err());
}
#[tokio::test]
async fn a_closed_connection_says_so() {
let (address, _server) = stub(b"").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
let err = connection.get("k").await.unwrap_err();
assert!(err.message().contains("closed"), "{}", err.message());
}
#[tokio::test]
async fn flush_all_expects_ok() {
let (address, server) = stub(b"OK\r\n").await;
let mut connection = MemcachedConnection::connect(&address).await.unwrap();
connection.flush_all().await.unwrap();
assert_eq!(server.await.unwrap(), b"flush_all\r\n");
}
}