Redis cache errors/retries due to bool value

We have encoutered an issue where attempts to cache a bool value in Redis are producting a repeating exception.

ServiceStack.Redis.RedisException: [09:37:49.828] Unable to Connect: sPort: 63912, Error: Object reference not set to an instance of an object.
   at ServiceStack.Redis.RedisNativeClient.WriteAllToSendBuffer(Byte[][] cmdWithBinaryArgs) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisNativeClient_Utils.cs:line 437
   at ServiceStack.Redis.RedisNativeClient.SendReceive[T](Byte[][] cmdWithBinaryArgs, Func`1 fn, Action`1 completePipelineFn, Boolean sendWithoutRead, String operation) ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at ServiceStack.Redis.RedisNativeClient.WriteAllToSendBuffer(Byte[][] cmdWithBinaryArgs) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisNativeClient_Utils.cs:line 437
   at ServiceStack.Redis.RedisNativeClient.SendReceive[T](Byte[][] cmdWithBinaryArgs, Func`1 fn, Action`1 completePipelineFn, Boolean sendWithoutRead, String operation)
   --- End of inner exception stack trace ---
   at ServiceStack.Redis.RedisNativeClient.CreateConnectionError(Exception originalEx) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisNativeClient_Utils.cs:line 371
   at ServiceStack.Redis.RedisNativeClient.SendReceive[T](Byte[][] cmdWithBinaryArgs, Func`1 fn, Action`1 completePipelineFn, Boolean sendWithoutRead, String operation)
   at ServiceStack.Redis.RedisNativeClient.SendExpectData(Byte[][] cmdWithBinaryArgs) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisNativeClient_Utils.cs:line 786
   at ServiceStack.Redis.RedisNativeClient.Set(String key, Byte[] value, Boolean exists, Int32 expirySeconds, Int64 expiryMs) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisNativeClient.cs:line 490
   at ServiceStack.Redis.RedisClient.<>c__DisplayClass467_0`1.<Add>b__0(RedisClient r) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisClient.ICacheClient.cs:line 90
   at ServiceStack.Redis.RedisClient.Exec[T](Func`2 action) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisClient.ICacheClient.cs:line 29
   at ServiceStack.Redis.RedisClient.<>c__DisplayClass470_0`1.<Add>b__0(RedisClient r) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisClient.ICacheClient.cs:line 110
   at ServiceStack.Redis.RedisClient.Exec[T](Func`2 action) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisClient.ICacheClient.cs:line 29
   at ServiceStack.Redis.RedisClient.Add[T](String key, T value, DateTime expiresAt) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisClient.ICacheClient.cs:line 108
   at ServiceStack.Redis.RedisClientManagerCacheClient.Add[T](String key, T value, DateTime expiresAt) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack.Redis/src/ServiceStack.Redis/RedisClientManagerCacheClient.cs:line 101
   at ExtremeReach.Api.Public.PublicApiServices.NetworkService.IsDestinationCacheEnabledForCallerCustomer(IServiceClient customerApiClient) in D:\build\1\s\api\ExtremeReach.Api.Public\ExtremeReach.Api.Public\PublicApiServices\NetworkService.cs:line 252

The call being made is a simple cache.Add, which we use everywhere, but couldn’t see why it caused so many errors withing our single API call. We inspected ServiceStack.Redis/src/ServiceStack.Redis at master · ServiceStack/ServiceStack.Redis · GitHub to investigate the code path.
RedisClient.ICacheClient.cs:line 108 .RedisClientManagerCacheClient.Add[T](String key, T value, DateTime expiresAt)
but under the hood it invokes:
RedisNativeClient.Set(String key, Byte[] value, Boolean exists, Int32 expirySeconds, Int64 expiryMs)
byte is the param now, not generic type T, because this the Redis client calls the ToBytes() method.

//Looking up Dictionary<Type,bool> for type is faster than HashSet<Type>.
        private static readonly Dictionary<Type, bool> numericTypes = new Dictionary<Type, bool> {
            { typeof(byte), true},
            { typeof(sbyte), true},
            { typeof(short), true},
            { typeof(ushort), true},
            { typeof(int), true},
            { typeof(uint), true},
            { typeof(long), true},
            { typeof(ulong), true},
            { typeof(double), true},
            { typeof(float), true},
            { typeof(decimal), true}
        };

        private static byte[] ToBytes<T>(T value)
        {
            var bytesValue = value as byte[];
            if (bytesValue == null && (numericTypes.ContainsKey(typeof(T)) || !Equals(value, default(T))))
                bytesValue = value.ToJson().ToUtf8Bytes();
            return bytesValue;
        }

Our code is passing isEnabled (bool) to ICacheClient.Add, but boolean, which is clearly a value type in .NET - not a reference type, is not in the numericTypes (which contains only numerics btw), so it may be getting improperly converted to a Redis value, as ToBytes appears to execute this branch:
var bytesValue = value as byte[]; => { 0x32 0x32 0x32 0x32 0x32 0x32 0x32 }
not
bytesValue = value.ToJson().ToUtf8Bytes(); => 'false' / 'true' and then ToUtf8Bytes

…and this might lead to an invalid value, which may cause the retry, throw error

RedisNativeClient_Utils.cs:line 371:private RedisRetryableException CreateNoMoreDataError()
        {
            Reconnect();
            return CreateRetryableResponseError("No more data");
        }

Is this a potential bug in the ServiceStach Redis client, or are we barking up the wrong tree?

What is the Redis Client Code that’s causing this Exception?

The exception is being triggered from the call to ServiceStack.Redis.RedisClientManagerCacheClient.Add[T](String key, T value, DateTime expiresAt)
but the actual exception appears to be down the stack at
ServiceStack.Redis.RedisNativeClient.WriteAllToSendBuffer(Byte[][] cmdWithBinaryArgs)
Our hypothesis is the attemtp to convert the bool to byte[] in the ToBytes() method is failing and resulting in a null being passed into WriteAllToSendBuffer(). However, this may be an incorrect assessment.

I’m trying to repro this, please provide the client code and parameter values I can run to generate this Exception

Here’s some pseudo-code that represents the code in the stack trace from above. I removed and changed a few of the pieces that generate the values used in the Cache.Add method call.

This is executing in the NetworkService class that derives from the standard ServiceStack.Service class that provides the Cache instance, if that helps.

var cacheKey = $"IsEnabledCacheKey";
var isEnabled = false;

... // Code to load data isEnabled from configuration or leave default value

var cacheDurationInMinutes = ...; // int value loaded from configuration provider
Cache.Add(cacheKey, isEnabled, DateTime.UtcNow.AddMinutes(cacheDurationInMinutes)); // Cache is the Service class' ICacheClient instance

Cheers, this should now be resolved from this commit.

This change is available from the latest v8.3.1 that’s now available in the pre release packages.