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?