Skip to main content

Orleans Redis provider compatibility

Run the official Orleans Redis providers on Respire by injecting the Respire.StackExchangeCompat adapter. You keep the official providers and their Redis storage format; no separate native provider is needed.

Tested packages, all at 10.2.2 (Orleans commit a575888):

  • Microsoft.Orleans.Clustering.Redis
  • Microsoft.Orleans.Persistence.Redis
  • Microsoft.Orleans.Reminders.Redis
  • Microsoft.Orleans.Server (the acceptance suite's silo runtime and grain code generator)

Other versions, including 10.4.0, are not assessed. A new version needs a new compatibility check.

Configure the official providers​

Install Respire.StackExchangeCompat and the three providers at 10.2.2. Pass the adapter through each provider's CreateMultiplexer option:

using Microsoft.Extensions.Hosting;
using Orleans.Hosting;
using Respire;
using Respire.StackExchangeCompat;
using StackExchange.Redis;

// Keep this multiplexer alive until the silo has stopped, then dispose it.
IConnectionMultiplexer multiplexer = RespireConnectionMultiplexer.Create(
RespireOptions.Parse("redis://localhost:6379/0"));
var configuration = ConfigurationOptions.Parse("localhost:6379,defaultDatabase=0");

var builder = Host.CreateApplicationBuilder();
builder.UseOrleans(siloBuilder =>
{
siloBuilder.UseRedisClustering(options =>
{
options.ConfigurationOptions = configuration;
options.CreateMultiplexer = _ => Task.FromResult((multiplexer, true));
});
siloBuilder.AddRedisGrainStorageAsDefault(options =>
{
options.ConfigurationOptions = configuration;
options.CreateMultiplexer = _ => Task.FromResult((multiplexer, true));
});
siloBuilder.UseRedisReminderService(options =>
{
options.ConfigurationOptions = configuration;
options.CreateMultiplexer = _ => Task.FromResult((multiplexer, true));
});
});
  • true in the tuple is IsShared: Orleans borrows the multiplexer and does not dispose it.
  • For provider-owned connections, return a separate adapter instance from each factory with IsShared = false. RespireConnectionMultiplexer.Create(options) owns its native clients; when using Wrap, pass ownsClient: true if the provider should also dispose the native client. Never return one shared instance with false from several providers: the first provider to stop would close the others' connection.
  • Orleans' validators still require ConfigurationOptions. Keep its endpoint, database, and credentials the same as the Respire options.
  • Configure service and cluster IDs, serializer, expiry, and clear mode as usual.
  • For client gateway discovery, set the same clustering CreateMultiplexer option on the Orleans client builder.
  • A ServiceKey can supply a DI-registered multiplexer through the same factory.

What the providers use​

All calls use CommandFlags.None and GetDatabase() with no arguments, so the configured default database applies.

Provider/sourceMembers actually called
RedisMembershipTableIDatabase.KeyDeleteAsync(RedisKey), HashSetAsync(RedisKey, RedisValue, RedisValue, When), KeyExpireAsync(RedisKey, TimeSpan?), HashGetAllAsync, HashDeleteAsync(RedisKey, RedisValue), CreateTransaction()
Membership transactionsITransaction.HashSetAsync with When.Always, HashGetAsync(RedisKey, RedisValue), AddCondition(Condition.HashEqual(...)), AddCondition(Condition.HashNotExists(...)), ExecuteAsync(), ConditionResult.WasSatisfied after abort
RedisGatewayListProviderNo direct Redis interface calls; uses the membership table
RedisGrainStorageIDatabase.HashGetAllAsync, ScriptEvaluateAsync(string, RedisKey[], RedisValue[], CommandFlags) for write, clear, and delete-on-clear
RedisReminderTableIDatabase.KeyExpireAsync(RedisKey, TimeSpan?), KeyDeleteAsync(RedisKey), SortedSetRangeByValueAsync(RedisKey, RedisValue, RedisValue, Exclude, long, long, CommandFlags), SortedSetRemoveRangeByValueAsync, ScriptEvaluateAsync(string, RedisKey[], RedisValue[], CommandFlags)

The adapter implements all of these. Key behaviors:

  • ScriptEvaluateAsync sends the provider's Lua script in one EVAL, so ETag compare-and-write, clear, and delete stay atomic.
  • SortedSetRangeByValueAsync uses ZRANGEBYLEX with the same bounds, including wrapped hash-ring ranges.
  • SortedSetRemoveRangeByValueAsync uses ZREMRANGEBYLEX, so a stale ETag cannot remove a replacement reminder.

None of the providers use IBatch, ISubscriber, server discovery, profiling, or ChannelMessageQueue. See the adapter guide for script and flag limits.

Tested scope​

tests/Respire.Orleans.Acceptance.Tests runs upstream provider scenarios against StackExchange.Redis and then the adapter, on .NET 8 and .NET 10 with RESP2 and RESP3. Ten additional adapter-only cases start real Orleans silo and client hosts. All containers use Redis 7.0.15. Its NOTICE.md records sources and exclusions.

ProviderCovered
ClusteringInsert/read, stale versions, duplicate inserts, update, heartbeat, cleanup, gateway filtering
PersistenceWrite/read, both clear modes, stale/null ETag rejection, concurrent writers, expiry (TTL set, not waited out), binary state, interoperability with StackExchange.Redis
RemindersUpsert/read, removal, interoperability, multiple grains, hash-ring ranges, concurrent upserts

Interoperability tests write with one client and read with the other, so data written by the official StackExchange.Redis setup works with the adapter and back.

Silo lifecycle and Redis recovery​

The real-host scenarios configure Redis membership, default grain storage, reminders, and client gateway discovery through the adapter. No provider calls fall back to StackExchange.Redis. A small integer-state grain uses the Orleans binary serializer; test-only reminder settings lower the minimum period to one second.

DeploymentCovered
Standalone, shared multiplexerGrain activation, persisted write/read, real reminder delivery, graceful silo shutdown, complete silo restart with the same service/cluster IDs, reminder-driven reactivation without re-registering, state surviving restart
Standalone, provider-owned multiplexersThe same lifecycle; membership, storage, reminders, and client discovery each own a separate adapter/native client. Shutdown asserts both layers are disposed, while stopping the silo leaves the still-running client's gateway connection usable
Three-primary Redis Cluster, database 0The shared lifecycle scenario, including membership transactions, persistence scripts, reminder delivery and restart through native Cluster routing
Standalone socket lossDisconnect live Redis TCP connections with CLIENT KILL, reconnect with the same adapter and grain activation, write state and deliver reminders after recovery, join a second silo, then reactivate there and read the saved state
Sentinel (one primary, one replica, three Sentinels)Fence completed writes to the replica, stop the primary, observe the same native client connect to the promoted replica, continue grain writes/reminders, join a second silo, return the old primary as a replica, then verify state from a new activation
Cluster slot migrationMove the membership key between primaries, finish slot ownership changes, then join another silo and use grain storage through the existing adapter. While the slot is importing/migrating, insert and update membership rows, reject duplicate inserts and stale table versions without partial writes, and update heartbeats through ASK routing
Cluster with three primaries and three replicasCoordinated CLUSTER FAILOVER on all three replicas while a silo is running; retain its activation, write state, receive reminders, join another silo, then reactivate there and read saved state
Abrupt silo process death, standalone RedisStart a second silo in a child process, kill it without graceful shutdown, and require the surviving silo to mark its row Dead within 60 seconds. The test logs elapsed detection time, verifies the survivor remains active, and calls a grain through client discovery

The topology cases use Respire.Testing.Containers, which runs each deployment in an isolated disposable container. They model server processes and protocol routing, not independent host or network failures. The socket-loss and replication-fence setup uses a separate administrative control connection only. Recovery hosts run in a child process with a four-minute budget after Redis startup. The parent captures server logs and removes its container even if Orleans shutdown ignores cancellation. Diagnostics, process termination, output drain and container disposal extend the maximum wait to 310 seconds after child startup; Docker cleanup failures are reported. A separate failed-election regression removes the only replica before stopping the primary and requires recovery to fail. It leaves Sentinel TILT protection enabled and retains the healthy failover assertions. The tests do not claim lossless writes during failover: Redis replication is asynchronous. The Sentinel scenario fences the writes it expects to retain before stopping the primary; the Cluster scenario uses coordinated promotion, which synchronizes the replica before switching roles. Unplanned Cluster primary crashes and writes racing the promotion are not covered by that test.

The silo-death deadline is a test bound, not a production eviction guarantee. Tests set one-second probes, membership refreshes and heartbeats, two missed probes, and one death vote. Production detection time depends on Orleans membership settings, cluster size, scheduling and connectivity. Eviction here means the membership row becomes Dead; historical rows need not be deleted immediately.

Supported topologies and CI limits​

Standalone Redis, database-0 Redis Cluster (including membership writes during ASK migration), and Sentinel are supported through the adapter within the coverage above. When WATCH receives ASK, the adapter discards the rejected connection and establishes a fresh WATCH on the importing node with ASKING. It evaluates the original conditions again, then sends ASKING and MULTI on that same watched connection before the queued commands and EXEC. A concurrent write still aborts the watch; a stale membership version still returns false without changing the row or table version.

WATCH setup follows at most the native Cluster redirect limit and shares one CommandTimeout budget across its attempts. Closing the adapter with allowCommandsToComplete: false cancels setup. Once WATCH succeeds, commit-time routing errors and connection failures propagate; the adapter never replays an accepted or ambiguous commit. Applications must still tolerate failed membership operations when topology changes race a transaction. Native CreateTransactionAsync retains its explicit RespireTransactionRetryException contract on Cluster rejections.

The provider's unconditional read transactions also handle ASK: only a complete routing rejection can start one new attempt on an exclusive importing connection, using ASKING before MULTI. An EXEC result array, including partial command errors, is never replayed. Errors from the importing attempt propagate to the caller.

Multi-host network partitions are not validated in CI. The Redis fixture publishes loopback endpoints and runs nodes in one container; silo hosts also advertise loopback. Disconnecting that container does not model asymmetric silo-to-silo, silo-to-Redis, or independent host failures. Such a test needs silos and Redis nodes with independently routable addresses and selective network fault injection (for example network namespaces and packet filters). The current CI fixture has no such harness. No partition-tolerance or split-brain claim follows from the socket-loss, process-death, or failover cases.

For Cluster, set RespireOptions.UseCluster = true, configure reachable seed/advertised node endpoints, and use database 0. Nonzero Cluster databases are unsupported. Provider transactions and scripts each operate on one Redis key, so the tested providers do not require a common hash tag across their keys. For Sentinel, configure the native adapter with Sentinel endpoints and SentinelPrimaryName; keep ConfigurationOptions.ServiceName and the provider configuration consistent. Combining Cluster and Sentinel routing in one adapter is unsupported. Provider configuration alone does not configure native routing.

Run the suite from a repository checkout (requires Docker):

$env:RESPIRE_TEST_PROTOCOL = '2' # Repeat with '3'.
dotnet run --project tests/Respire.Orleans.Acceptance.Tests -c Release -f net10.0 -- --minimum-expected-tests 28
# Repeat with -f net8.0 as well.

Not validated:

  • Multi-host partitions, long outages, sustained stress, other serializers and datasets.
  • Unplanned Cluster primary crashes, failover racing provider transactions, and uninterrupted availability during arbitrary topology changes.
  • Provider-owned topology recovery, TLS/authentication changes, Redis data loss/restart without retained data, and simultaneous Redis and silo failures.
  • Grain directory, streaming, and journaling providers, and other Orleans versions.

Topology and silo-death assessment extends the original lifecycle assessment. Neither assessment establishes support for the untested deployment scenarios above.