squirix exposes a typed gRPC client SDK. Cache operations use gRPC only; HTTP endpoints are limited to health and metrics.
Client SDK
Entry point (Squirix.Client):
using System;
using System.Threading;
using Squirix.Client;
await using var client = await SquirixClient.ConnectAsync(new Uri("https://localhost:5001"), CancellationToken.None);
var cache = await client.GetCacheAsync<T>("cache-name", CancellationToken.None);
ICache<T> methods (v0.1 exported surface):
| Method | Purpose |
|---|---|
AddAsync / TryAddAsync |
Insert if absent |
SetAsync |
Upsert with optional expiration |
UpdateAsync |
Update existing entry |
GetValueAsync / GetEntryAsync |
Read with explicit presence |
GetExpirationAsync |
Read expiration metadata |
GetOrAddAsync |
Read or insert factory value |
RemoveAsync |
Delete key |
TouchAsync / RemoveExpirationAsync |
Expiration management |
Prefer GetValueAsync for reads with explicit presence.
Writes accept (key, value, options?, cancellationToken). Expiration uses CacheEntryOptions, not CacheEntry<T>
write overloads.
When options is omitted or null, or neither CacheEntryOptions.Expiration nor CacheEntryOptions.ExpiresAt is set,
the entry is stored without expiration and does not expire by TTL. Pass an explicit relative or absolute expiration
when you need TTL eviction.
An absolute expiration (CacheEntryOptions.ExpiresAt, TouchAsync(key, DateTimeOffset)) is on the client clock. The client
sends the time left before it, so the entry lives that long even when the client and server clocks disagree. A value that is not
in the future on the client clock is rejected with ArgumentOutOfRangeException. GetEntryAsync reports ExpiresUtc on the
client clock too: the server returns the time the entry has left, and the client adds it to its own time.
SetAsync overwrites the whole entry, including its expiration: a SetAsync without expiration on a key that already has
a TTL clears that TTL, and the key no longer expires. To change only the value and keep the current expiration, use
UpdateAsync.
Out of scope for v0.1: batch, scan, watch, counters, tag invalidation, compare-and-set.
Configuration (SquirixClientOptions): endpoints, JWT bearer token provider, custom serializer.
See configuration.md and serialization.md.
Wire contract
gRPC contract: src/shared/Squirix/Transport/Grpc/Protos/SquirixCache.proto (shared source, not a separate NuGet package).
SquirixCacheService exposes 10 unary RPCs on the v0.1 server surface:
Wire RPC (SquirixCache.proto) |
gRPC client / ICache<T> |
Notes |
|---|---|---|
SetEntry |
SetAsync |
Upsert via CacheEntryWire (SetEntryAsync on generated client) |
TryAddEntry |
TryAddAsync / AddAsync |
Insert-if-absent via CacheEntryWire (TryAddEntryAsync on generated client); AddAsync throws when the key exists |
GetValue |
GetValueAsync |
Value-only read; returns found + value |
GetEntry |
GetEntryAsync |
Full entry read; returns found + entry (missing key → found=false, not an error) and the time left (remaining) |
GetExpiration |
GetExpirationAsync |
Expiration metadata only; handler reads via runtime GetEntry |
GetOrAdd |
GetOrAddAsync |
Single RPC with CacheEntryWire; client runs factory locally, server get-or-insert atomically; no value on add |
Update |
UpdateAsync |
Update value if key exists via CacheEntryWire (value field only) |
Remove |
RemoveAsync |
|
Touch |
TouchAsync |
Relative expiration (Duration) |
RemoveExpiration |
RemoveExpirationAsync |
Mutations that accept expiration use SetEntry / TryAddEntry with CacheEntryWire. There are no flat Set / TryAdd
value-only mutation RPCs on the wire surface.
The server runtime pipeline (ICacheApi) is entry-based only (eight methods). gRPC handlers translate wire requests into
that runtime surface; GetExpiration and GetOrAdd are wire convenience RPCs whose handlers may compose runtime calls
internally — the client SDK calls one RPC per exported method and does not stitch multiple RPCs together.
Wire RPC names omit the Async suffix; grpc-dotnet appends it on generated client methods (for example SetEntry →
SetEntryAsync). Public ICache<T> names stay SetAsync / TryAddAsync.
There is no Contains RPC. Prefer GetValueAsync or GetEntryAsync for presence checks (found=false is not an
error). HTTP on the primary listener is limited to health and metrics — there is no REST cache HEAD route in v0.1.
Value encoding
Every service lives in the proto package squirix.cache.v2, so a client and a server that disagree on the value encoding
meet as Unimplemented instead of silently corrupting values. A CacheValue is one of:
| Kind | Used for |
|---|---|
null_value |
null |
string_value |
string |
int64_value |
int and long |
double_value |
double |
bool_value |
bool |
json_utf8 |
everything else: objects, arrays, decimal, numbers of a JSON element |
json_utf8 holds exactly one JSON value as UTF-8: no byte order mark, valid UTF-8, no comments, no trailing commas, nesting
of at most 64 levels. The server checks it and refuses anything else with InvalidArgument before any durable effect, then
stores, journals, replicates and returns the very same bytes: it does not parse, reformat or canonicalize the JSON, so every
digit of a number survives. A Remove response carries only removed. A GetOrAdd response carries the value only when
the key already held one (added=false, found=true); when it added, the caller already has the value.
Mutating gRPC RPCs require a non-empty operation_id of exactly 32 lowercase hex characters (UUID without
hyphens, for example 0123456789abcdef0123456789abcdef). The Squirix client SDK generates a fresh id per mutating
call via RpcOperationIdentity.New(); custom gRPC clients must supply a conforming value. Over-length or malformed ids
are rejected with gRPC InvalidArgument. The server deduplicates retries with the same operation_id and rejects
reuse with a different payload (FailedPrecondition).
Entry limits (v0.1)
| Limit | Value |
|---|---|
| Encoded entry payload | 4 MiB |
| Tags per entry | 32 |
| Tag key UTF-8 size | 256 bytes |
| Tag value UTF-8 size | 1024 bytes |
Violations return stable public codes: INVALID_ENTRY_TAGS as gRPC InvalidArgument, and PAYLOAD_TOO_LARGE as
gRPC ResourceExhausted. Other live public codes include INVALID_CACHE_KEY, TOO_MANY_REQUESTS,
MEMORY_PRESSURE, JOURNAL_DISK_QUOTA, and the OPERATION_ID_* family. Cache names still fail as
ArgumentException → gRPC InvalidArgument without a structured INVALID_CACHE_NAME code.
Request Routing
In a multi-node cluster, the entry node forwards the client request to the key owner (the replica group leader when automatic failover is on) over internode gRPC unchanged, with the client operation_id and without running the cache pipeline itself.
Only the owner records the outcome: idempotency records are per-node in memory (durable nodes also persist outcomes through the journal/snapshot path), and the entry node keeps none.
Retry Behavior
When a retry lands on a different entry node (bootstrap endpoint switch or transport failover), the owner node replays the cached outcome for the same operation_id and fingerprint so the mutation is not applied twice.
The owner response and errors reach the client unchanged; an unreachable owner surfaces as Unavailable.
Replay Windows
-
In a replicated cache an outcome is replayable for at least 30 seconds by the clock of the node that answers the retry; with node clocks within 10 seconds of each other, it is replayable for at least 20 seconds after the leader decided it, across a leader change or a restart too.
-
The
squirixclient retries within its 15 s operation deadline, so it is covered. -
A raw gRPC client that retries later than that may get a new execution of the mutation.
-
On an RF=1 node an outcome is replayable for at least 30 seconds, and up to 15 minutes while the byte budget of the store has room.
Conditional Writes
A replicated host dedupes every write in the replica group of its key and keeps no node-wide record of it.
There, a conditional write that took no effect (TryAddEntry or GetOrAdd over a present key, Update of an absent key) is evaluated again on retry instead of replayed, and the read of GetOrAdd reflects the current value.
Store Capacity
Operators can tune the in-memory store caps of an RF=1 node with SQUIRIX_IDEMPOTENCY_* — see configuration.md — Idempotency store capacity.
RPC Surface
The approved RPC list is locked by a golden snapshot test:
tests/squirix.server/squirix.server.unit-tests/ApiSnapshots/SquirixGrpcEndpointSurface.golden.txt.
Transport And Authentication
Transport requires HTTPS endpoints. Cleartext http:// URLs are rejected at configuration time.
Authentication uses JWT bearer tokens when enabled via SquirixClientOptions.BearerTokenProvider.
Cache names
Validation rules for logical cache names: cache-name-validation.md.