Skip to content

beyondoss/resp

Repository files navigation

beyond-resp

Frame and parse RESP2/RESP3 over any async byte stream

RespCodec implements tokio-util's Encoder and Decoder traits. Drop it onto a Framed transport and you have a working Redis wire codec.

Install

[dependencies]
beyond-resp = "0.1"

Enable monoio support:

[dependencies]
beyond-resp = { version = "0.1", features = ["monoio"] }

Quick Start

use beyond_resp::{RespCodec, Value};
use bytes::BytesMut;
use tokio_util::codec::{Decoder, Encoder};

let mut codec = RespCodec::resp2();
let mut buf = BytesMut::new();

// Encode
codec.encode(Value::BulkString(b"hello"[..].into()), &mut buf)?;

// Decode
let mut incoming = BytesMut::from(&b"+OK\r\n"[..]);
let value = codec.decode(&mut incoming)?; // Some(Value::SimpleString("OK"))

With a tokio transport:

use beyond_resp::{RespCodec, Value};
use tokio::net::TcpStream;
use tokio_util::codec::Framed;
use futures::{SinkExt, StreamExt};

let stream = TcpStream::connect("127.0.0.1:6379").await?;
let mut framed = Framed::new(stream, RespCodec::resp2());

framed.send(Value::Array(vec![
    Value::BulkString(b"GET"[..].into()),
    Value::BulkString(b"mykey"[..].into()),
])).await?;

if let Some(response) = framed.next().await {
    println!("{:?}", response?);
}

With a monoio transport:

use beyond_resp::{RespCodec, Value};
use monoio::net::TcpStream;
use monoio_codec::Decoder; // brings .framed() into scope

let stream = TcpStream::connect("127.0.0.1:6379").await?;
let mut framed = RespCodec::resp2().framed(stream);

monoio_codec::Decoder returns Decoded<Value>Decoded::Some(v) when a frame is ready, Decoded::Insufficient when more bytes are needed.

Protocol Version

RespCodec::resp2() and RespCodec::resp3() select the version at construction. Switch mid-stream after a HELLO 3 handshake:

codec.set_version(Version::Resp3);

RESP3 enables Map, Set, Push, Boolean, Double, BigNumber, VerbatimString, BulkError, and Attribute types. RESP2 encodes Null as $-1\r\n; RESP3 uses _\r\n.

Frame Size Limit

Default maximum frame size matches Redis at 512 MiB. Override:

let codec = RespCodec::resp2().with_max_frame_bytes(64 * 1024 * 1024);

Frames exceeding the limit return RespError::FrameTooLarge.

Value Types

Variant RESP2 RESP3
SimpleString
SimpleError
Integer
BulkString
Array
Null
Boolean
Double
BigNumber
BulkError
VerbatimString
Map
Attribute
Set
Push

Performance

Benchmarked with divan on an Apple M2 (MacBook Air, 8-core, 24 GB RAM).

The decoder uses a two-phase design: phase 1 walks the wire bytes to find the frame boundary without allocating; phase 2 builds the Value tree using zero-copy Bytes::slice() views into the receive buffer. String data (SimpleString, SimpleError, BulkString, BulkError, BigNumber, VerbatimString) is never copied — it points directly into the BytesMut.

decode_simple_string          36 ns
decode_integer                57 ns
decode_bulk_string   (11 B)   54 ns
decode_bulk_string   (1 KB)   92 ns
decode_bulk_string   (64 KB)  1.1 µs
decode_array         (10 i64) 273 ns
decode_array         (10 str) 500 ns
decode_nested_array           315 ns
decode_map           (5 KV)   273 ns
decode_pipeline_100           2.9 µs   (~29 ns/frame)

encode_null                   11 ns
encode_simple_string          15 ns
encode_integer                19 ns
encode_double                 29 ns
encode_bulk_string   (11 B)   32 ns
encode_bulk_string   (64 KB)  958 ns
encode_array         (10 i64) 140 ns

roundtrip_get_response        68 ns
roundtrip_hgetall_response    1.2 µs   (10-entry map)

Run benchmarks locally:

cargo bench

License

MIT

About

A Redis serialization protocol written in Rust for KV on https://beyond.dev

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages