Problem
AddInMemoryRedis() unconditionally wires up cross-node L1 invalidation via Redis Streams. There is currently no documented way to use the InMemoryRedis provider's L1+L2 tiering without this broadcast mechanism. Against Redis-compatible backends whose Streams support doesn't (yet) cover the consumer-group read side such as Microsoft Garnet, a RESP-compatible cache-store from Microsoft Research, not a fork of Redis this produces a continuous error loop as soon as the provider is used.
Environment
- Cache store: Microsoft Garnet (RESP-compatible)
- Provider:
InMemoryRedis
Steps to reproduce
builder.Services.AddCaching(builder.Configuration.GetSection("Caching"), cachingBuilder =>
cachingBuilder
.AddRedisConnection(connectionOptions =>
{
connectionOptions.ConnectionString = redisConnectionString; // pointed at Garnet
connectionOptions.AbortOnConnectFail = false;
connectionOptions.WarmUpOnStart = false;
})
.AddRedis()
.AddInMemoryRedis()
.AddMemory()
.AddLocalLock()
.AddRedisDistributedLock()
.AddResilienceStrategies()
,
options => { /* ... */ });
_cache = cacheFactory.CreateCache("InMemoryRedis");
Observed behavior
Once "InMemoryRedis" is requested via CreateCache, the following error repeats continuously (roughly every 250ms):
fail: UiPath.Caching.Broadcast.Redis.RedisStreamsTopic
Fetch events loop error
StackExchange.Redis.RedisServerException: ERR unknown command
at UiPath.Caching.Broadcast.Redis.RedisStreamSubjectWriter1.FetchBatch() at UiPath.Caching.Broadcast.Redis.RedisStreamSubjectWriter1.FetchLoop()
Commenting out .AddBroadcast() in the builder chain has no effect on this behavior. Per concepts.md, AddInMemoryRedis() wires broadcast internally regardless of whether .AddBroadcast() is called this isn't evident from the builder API itself, and the presence of .AddBroadcast() as a separate chainable method reasonably suggests it controls broadcast for all providers, including InMemoryRedis.
Impact
There is currently no way to use L1+L2 tiering against a backend without full Streams read support. The only available workaround is to request the plain Redis provider instead, which forgoes the in-process L1 tier entirely a real cost even for single-node deployments, where cross-node invalidation provides no benefit in the first place.
Separately, on a Dragonfly backend, we hit a related-but-distinct issue: StackExchange.Redis's automatic CLUSTER topology probe (issued against any server on connect) returned an announced endpoint different from our configured one, causing an unrelated reconnect loop. Not a UiPath.Caching issue, but noted here as another example of assumptions that don't hold uniformly across Redis-compatible backends.
Garnet's Streams support (for context)
Worth noting: Garnet does have some Streams support now (writes/ranges), but it doesn't yet support the read/consumer-group commands (XREAD/XREADGROUP) that UiPath.Caching's broadcast loop actually depends on so this isn't a bug on Garnet's side either, just a gap between what the two projects currently support. (Tracked upstream at microsoft/garnet#1379, if useful.)
Proposed solution
- An option to construct
InMemoryRedis with broadcast disabled, e.g.:
.AddInMemoryRedis(options => options.BroadcastEnable = false)
so L1+L2 tiering can be used independent of the backend's Streams read support, and independent of whether cross-node invalidation is even needed (e.g. single-node deployments).
- Documentation clarifying that
.AddBroadcast() does not affect InMemoryRedis behavior, so others don't spend time on the same assumption.
Alternatives considered
- Plain
Redis provider (current workaround) avoids the error, but forgoes in-process (L1) caching entirely, even in cases (single-node, or backends where Streams reads simply aren't available yet) where L1 would otherwise be safe and beneficial to use.
- Waiting on upstream Garnet Streams support doesn't help today, and doesn't address the single-node case where broadcast isn't needed regardless of backend capability.
Problem
AddInMemoryRedis()unconditionally wires up cross-node L1 invalidation via Redis Streams. There is currently no documented way to use theInMemoryRedisprovider's L1+L2 tiering without this broadcast mechanism. Against Redis-compatible backends whose Streams support doesn't (yet) cover the consumer-group read side such as Microsoft Garnet, a RESP-compatible cache-store from Microsoft Research, not a fork of Redis this produces a continuous error loop as soon as the provider is used.Environment
InMemoryRedisSteps to reproduce
Observed behavior
Once
"InMemoryRedis"is requested viaCreateCache, the following error repeats continuously (roughly every 250ms):fail: UiPath.Caching.Broadcast.Redis.RedisStreamsTopic
Fetch events loop error
StackExchange.Redis.RedisServerException: ERR unknown command
at UiPath.Caching.Broadcast.Redis.RedisStreamSubjectWriter1.FetchBatch() at UiPath.Caching.Broadcast.Redis.RedisStreamSubjectWriter1.FetchLoop()
Commenting out
.AddBroadcast()in the builder chain has no effect on this behavior. Perconcepts.md,AddInMemoryRedis()wires broadcast internally regardless of whether.AddBroadcast()is called this isn't evident from the builder API itself, and the presence of.AddBroadcast()as a separate chainable method reasonably suggests it controls broadcast for all providers, includingInMemoryRedis.Impact
There is currently no way to use L1+L2 tiering against a backend without full Streams read support. The only available workaround is to request the plain
Redisprovider instead, which forgoes the in-process L1 tier entirely a real cost even for single-node deployments, where cross-node invalidation provides no benefit in the first place.Separately, on a Dragonfly backend, we hit a related-but-distinct issue: StackExchange.Redis's automatic
CLUSTERtopology probe (issued against any server on connect) returned an announced endpoint different from our configured one, causing an unrelated reconnect loop. Not aUiPath.Cachingissue, but noted here as another example of assumptions that don't hold uniformly across Redis-compatible backends.Garnet's Streams support (for context)
Worth noting: Garnet does have some Streams support now (writes/ranges), but it doesn't yet support the read/consumer-group commands (
XREAD/XREADGROUP) thatUiPath.Caching's broadcast loop actually depends on so this isn't a bug on Garnet's side either, just a gap between what the two projects currently support. (Tracked upstream at microsoft/garnet#1379, if useful.)Proposed solution
InMemoryRediswith broadcast disabled, e.g.:so L1+L2 tiering can be used independent of the backend's Streams read support, and independent of whether cross-node invalidation is even needed (e.g. single-node deployments).
.AddBroadcast()does not affectInMemoryRedisbehavior, so others don't spend time on the same assumption.Alternatives considered
Redisprovider (current workaround) avoids the error, but forgoes in-process (L1) caching entirely, even in cases (single-node, or backends where Streams reads simply aren't available yet) where L1 would otherwise be safe and beneficial to use.