2

My problem is very similar to this one: Protocol errors, "no more data" errors, "Zero length response" errors while using servicestack.redis in a high volume scenario

I'm using ServiceStack v3.9.54.0 in a C# web application working on IIS. I could see the errors in both Redis versions 2.8.17 and 3.0.501.

The errors I've been receiving are the following:

ServiceStack.Redis.RedisResponseException: Unexpected reply: +PONG, sPort: 65197, LastCommand: GET EX:KEY:230
at ServiceStack.Redis.RedisNativeClient.CreateResponseError(String error)
at ServiceStack.Redis.RedisNativeClient.ParseSingleLine(String r)
at ServiceStack.Redis.RedisNativeClient.ReadData()
at ServiceStack.Redis.RedisNativeClient.SendExpectData(Byte[][] cmdWithBinaryArgs)
at ServiceStack.Redis.RedisNativeClient.GetBytes(String key)
at ServiceStack.Redis.RedisNativeClient.Get(String key)

And:

ServiceStack.Redis.RedisResponseException: Unknown reply on integer response: 43PONG, sPort: 59017, LastCommand: EXISTS EX:AnKey:Cmp6
at ServiceStack.Redis.RedisNativeClient.CreateResponseError(String error)
at ServiceStack.Redis.RedisNativeClient.ReadLong()
at ServiceStack.Redis.RedisNativeClient.SendExpectLong(Byte[][] cmdWithBinaryArgs)
at ServiceStack.Redis.RedisNativeClient.Exists(String key)
at Redis.Documentos.RedisBaseType.Exists(String key)

The first thing that I thought was that I was sharing the Redis Connection across multiple threads, but I can't see the problem on my singleton implementation of the PooledRedisClientManager (Configs is a static class that stores the connection information):

public class RedisProvider
{
    public PooledRedisClientManager Pool { get; set; }
    private RedisProvider()
    {

        var srv = new List<string> { $"{Configs.Server}:{Configs.Port}" };

        Pool = new PooledRedisClientManager(srv, srv, null, 
            Configs.Database, Configs.PoolSize, Configs.PoolTimeout);
    }

    public IRedisClient GetClient()
    {
        try
        {
            var connection = (RedisClient)Pool.GetClient();
            return connection;
        }
        catch (TimeoutException)
        {
            return null;
        }
    }

    private static RedisProvider _instance;
    public static object _providerLock = new object();
    public static RedisProvider Provider
    {
        get
        {
            lock (_providerLock)
            {
                if (_instance == null)
                {
                    var instance = new RedisProvider();
                    _instance = instance;
                    return _instance;
                }
                else
                {

                    return _instance;
                }
            }
        }
    }

}

All the clients are obtained through the pool, as follows:

var redis = (RedisClient)RedisProvider.Provider.GetClient();

I'm sure that the redis var is not shared across multiple threads and, as far as I can see, this code shows a proper thread-safe implementation...

Any help would be much appreciated.


Edit: As per some technologies that I use, I have no access to the App Startup code nor can use using blocks. So, I wrap all clients like that:

RedisClient redis;
try {
    redis = (RedisClient)RedisProvider.Provider.GetClient();
    // Do stuff
} finally {
    redis.Dispose();
}
Community
  • 1
  • 1
Dinei
  • 4,494
  • 4
  • 36
  • 60

1 Answers1

2

This error message is an indication the same redis client instance is being shared across multiple threads, the source code provided doesn't provide any verification that it's not.

The above RedisProvider is just a more verbose version of access wrapped around a singleton, e.g:

public static class RedisProvider
{
    public static IRedisClientManager Pool { get; set; }

    public static RedisClient GetClient()
    {
        return (RedisClient)Pool.GetClient();
    }
}

The RedisManager only needs to be initialized once on App Startup:

var srv = new List<string> { $"{Configs.Server}:{Configs.Port}" };
RedisProvider.Pool = new PooledRedisClientManager(srv, srv, null, 
    Configs.Database, Configs.PoolSize, Configs.PoolTimeout);

From then on, the verbose locking just adds overhead and doesn't provide any thread-safety benefits over accessing the Singleton RedisManager directly.

Whilst resolving the client is ThreadSafe:

var redis = RedisProvider.GetClient();

The redis client instance returned is not Thread-Safe (as per .NET conventions). As a result you need to make sure you're not sharing the same instance across multiple threads, you also need to ensure the client is disposed after use.

To ensure that it is accessed and disposed in the same thread, you should wrap the client usage in a using statement:

using (var redis = RedisProvider.GetClient())
{
    //...
} 

If you do this whenever you need to use the RedisClient and don't share the same client instance in a different background thread, async task, parallelized code, etc you should no longer have any multi-threading issues. When you need a new client instance in a different thread you should use the same access pattern and retrieve (and dispose) a separate client instance from the pool.

mythz
  • 141,670
  • 29
  • 246
  • 390
  • Thanks by your answer mythz. Please take a look at my edit. Whilst I understand and agree with your suggestions, can I assume that singleton implementation as thread-safe whereas I put the client in the try finally blocks and ensure that I'm not sharing it across multiple threads? Is there another possible cause of the problem, apart from sharing the client in multi-threads? – Dinei Apr 06 '16 at 13:37
  • 1
    @DineiA.Rockenbach This error says it's not getting the expected reply which has always been an indication that the same client instance was used to send a new command in a different thread. I wont be able to tell where in your code base the issue is but if you can put together a small stand-alone repo (e.g. on GitHub) showing the error I can let you know what the issue is. – mythz Apr 06 '16 at 14:07
  • @mythz where should I dispose the static `IRedisClientsManager`? I don't use a DI framework at all. I was creating and disposing `IRedisClientsManager` per call (with a `using` statement) as well as the `IRedisClient`. Is it ok not using a singleton for the clients manager? – Alisson Reinaldo Silva Apr 24 '17 at 11:50
  • 1
    @Alisson it needs to be a singleton in order to pool clients, you don't need to dispose the singleton it will dispose when the AppDomain recycles. – mythz Apr 24 '17 at 13:12
  • @mythz it makes sense. Thanks for your reply, upv because your answer also solved my issue. – Alisson Reinaldo Silva Apr 24 '17 at 13:36