Ruvio.Client
0.2.13
dotnet add package Ruvio.Client --version 0.2.13
NuGet\Install-Package Ruvio.Client -Version 0.2.13
<PackageReference Include="Ruvio.Client" Version="0.2.13" />
<PackageVersion Include="Ruvio.Client" Version="0.2.13" />
<PackageReference Include="Ruvio.Client" />
paket add Ruvio.Client --version 0.2.13
#r "nuget: Ruvio.Client, 0.2.13"
#:package Ruvio.Client@0.2.13
#addin nuget:?package=Ruvio.Client&version=0.2.13
#tool nuget:?package=Ruvio.Client&version=0.2.13
Ruvio.Client
Binary commands
IRuvioClient.ExecuteBinaryAsync(IReadOnlyList<ReadOnlyMemory<byte>>, CancellationToken)
sends raw RESP bulk arguments without UTF-8/base64 conversion; binary replies are in
RespValue.Bytes. Keep argument memory unchanged until the operation completes.
The default interface implementation throws NotSupportedException, so existing
implementations remain compatible but must opt into binary support.
await client.ExecuteBinaryAsync(new ReadOnlyMemory<byte>[]
{
"SET"u8.ToArray(), "binary-key"u8.ToArray(), new byte[] { 0, 255, 128 }
}, cancellationToken);
This API never replays a command after a transport failure; cancellation after dispatch does not establish whether the server executed it. Explicit MOVED rejections may redirect. Standalone mode accepts arbitrary commands. Cluster mode supports GET, SET, DEL, UNLINK, EXISTS, HGET, HSET, HMGET, HMSET, HDEL, PEXPIRE, PEXPIREAT, PTTL, TTL, PERSIST, IDEM, EVAL, EVALSHA, PING, and ECHO. Binary keys use raw-byte hash slots; all keys must share one slot. Cross-slot splitting and cluster transactions are deliberately not supported by this API; unsupported commands fail explicitly. Existing string APIs are unchanged.
RESP2 client for Ruvio. Connecting does not send CLIENT SETINFO or HELLO. The typed methods cover every command in docs/compatibility-matrix.json and the options that change what Ruvio does. FLUSHDB ASYNC (it runs synchronously) and HELLO AUTH/SETNAME have no typed form. Anything else can go through ExecuteAsync with the same argument lists as redis-cli.
Serial socket profiling
On a disposable standalone server, phase profiling can be run with:
dotnet run -c Release --project clients/csharp/Ruvio.Client.Benchmarks -- --socket-phases 16479 5
This uses serial typed GET/SET, 256-byte values, a one-second warmup and five
measured seconds per operation, and reports client CPU/allocation and the
ruvio_client_socket_phase_duration_milliseconds histogram's queue, encode,
write and reply means. SET repeats the same value. Metrics are enabled for the
measurement and perturb timings/allocations; compare throughput separately with
--socket-load. Encode timing is collected only when the histogram is enabled.
Queue includes time through encoding, and reply can overlap write: the phases
must not be summed or treated as pure network/server timings.
An optional third argument selects concurrency 1/16/128; for example,
--socket-phases 16479 5 16. wallMicrosecondsPerOperation is elapsed wall
time divided by completed operations, not concurrent request latency.
On local Apple M4 Pro/.NET 8.0.18 loopback, initial means were about 30 us total, 0.04–0.05 us encode, 0.26–0.34 us queue, 2.7–2.8 us write and 28.5–28.7 us reply. The dominant interval includes reply waiting, parsing and async scheduling; these data do not isolate wire latency.
The reader now rechecks its buffered scalar fast path after filling an empty
buffer, avoiding nested asynchronous parsing for a newly received complete
reply. Fragmented/array replies retain their parser and failure classification.
Local ReplyBenchmarks changed bulk parsing from 65.27 to 39.40 ns, integer
from 39.90 to 35.11 ns, and array from 92.08 to 94.83 ns; measured allocation
was unchanged. Instrumented serial socket rates remained about 33k ops/s, so no
end-to-end throughput gain from the reader change is established. No batching
delay or connection-wide retry was added. The subsequent coalescing experiment
and its separate concurrent write-count/CPU measurements are described below.
An instrumented concurrency-16 run measured about 4.5 us write, 36–37 us queue
and 49 us reply per phase sample. Write time can also serialize queue progress,
so these overlapping means neither prove nor rule out coalescing benefits.
Write-count and pipeline comparison
Socket metrics additionally report ruvio_client_socket_write_calls and
ruvio_client_socket_written_bytes for successfully completed stream writes.
These count application WriteAsync completions and payload bytes, not TCP
packets, kernel syscalls or server execution acknowledgements. Failed writes
are not counted, even when the transport may have accepted a partial payload.
dotnet run -c Release --project clients/csharp/Ruvio.Client.Benchmarks -- --socket-phases 16479 5 16
dotnet run -c Release --project clients/csharp/Ruvio.Client.Benchmarks -- --socket-phases 16479 5 16 --pipeline
The second mode explicitly submits prebuilt groups of 16 commands through
ExecuteManyAsync, verifies every reply and reports per-operation CPU, write
count and average bytes per write. It is an explicit pipeline experiment, not
automatic batching of independent calls.
Local instrumented .NET 8.0.18/M4 Pro comparison (256-byte values, one shared loopback connection, identical SET values, one warmup and five measured seconds):
| Operation | Concurrent calls ops/s | Pipeline ops/s | Writes/op, calls → pipeline | Client CPU us/op, calls → pipeline |
|---|---|---|---|---|
| GET | 173,178 | 485,978 | 1 → 0.0625 | 55.18 → 8.04 |
| SET | 168,511 | 459,929 | 1 → 0.0625 | 58.60 → 8.30 |
GET write payload grew from 66 to 1,056 bytes; SET from 330 to 5,280 bytes. Keys include a random run ID. This single ordered comparison is diagnostic, not release performance evidence. Pipeline changes request grouping, completion and round-trip scheduling as well as write count; it does not isolate syscall cost or predict automatic coalescing gains. Instrumentation also perturbs CPU and timing. Serial calls still send immediately; no timer or batch-fill delay was introduced. Independent calls on one socket always coalesce already queued frames into a pooled buffer, with at most 16 request groups per write. The writer stops collecting after reaching 64 KiB; the last complete frame can exceed that threshold. An initially large frame is sent separately. There is no timer, batch-fill wait, configuration switch, or automatic transport replay. Explicit pipelines are unchanged. Pending replies retain FIFO order; canceled queued requests are skipped and encoding failures remain per-request errors. Partial/failed stream writes fault the connection. Database-scoped and reconnected sockets use the same writer.
Two alternating-order passes per variant on the same instrumented local setup (three-second cases) measured:
| Concurrency | Operation | Before → coalesced ops/s | Writes/op after | CPU us/op before → after |
|---|---|---|---|---|
| 1 | GET | 34,190 → 34,366 | 1 | 124.29 → 125.24 |
| 1 | SET | 34,059 → 33,254 | 1 | 121.06 → 129.35 |
| 16 | GET | 174,952 → 235,687 | 0.242 | 55.86 → 41.96 |
| 16 | SET | 173,031 → 231,472 | 0.221 | 55.44 → 42.53 |
| 128 | GET | 236,816 → 716,073 | 0.085 | 39.31 → 13.17 |
| 128 | SET | 226,779 → 704,713 | 0.073 | 42.25 → 13.64 |
These are means of short instrumented runs, not latency percentiles or release certification. Serial throughput in these instrumented runs was essentially unchanged/slightly lower in the SET cases.
The subsequent uninstrumented alternating comparison was interrupted by INF-11: SET with four workers failed during warmup on .NET 8.0.18. The completed before and after passes included a serial SET decline from 35,691 to 20,950 ops/s (p99 0.037 to 0.080 ms), despite gains at high concurrency. This incomplete comparison is not evidence of an established regression or a general gain.
A narrower ten-second serial SET comparison used the same binary with the option off/on/on/off. Its two-run means were 35,965 versus 35,612 ops/s; p99 was 0.037–0.040 ms off and 0.039 ms on. The large serial decline did not reproduce in this isolation. A separate ten-second SET/four-worker attempt failed again with coalescing enabled, this time during measurement, at the initial pending-channel publication rather than inside the batching helper. Eight later SET/four-worker cases (two per option/runtime combination on 8.0.18 and 8.0.31) completed without failure. INF-11 remains unresolved: it was observed before coalescing existed, and these successful short cases do not prove either mode or runtime immune.
Independent socket calls always coalesce. --socket-load 16479 10 --inline set:4
measures that path; --socket-phases 16479 3 16 profiles it. --pipeline is
still the explicit pipeline, not a switch for automatic coalescing. INF-11 is a
.NET channel bug on ARM64, not a reason to disable this writer.
Compile-time command encoding
Small typed collection commands (LPUSH, RPUSH, SADD, SREM, HDEL,
HMGET, ZREM) keep the key and up to four additional arguments inline.
HSET keeps up to two field/value pairs inline. Larger calls retain a copied
argument array; caller arrays are not retained by either factory. Public params
arrays are still created at the call site when applicable: this removes the
client's second, flattened array, not all allocations in the operation.
KeyedCommandBenchmarks measures construction plus encoding with preallocated
input pairs. On Apple M4 Pro / .NET 8.0.18 (three warmups, five measured
iterations), one-pair HSET changed from 89.49 to 67.19 ns and 56 to 0 allocated
bytes; two pairs from 126.59 to 97.32 ns and 72 to 0 bytes. Three pairs use the
array fallback (162.83 vs 164.50 ns, 88 bytes in both). These measurements exclude
the caller's input array, network and response costs.
The existing source generator reads RespVerbs.txt and emits both UTF-8 verb
spans and complete RESP bulk fragments (including length and CRLF). Inline typed
commands copy a generated fragment instead of measuring, formatting and encoding
their command name at runtime. Arguments still use strict UTF-8 encoding and
integer arguments are written directly. Unlisted or differently cased names,
caller-supplied command lists, and binary commands retain their generic encoders;
no runtime reflection or public API change is involved.
To measure the inline encoder without network or server costs:
dotnet run -c Release --project clients/csharp/Ruvio.Client.Benchmarks -- --filter '*InlineCommandBenchmarks*' --job short
On Apple M4 Pro / .NET 8.0.18, a local five-iteration comparison reduced encoding time from 34.28 to 25.55 ns for GET, 45.99 to 37.26 ns for SET, 34.59 to 25.64 ns for INCR, and 21.79 to 14.92 ns for PING, with no measured managed allocation in either version. An unlisted inline CUSTOM command increased from 33.95 to 36.47 ns because of the lookup; public arbitrary-command list encoding is unchanged. These are microbenchmark results, not end-to-end throughput claims.
The list now covers 43 verbs, including TTL, hash, integer and native coordination commands. With this expanded lookup, GET remained 25.96 ns and SET 38.03 ns; INCRBY measured 32.44 vs 42.03 ns, HGET 36.15 vs 45.05 ns, and IDEM BEGIN 68.18 vs 81.81 ns against the unchanged encoder in a separate local comparison. All generated verbs are checked against generic encoding with zero through five inline arguments, including UTF-8 strings and integer boundaries.
For a bounded socket comparison, use the same benchmark executable with two client builds that differ only in the encoder:
dotnet run -c Release --project clients/csharp/Ruvio.Client.Benchmarks -- --socket-load 16479 5 --inline
Append get:128, set:16 or limit:1 to select a single case for failure
isolation. Supported concurrency values are 1/2/4/16/128; invalid selectors fail
explicitly. Runtime comparisons should execute the same built DLL with each
runtime host, rather than rebuilding between runs.
This runs GET, SET and integer-argument LIMIT with concurrency 1/2/4/16/128,
one shared standalone connection, 256-byte GET/SET values, one-second warmups
and five-second measurements per case. SET repeats an identical value.
The benchmark uses unique keys and removes them after each successful case;
failures propagate, and case progress is written to stderr. Without --inline
it retains the generic list-command path. Use a disposable server.
Local loopback results were much smaller and noisier than encoder-only results.
On .NET 10.0.2, one new/old pass ranged from -2.7% to +4.2% throughput;
this does not establish an end-to-end improvement. On .NET 8.0.18, one generated
pass completed but two subsequent passes failed in
System.Threading.Channels.AsyncOperation.SignalCompletion with an unexpected
state exception; two unchanged-encoder passes completed. The cause is unresolved,
and the successful newer-runtime pass does not establish .NET 8 reliability.
Five subsequent .NET 8 passes completed 75 cases and about 31.6 million
measured operations without reproducing the exception. Added inline/list request
ownership stress also passed; neither result resolves the earlier failures.
The harness logs runtime, run ID, operation/concurrency and warmup/measurement
phase to stderr so a future failure can be located precisely.
Four further alternating 8.0.18/8.0.31 passes completed without failure on either
runtime; a current servicing runtime alone has not been shown to fix the issue.
See INF-11
before treating this optimization as release-ready.
Reads start with Get. Writes use the mutation: Set, Add, Remove, Delete, Increment. A range that returns values is GetSortedSetRangeByRankWithScoresAsync.
using Ruvio.Client;
await using var connection = await RuvioConnection.ConnectAsync("127.0.0.1", 6379);
RuvioClient db = await connection.GetDatabaseAsync(0);
await db.SubscribeAsync("news", msg => Console.WriteLine(msg.Payload));
await db.PublishAsync("news", "hello");
await db.SubscribeExpiredAsync(msg => Console.WriteLine($"Expired: {msg.Payload}"));
await db.SubscribeEvictedAsync(msg => Console.WriteLine($"Evicted: {msg.Payload}"));
await db.AddSortedSetAsync("leaderboard", ("player2", 50), ("player1", 100));
IReadOnlyList<SortedSetEntry> rows = await db.GetSortedSetRangeByRankWithScoresAsync(
"leaderboard",
0,
-1,
descending: true);
SubscribeExpiredAsync and SubscribeEvictedAsync listen for keyspace events in
the client's fixed logical database. They use exact sharded
channels internally; the event key is the message payload. Enable the matching
server-side notify_keyspace_events classes (x and/or e, plus E for
keyevent channels), for example notify_keyspace_events = "Exe" in
ruvio.toml. In multi-shard deployments, key-event delivery is limited to the
shard receiving the subscription.
Connecting
Native idempotency is exposed as BeginIdempotencyAsync(key, fingerprint, owner, ttlMs) and CompleteIdempotencyAsync(key, fingerprint, owner, result).
Begin returns a RESP array (acquired, pending, conflict, or completed plus
result bytes); complete returns a boolean acknowledgment. The completion helper
takes text; ExecuteBinaryAsync supports arbitrary binary tokens/results with
IDEM key BEGIN ... / IDEM key COMPLETE .... No Lua is loaded.
Only acquired authorizes the caller to execute its operation. Neither replay
nor completion renews retention, and no ambiguous write is automatically retried.
See the native contract before using it for
external side effects.
Give the client one address. On connect it sends CLUSTER SLOTS. A standalone server replies with an error and the client keeps that one socket. A sharded server returns the slot map; the client then opens one socket per shard and sends each command to the shard that owns the key. MOVED refreshes the map and retries. Multi-key reads and writes (MGET, MSET, DEL, UNLINK, EXISTS, TOUCH) are split by slot and merged. FLUSHDB, FLUSHALL, and DBSIZE go to every shard.
await using var clusterConnection = await RuvioConnection.ConnectAsync("127.0.0.1", 6379);
RuvioClient cluster = await clusterConnection.GetDatabaseAsync(0);
await cluster.SetAsync("session:ada", "hello");
await cluster.SetAsync("somekey", "on-another-shard");
IReadOnlyList<string?> values = await cluster.GetManyAsync("session:ada", "somekey");
RuvioClient.GetHashSlot(key) is the same 0–16383 slot Ruvio uses, including {hash tags}. Set RuvioClientOptions.DiscoverCluster to false to stay on the seed socket.
Concurrent commands
Command sockets multiplex concurrent calls: one writer sends complete RESP frames without waiting for their replies, and one reader matches replies to callers in FIFO order. Each shard/database socket has its own queues. Pub/Sub still uses dedicated sockets. Reuse the connection rather than opening one connection per request. Keep argument lists and binary argument memory unchanged until the call completes.
The writer is demand-driven: an atomic reservation lets the first caller start writing directly when there are no scheduled writes, bypassing the outgoing channel. Concurrent producers enqueue behind that owner. The owner drains queued frames and releases ownership only after all reserved writes have been consumed; it waits asynchronously if a producer has reserved a slot but not yet published. There is no idle writer task, spin loop, per-request worker or reader-mode switch. Pending replies do not prevent direct writing when the writer is idle.
RuvioClientOptions.MaxInFlightRequests defaults to 256 and must be positive. It
limits admitted command groups per socket, including queued writes and pending
replies; one ExecuteManyAsync pipeline is one group. Additional calls asynchronously
wait for capacity. This is not a byte limit: large arguments, replies, and pipelines
still need appropriately sized application limits. Ordinary commands are encoded
by the writer into pooled buffers that are returned after writing, not after the
server replies. Raw frames and cancellable binary/string argument lists are copied
after admission so caller-owned memory can be reused after cancellation.
The queues use bounded Channel<T> and admission uses SemaphoreSlim; this design
is not lock-free. There is no per-request socket write lock, linked
CancellationTokenSource, timer, dedicated thread, or Task.Run.
Caller continuations run asynchronously rather than inside the reply reader.
The socket.queue trace/histogram covers admission, writer queueing and encoding.
socket.write covers the socket write. socket.reply covers publication to the
reply queue through completion of parsing, including pipelined waiting and the
socket write; it overlaps socket.write and is not network-only time.
Cancellation observed by the writer before dispatch skips the command.
After dispatch it cancels the caller's wait, not the shared socket: the reader must
still consume that command's replies. Cancellation does not prove a mutation was
not executed. A transport failure fails pending calls; ambiguous mutations are not
automatically replayed. Do not interleave connection-scoped commands such as
MULTI/EXEC or blocking commands with unrelated concurrent calls. Send a complete
transaction in one ExecuteManyAsync group or use a separate connection.
The library is marked AOT-compatible. Ruvio.Client.Tour publishes a native binary (PublishAot). Command names such as GET and GEOADD are UTF-8 spans generated from RespVerbs.txt, so a standalone GET or INCR does not allocate an argument array for the verb.
Client CPU cost, separate from socket time, is in Ruvio.Client.Benchmarks (BenchmarkDotNet). It writes a GET and a GEOADD frame, a pipeline of 32 GETs, and reads an integer, a bulk, and an array on a reused reader. It also hashes a plain key and a hash tag.
dotnet run -c Release --project clients/csharp/Ruvio.Client.Benchmarks
For a short live socket comparison, start a dedicated loopback server, then run:
ruvio --address 127.0.0.1:16389 --durability memory --data-dir /tmp/ruvio-client-load
dotnet run -c Release --project clients/csharp/Ruvio.Client.Benchmarks -- --socket-load 16389 3
This writes a JSON summary for GET (256-byte values), SET (256-byte values), and LIMIT at 1, 2, 4, 16, and 128 concurrent callers. Each case warms up for one second and measures for three seconds. It includes throughput, mean/p50/p95/p99, process-wide allocated bytes per operation, GC counts, CPU time, end-of-case working set and the most recent GC's heap size. Latency buckets have 1-microsecond resolution, with a 100-ms overflow bucket; allocation totals include the load harness. Working set is not peak memory, and heap size is not a live retained-size measurement. Run the same command in separate client processes before/after a change with the same server and no other benchmark/build running. This is a closed-loop client test, not an ASP.NET middleware benchmark or a Redis comparison.
A connection owns the command clients it creates. Each standalone database client
uses a dedicated command connection so commands for separate databases never race
through mutable SELECT state. The connection is the only object that should be
disposed:
await using var connection = await RuvioConnection.ConnectAsync("127.0.0.1", 6379);
IRuvioClient cache = await connection.GetDatabaseAsync(2);
IRuvioClient sessions = await connection.GetDatabaseAsync(3);
IRuvioClient main = await connection.GetDatabaseAsync(0);
await using var securedConnection = await RuvioConnection.ConnectAsync(new RuvioClientOptions
{
Host = "10.0.0.5",
Port = 6379,
Username = "app",
Password = "secret",
Database = 4,
ConnectTimeout = TimeSpan.FromSeconds(2),
});
IRuvioClient secured = await securedConnection.GetDefaultDatabaseAsync();
The initial connection opens the configured RuvioClientOptions.Database; other
databases are created on demand and cached by the owning connection. A nonzero
database sends SELECT on its own socket. Indexes run from 0 to the server's
databases setting minus one (16 by default, at most 256). Cluster mode has only
database 0; requesting another index throws.
Database clients are fixed to the database they were created for. SELECT is
not exposed on the typed API and is rejected through raw command and pipeline
methods. Disposing the owning connection invalidates and closes every client and
socket it created.
Interfaces
RuvioConnection is the resource owner and RuvioClient is its database-scoped command client:
IRuvioConnectioncreates/caches clients throughGetDatabaseAsyncand owns endpoint/status information, reconnect, lifecycle events, and disposal.IRuvioClientexposes the commands and immutable database/endpoint information. Applications pass it to command-oriented services; disposing the connection invalidates all clients.
Ruvio.Client.AspNetCore registers one connection owner and its configured
default IRuvioClient; both share the same DI lifetime boundary.
FlushDatabaseAsync, FlushAllDatabasesAsync, SwapDatabasesAsync, and MoveKeyAsync send FLUSHDB, FLUSHALL, SWAPDB, and MOVE. See docs/LOGICAL_DATABASES.md.
Options
| Command | Methods |
|---|---|
SET |
SetAsync (PX, NX, XX), SetAsync(key, value, DateTimeOffset) (PXAT), SetKeepingTimeToLiveAsync (KEEPTTL), SetAndGetAsync (GET) |
LPOP / RPOP |
one element or a count |
SPOP / SRANDMEMBER |
one member or a count; a negative SRANDMEMBER count may repeat |
HSCAN / SCAN |
MATCH and COUNT |
ZADD |
(member, score) tuples, SortedSetEntry, or one member, score; SortedSetAddFlags: NX, XX, GT, LT, CH; IncrementSortedSetAsync(..., flags) sends INCR |
HEXPIRE family |
ExpireHashFieldsAsync(key, ttl, [condition,] fields) sends HEXPIRE for whole seconds, otherwise HPEXPIRE; FieldExpireCondition: NX, XX, GT, LT. ExpireHashFieldsAtAsync sends HEXPIREAT or HPEXPIREAT. One reply per field |
ZPOPMIN / ZPOPMAX |
optional count; PopSortedSetMinBlockingAsync / PopSortedSetMaxBlockingAsync send BZPOPMIN / BZPOPMAX for one key or a key list |
XADD |
AddStreamEntryAsync(key, id, maxLength, ...) sends MAXLEN |
XRANGE / XREVRANGE |
optional COUNT |
XREAD / XREADGROUP |
StreamReadOptions: COUNT, BLOCK, NOACK (group reads only) |
XGROUP CREATE |
createStream: true sends MKSTREAM |
XPENDING |
summary, or the extended form with IDLE, a range, a count, and a consumer |
XCLAIM |
StreamClaimOptions: IDLE, TIME, RETRYCOUNT, FORCE, LASTID; ClaimStreamEntryIdsAsync sends JUSTID |
INFO |
optional section; prometheus returns Prometheus text |
FUNCTION LOAD |
LoadFunctionAsync(name, body); the body is plain Lua and is lost on restart |
A blocking read (BLPOP, XREAD BLOCK, XREADGROUP BLOCK) holds the connection until it returns. Use a separate client for it.
Subscribe opens a second TCP socket. GET / SET / PUBLISH stay on the command socket. Register Message or SubscribeAsync(channel, handler) (StackExchange / Garnet app style), or await foreach ListenAsync. ReadPubSubMessageAsync still works. IsConnected is false when every command socket is broken.
A dropped socket is opened again before the next command, including a shard socket in cluster mode. The command that discovers the drop fails, so a write is not applied twice. One exception: a single GET, MGET, EXISTS, HGET, TTL, or PTTL is sent once more when the socket closes before any byte of the reply arrives. A pipeline is not retried. A reply that has already started is not retried. Pub/Sub channels are subscribed again on the new socket. ConnectionLost and ConnectionRestored report that, and ConnectionFailed still fires on the drop. Set RuvioClientOptions.Reconnect to false and call ReconnectAsync yourself, or set ReconnectDelay to own the wait. ReconnectDelay returning null stops the retry.
A password is sent as AUTH only when RuvioClientOptions.Password is set. One client runs commands one at a time. ExecuteManyAsync pipelines several commands on that connection. A server error reply leaves the connection usable. A broken read or write closes it.
Unreleased
- Breaking: every command method on
RuvioClient/IRuvioClientreturnsValueTaskorValueTask<T>instead ofTask/Task<T>. A pending command is a pooled object that is reused once it completes. Await each result once and do not keep it after awaiting. Do not call.Result/GetAwaiter().GetResult()on a result that has not completed. To useTask.WhenAll/WhenAnyor aTask-based interface, call.AsTask()once. Usualawait client.GetAsync(key)code only needs to be compiled again. On a local memory server with 16 callers, this cut allocations from about 1.1–1.4 KB to 56–336 B per command, and the 336 B left onGETis mostly the reply payload. - Frequent replies are shared rather than allocated: null,
+OK,+PONG,+QUEUED, the empty bulk string, and integers from -2 to 255.RespValueis immutable, so this is not visible to callers, but do not compare replies by reference. - Typed commands no longer build a
string[]or format integers into strings. Arguments are stored inline in aRespCommandstruct, and numbers are written straight to the buffer as invariant UTF-8. Simple-string replies such as+OKare matched without a string. ASET(typed or raw) now allocates 0 B per command; aGETallocates only the reply payload. Passing anullstring argument now throwsArgumentNullExceptionimmediately instead of returning a faulted task. - Hash field TTLs:
ExpireHashFieldsAsync,ExpireHashFieldsAtAsync,GetHashFieldTimeToLiveAsync,GetHashFieldTimeToLiveMillisecondsAsync,GetHashFieldExpireTimeAsync,GetHashFieldExpireTimeMillisecondsAsync, andPersistHashFieldsAsyncsendHEXPIRE,HPEXPIRE,HEXPIREAT,HPEXPIREAT,HTTL,HPTTL,HEXPIRETIME,HPEXPIRETIME, andHPERSIST. AcquireSemaphoreAsyncsendsSEMAPHORE;ReleaseAsyncfrees a semaphore token as well as a lease.IncrementWithLimitAsyncsendsINCRBY key delta MAX limitand returns null when the limit would be passed.- Sorted sets:
PopSortedSetMinAsync,PopSortedSetMaxAsync,PopSortedSetMinBlockingAsync,PopSortedSetMaxBlockingAsync,RemoveSortedSetRangeByRankAsync,RemoveSortedSetRangeByScoreAsync, andRemoveSortedSetRangeByLexAsyncsendZPOPMIN,ZPOPMAX,BZPOPMIN,BZPOPMAX,ZREMRANGEBYRANK,ZREMRANGEBYSCORE, andZREMRANGEBYLEX. - Cluster mode: a single-key command (
GET,SET,INCR, …) is routed from its inline arguments without building a key list, and each topology keeps one socket per node instead of looking it up by an"host:port"string on every call. Pipelines that hit one shard are sent as they are; multi-shard pipelines no longer use LINQ. - Array, integer-array, scan, stream, and
GEOADDreplies and requests are built with exact-size arrays instead of LINQ or closures. - A
GETsent on a socket that closed before any reply byte is sent once more on a new socket, asExecuteAsyncalready did. Before, the fastGETpath threw instead.
0.2.7
- The library is AOT-compatible and trimmable.
Ruvio.Client.Tourpublishes a native binary. GET,INCR, andGEOADDon a standalone connection write the command name from a generated UTF-8 span.
0.2.6
- A dropped command or shard socket is opened again before the next command. The command that sees the drop fails. A single
GET,MGET,EXISTS,HGET,TTL, orPTTLis sent once more when no reply byte arrived. Pipelines are not retried. Pub/Sub channels are subscribed again on the new socket. ConnectionLostandConnectionRestored(RuvioConnectionNotice: host, port, subscriber, error).ConnectionFailedstill fires on the drop.RuvioClientOptions.Reconnect(default true),MaxReconnectAttempts,ReconnectBaseDelay,ReconnectMaxDelay, andReconnectDelay.ReconnectDelayreturning null stops the retry and ignoresMaxReconnectAttempts.ReconnectAsyncopens the socket when automatic retry is off.
0.2.5
- Pub/Sub uses a dedicated socket.
SubscribeAsync(channel, handler),Message, andListenAsyncdeliver payloads without a manual read loop on the command connection. IsConnectedandConnectionFailed.
0.2.4
DeleteAsync(key)sendsDELfor one key. The list overload remains for many keys.PopListLeftBlockingAsync(key, timeout)andPopListRightBlockingAsync(key, timeout)sendBLPOP/BRPOPwithout a key list.
0.2.3
- A
MOVEDreply after a plain connect (or with discovery off) learns the slot map and retries, soSETon the seed port no longer throws.
0.2.2
- A sharded server is discovered from one seed address. Commands are routed by hash slot,
MOVEDis followed, andMGET/MSET/DELsplit across shards. GetClusterShardsAsync,GetClusterInfoAsync,GetClusterMyIdAsync,GetClusterKeySlotAsync,ReadOnlyAsync,ReadWriteAsync.GetHashSlot,IsCluster,ShardCount, andRuvioClientOptions.DiscoverCluster.
0.2.1
AddSortedSetAsync(key, ("ada", 100), ("bob", 50))takes(member, score)tuples, with or withoutSortedSetAddFlags.AddSortedSetAsync(key, member, score)adds one member and returns whether it was new.
0.2.0
ConnectAsync(host, port, database),ConnectAsync(IPAddress, port, database),ConnectAsync(IPEndPoint, database), andRuvioClientOptions.Database.RuvioConnection.GetDatabaseAsync,FlushDatabaseAsync,FlushAllDatabasesAsync,SwapDatabasesAsync,MoveKeyAsync.- The option overloads in the table above, and
PingAsync(message). - Breaking:
LoadFunctionAsync(body)becameLoadFunctionAsync(name, body). The old form sent a request Ruvio always refused.DeleteFunctionAsyncreturnsTask<bool>.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages (6)
Showing the top 5 NuGet packages that depend on Ruvio.Client:
| Package | Downloads |
|---|---|
|
Ruvio.Client.AspNetCore
ASP.NET Core integration for the Ruvio.Client RESP2 SDK. |
|
|
Ruvio.Extensions.Caching
Binary-safe IDistributedCache integration for Ruvio. |
|
|
Ruvio.AspNetCore.DataProtection
Append-only ASP.NET Core Data Protection key-ring storage on Ruvio. |
|
|
Ruvio.AspNetCore.Idempotency
Explicitly opted-in, bounded HTTP idempotency for ASP.NET Core and Ruvio. |
|
|
Ruvio.AspNetCore.RateLimiting
Opt-in distributed fixed-window HTTP rate limiting backed by Ruvio. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.13 | 92 | 10/5/2026 |
| 0.2.12 | 103 | 10/2/2026 |
| 0.2.11 | 101 | 10/2/2026 |
| 0.2.10 | 101 | 10/2/2026 |
| 0.2.9 | 161 | 10/2/2026 |
| 0.2.8 | 96 | 10/1/2026 |
| 0.2.7 | 96 | 9/30/2026 |
| 0.2.6 | 109 | 9/26/2026 |
| 0.2.5 | 115 | 9/26/2026 |
| 0.2.4 | 102 | 9/26/2026 |
| 0.2.3 | 107 | 9/26/2026 |
| 0.2.2 | 114 | 9/25/2026 |
| 0.2.1 | 115 | 9/25/2026 |
| 0.2.0 | 102 | 9/25/2026 |
| 0.1.3 | 94 | 9/23/2026 |
| 0.1.2 | 88 | 9/23/2026 |
| 0.1.1 | 99 | 9/23/2026 |
| 0.1.0 | 88 | 9/23/2026 |