Skip to content

Add ReadOnlySequence to RedisValue #3113

Closed

Description

@pairbit

Please consider adding a ReadOnlySequence buffer to RedisValue. #3112

This will allow:

  1. Reading a Stream of unknown length without allocating a finite-size buffer;

  2. Using the same buffers from RequestBufferPool;

  3. Slicing a large ReadOnlySequence into multiple RedisValue's.

For example:

I read Json from RequestBody.
Using Utf8JsonReader, i read the array of strings through the ValueSequence property. Save all strings to redis with one command.

Activity

  1. mgravell commented on Jun 24, 2026

    @mgravell
    Collaborator

    Any time we get to RedisValue, we're in lifetime hell. Lease<T> helps with this by at least providing lifetime hooks, but it still isn't ideal.
    I think the real gain here comes from the planned 3.2 work, which is where I plan to expose the RESPite reader API to the consumer against internally managed buffers, guarding against capture (breaking lifetime rules) by the ref struct nature of RespReader. So basically: my suggestion here would be:

    • defer until the 3.2 changes
    • add an API that exposes payloads as ReadOnlySequence<byte>
      • but note that we'd need to consider the capture problem; we do not want to allow use of chunks after the call; one way to do this might be to use a custom MemoryManager<byte>, which means we'd pay for the alloc of a small shim to avoid the risk
  2. pairbit commented on Jun 24, 2026

    @pairbit
    ContributorAuthor

    You're talking about reading the response, but what about writing the request? I'm particularly interested in methods for working with Lua scripts.

  3. mgravell commented on Jun 24, 2026

    @mgravell
    Collaborator

    I should have clarified: the 3.2 changes will include read and write - similar APIs should exist for RespWriter too; the internal aspects of RespWriter is the 3.1 work'; the 3.2 work is "put that on the public API"

  4. mgravell commented on Jun 24, 2026

    @mgravell
    Collaborator

    As a stop-gap: RedisValue does allow use from ReadOnlyMemory<byte>; so: you can lease and linearize - just be sure not to use fire-and-forget; you'll want to wait (sync)/await (async) the full completion to recycle your leased buffer.

  5. pairbit commented on Jun 24, 2026

    @pairbit
    ContributorAuthor

    As a stop-gap: RedisValue does allow use from ReadOnlyMemory<byte>; so: you can lease and linearize - just be sure not to use fire-and-forget; you'll want to wait (sync)/await (async) the full completion to recycle your leased buffer.

    Yes, I'm doing that right now. Thanks, I'll wait for version 3.2.

  6. pairbit commented on Jun 24, 2026

    @pairbit
    ContributorAuthor

    I see the ideal implementation without RedisValue.

    Requests are written via RespWriter to leased buffers in user code. The request is then queued, and the buffers are transferred to the SocketStream. The user can decide whether the buffer is immediately returned to the pool after writing or reused for a next query.

    The response is also written to leased buffers and returned to the user as a RespReader. The user reads the response in the required format. Once finished reading, the result is returned to the pool.

    The current library is very heavy in my opinion.
    I would implement a new lightweight RESP library with minimal functionality for reading and writing Lua scripts.

    My dream is to have a low-level library with minimal memory allocation and full control over the entire request/response lifecycle.

  7. mgravell commented on Jun 24, 2026

    @mgravell
    Collaborator

    The current library is very heavy in my opinion.

    Which library? if SE.Redis: absolutely

    I would implement a new lightweight RESP library with minimal functionality for reading and writing Lua scripts.

    It might be that what you're looking for here is RESPite, which is what v3 builds on, and which is what exposes RespReader (and soon: RespWriter)

    Requests are written via RespWriter to leased buffers in user code.

    This will be the main change in 3.1+; at the moment, the serialization happens inside the critical section (i.e. during the "single writer" lock on the underlying transport), meaning: only a single thread can be serializing. In 3.1 this will be limited to internal implementation, with 3.2 opening up this API to external callers. The planned and prototyped new write core moves serialization much earlier, with multiple advantages:

    1. much better scalability, by parallelizing serialization
    2. the caller doesn't need to think about lifetime - as soon as execution has returned from an async / F+F call, we never need to access the caller's data again
    3. in the case of batches, we can write the entire batch contiguously without needing changes to the calling code

    there's a ton of other things too, but those are the headlines

    I've also done similar prototyping around offloading parse; lots of plates spinning here, but we didn't want it to be a single "big bang" - for 3.0 I've shipped the MVP which mostly swaps just the read core logic, without moving other pieces.

  8. pairbit commented on Jun 24, 2026

    @pairbit
    ContributorAuthor

    I'm looking forward to version 3.2+

  9. mgravell commented on Jun 24, 2026

    @mgravell
    Collaborator

    With the PR linked, for the write path, I'm actually leaning more towards "yep, actually we can do this without any trade-offs"; so ignore most of my comments above, or just use them for extra context

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions