Redis internals
-ioxide.redis is a lean RESP2 client on the same ring as everything
- else. Its design mirrors the Postgres driver's pipelining and pooling;
- the value/parse layer borrows the allocation discipline of StackExchange.Redis. Every class
- below.
RedisConnection · RedisConnection.cs (+ .Commands)
-One Redis connection over a RingSocket. ConnectAsync connects, then
- AUTH (ACL-aware) and SELECT inline before returning a ready connection. The generic surface is
- ExecuteAsync(command, args); typed helpers (GET/SET/INCR/HSET/LRANGE/ZADD/…) layer on
- top in RedisConnection.Commands.cs.
Pipelining
-Identical shape to the pg driver: SubmitCore appends the encoded command into a
- fixed 512 KB native send buffer (overflow throws, never realloc - a pipelined send
- holds its address), enqueues a Pending (IValueTaskSource<RespValue>),
- and kicks the sender/reader loops. SenderLoopAsync drains the staged bytes;
- ReaderLoopAsync pulls one reply per in-flight entry and completes the dequeued
- Pending FIFO (Redis guarantees reply order on one connection). A documented invariant:
- there is no await between the sender loop's final drain check and clearing
- _sending, and completions resume inline, so no command can be stranded - it depends on
- RingOpSource keeping synchronous continuations. PipelineAsync sends several
- commands back to back and awaits all replies - one round trip.
RedisConnection.cs · staging a command into the fixed send buffer (overflow throws, never realloc)
-private unsafe void AppendCommand(ReadOnlySpan<byte> nameToken, RedisArg[] args)
-{
- int size = RespProtocol.CommandSize(nameToken, args);
- if (size > _sendCapacity)
- {
- throw new RedisException($"command exceeds send buffer ({_sendCapacity} bytes)");
- }
- if (_sendEnd + size > _sendCapacity)
- {
- throw new RedisException("pipelined send buffer full"); // an in-flight send holds _send's address
- }
- int written = RespProtocol.WriteCommand(
- new Span<byte>((void*)(_send + _sendEnd), _sendCapacity - _sendEnd), nameToken, args);
- _sendEnd += written;
-}
-
- Command encoding
-Command names are a tiny fixed set, so their full pre-framed RESP token
- ($3\r\nGET\r\n) is computed once and cached in a static
- ConcurrentDictionary<string, byte[]> - the hot path memcpy's the whole token in
- one shot, no per-call encoding or framing allocation.
RedisConnection.cs · the pre-framed command token is computed once and memoized
-private static readonly ConcurrentDictionary<string, byte[]> CommandCache = new();
-
-private static byte[] EncodeCommand(string command) =>
- CommandCache.GetOrAdd(command, static c => RespProtocol.FrameName(Encoding.ASCII.GetBytes(c)));
-
- Receiving with an examined cursor
-ReceiveReplyAsync reads into a growable native recv buffer (up to 64 MB),
- compacting the unparsed tail before growth. TryParse reports, on an incomplete reply,
- how many bytes are needed before a re-parse can make progress (the examined cursor), so a large
- reply arriving over many recvs isn't re-scanned from the start each time.
RespProtocol.cs · the examined cursor - on an incomplete reply, report how many more bytes are needed
-public static bool TryParse(ReadOnlySpan<byte> buffer, out RespValue value, out int consumed, out int needed)
-{
- int pos = 0;
- needed = 0;
- if (TryParseAt(buffer, ref pos, out value, ref needed))
- {
- consumed = pos;
- return true;
- }
- consumed = 0;
- return false; // 'needed' lets the caller wait for that many bytes before re-parsing
-}
-
- Timeouts and breakage
-Each Pending stamps EnqueuedAtMs; the pool's ticker calls
- CheckTimeout to tear down a connection whose oldest in-flight command has aged past
- CommandTimeoutMs (diagnostic RedisException; closing the fd cancels the
- stuck recv). A transport failure marks IsBroken and the pool evicts.
RespProtocol · RespProtocol.cs
-The RESP2 codec, a pure static. CommandSize/WriteCommand write a
- command as a RESP array of bulk strings (the pre-framed name token + each arg). FrameName
- builds the cached name token. TryParse recursively parses one reply -
- + simple string, - error, : integer, $ bulk
- (with $-1 null), * array (with *-1 null), and _
- (RESP3 null, tolerated) - returning false with a needed-bytes count when the reply
- isn't fully buffered. ParseLong is strict: optional sign then ASCII digits, with
- overflow-checked accumulation, so a malformed length breaks the connection cleanly instead of
- computing a garbage size and wedging. Bulk/array lengths are range-guarded.
RespProtocol.cs · WriteCommand - a RESP array of bulk strings, name token memcpy'd in
-public static int WriteCommand(Span<byte> dst, ReadOnlySpan<byte> nameToken, ReadOnlySpan<RedisArg> args)
-{
- int p = 0;
- dst[p++] = (byte)'*';
- p += WriteInt(dst[p..], args.Length + 1);
- dst[p++] = (byte)'\r';
- dst[p++] = (byte)'\n';
-
- nameToken.CopyTo(dst[p..]); // pre-framed $len\r\nNAME\r\n - one memcpy, no per-call framing
- p += nameToken.Length;
-
- foreach (RedisArg arg in args)
- {
- p += WriteBulk(dst[p..], arg.Bytes);
- }
- return p;
-}
-RespProtocol.cs · ParseLong - strict ASCII digits, overflow-checked, malformed input breaks the connection
-private static long ParseLong(ReadOnlySpan<byte> s)
-{
- bool neg = s.Length > 0 && s[0] == (byte)'-';
- if (neg) s = s[1..];
- if (s.IsEmpty)
- {
- throw new RedisException("malformed RESP integer");
- }
- long n = 0;
- foreach (byte c in s)
- {
- if (c < (byte)'0' || c > (byte)'9')
- {
- throw new RedisException($"malformed RESP integer (byte 0x{c:x2})");
- }
- n = checked(n * 10 + (c - '0'));
- }
- return neg ? -n : n;
-}
-
- RespValue · RespValue.cs
-A parsed reply, as a value type (struct), so parsing allocates no per-node object -
- integers, nulls, and booleans are entirely allocation-free; bulk/simple strings and arrays carry
- the bytes (or element array) they materialized during parse, which outlive the recv buffer.
- Accessors convert on demand: AsString, Bytes, AsInteger
- (accepts integer replies and numeric strings, length-checked), AsDouble (accepts
- integers, numeric strings, and the non-finite tokens inf/-inf/nan
- that Utf8Parser won't parse - e.g. a ZSCORE of +inf), AsBool,
- Items, IsNull, IsError.
RespValue.cs · a readonly struct - integers/nulls/bools cost zero allocation
-public readonly struct RespValue
-{
- public RespKind Kind { get; }
- public long Integer { get; }
- private readonly byte[]? _bytes;
- private readonly RespValue[]? _items;
-
- private RespValue(RespKind kind, long integer, byte[]? bytes, RespValue[]? items)
- {
- Kind = kind;
- Integer = integer;
- _bytes = bytes; // bulk/simple strings materialize here at parse, outliving the recv buffer
- _items = items; // arrays only
- }
- // ...accessors below convert on demand
-}
-RespValue.cs · AsDouble - falls back to the non-finite tokens Utf8Parser won't take
-public double AsDouble()
-{
- if (Kind == RespKind.Integer)
- {
- return Integer;
- }
- if (_bytes is not null && (Kind == RespKind.BulkString || Kind == RespKind.SimpleString))
- {
- if (Utf8Parser.TryParse(_bytes, out double d, out int n) && n == _bytes.Length)
- {
- return d;
- }
- ReadOnlySpan<byte> b = _bytes;
- if (b.SequenceEqual("inf"u8) || b.SequenceEqual("+inf"u8)) return double.PositiveInfinity;
- if (b.SequenceEqual("-inf"u8)) return double.NegativeInfinity;
- if (b.SequenceEqual("nan"u8)) return double.NaN; // e.g. a ZSCORE of +inf
- }
- throw new RedisException($"reply ({Kind}) is not a double");
-}
-
- RedisPool · RedisPool.cs
-N connections per reactor with round-robin and pipelining, mirroring PgPool:
- bounded fail-fast opens (WaitForConnectionAsync throws after
- MaxConnectAttempts instead of hanging), a reactor ticker that sweeps command timeouts
- and replenishes with jittered backoff, and the cache-aside surface (GetAsync,
- SetExAsync, DelAsync) plus ExecuteAsync and
- PipelineAsync (the latter picks one connection so the batch is a real single round
- trip). One pool per reactor, reactor-thread-only.
The value types
--
-
- RedisArg (
RedisArg.cs) - a value-type command argument with implicit - conversions from string/byte[]/int/long/double/bool, rendered to bytes for the wire (double via - round-trip "R", bool as "1"/"0").
- - RedisCommand (in
RedisConnection.cs) - a (name, args) pair for -PipelineAsync; the name is pre-framed to its RESP token at construction.
- - RedisOptions (
RedisOptions.cs) - host/port, password + optional ACL user, - database index,PoolSize,CommandTimeoutMs.
- - RedisException (
RedisException.cs) - a-ERRreply or a - transport failure.
-
Note: RESP2 only (no HELLO), and the FIFO
- one-reply-per-request model has no out-of-band/push path - pub/sub on a RESP2 connection would
- need a dedicated connection, as StackExchange.Redis does.