public async Task UseReplicatedHashAsyncShouldNotCrashWhenBothRedisInstancesAreFailing()
        {
            // bug: https://dev.azure.com/mseng/1ES/_workitems/edit/1656837

            // It is important to throw non-redis error to not trigger retry strategy.
            var primaryDb = new FailureInjectingRedisDatabase(SystemClock.Instance, InitialTestData)
            {
                FailingQuery = 1, ThrowRedisException = false
            };

            var primaryConnection    = MockRedisDatabaseFactory.CreateConnection(primaryDb, throwConnectionExceptionOnGet: () => true);
            var adapterConfiguration = new RedisDatabaseAdapterConfiguration(DefaultKeySpace, retryCount: 1);
            var primaryAdapter       = new RedisDatabaseAdapter(
                await RedisDatabaseFactory.CreateAsync(new EnvironmentConnectionStringProvider("TestConnectionString"), primaryConnection),
                adapterConfiguration);

            var raidedDatabaseAdapter = new RaidedRedisDatabase(new Tracer("Test"), primaryAdapter, null);
            var context = new OperationContext(new Context(Logger));

            var replicatedRedisHashKey = new ReplicatedRedisHashKey("key", new MockReplicatedKeyHost(), new MemoryClock(), raidedDatabaseAdapter);
            var error = await replicatedRedisHashKey.UseNonConcurrentReplicatedHashAsync(
                context,
                retryWindow : TimeSpan.FromMinutes(1),
                RedisOperation.All,
                (batch, key) =>
            {
                return(batch.StringGetAsync("first"));
            },
                timeout : Timeout.InfiniteTimeSpan).ShouldBeError();

            // The operation should fail gracefully, not with a critical error like contract violation.
            error.IsCriticalFailure.Should().BeFalse();
        }
        public async Task BatchIsCanceledBeforeOperationStarts()
        {
            // The test checks that if the connection is lost and the new connection is established,
            // all the pending operations are cancelled.

            var testDb  = new FailureInjectingRedisDatabase(SystemClock.Instance, InitialTestData);
            var cts     = new CancellationTokenSource();
            var context = new OperationContext(new Context(TestGlobal.Logger), cts.Token);

            // Setup Redis DB adapter
            Func <IConnectionMultiplexer> connectionMultiplexerFactory = () =>
            {
                return(MockRedisDatabaseFactory.CreateConnection(testDb, testBatch: null));
            };

            var redisDatabaseFactory = await RedisDatabaseFactory.CreateAsync(connectionMultiplexerFactory, connectionMultiplexer => BoolResult.SuccessTask);

            var adapterConfiguration = new RedisDatabaseAdapterConfiguration(DefaultKeySpace,
                                                                             // If the operation fails we'll retry once and after that we should reset the connection multiplexer so the next operation should create a new one.
                                                                             redisConnectionErrorLimit: 1,
                                                                             // No retries: should fail the operation immediately.
                                                                             retryCount: 0,
                                                                             cancelBatchWhenMultiplexerIsClosed: true);
            var dbAdapter = new RedisDatabaseAdapter(redisDatabaseFactory, adapterConfiguration);

            // Causing HashGetAllAsync operation to hang that will cause ExecuteGetCheckpointInfoAsync operation to "hang".
            var taskCompletionSource = new TaskCompletionSource <HashEntry[]>();

            testDb.HashGetAllAsyncTask = taskCompletionSource.Task;
            cts.Cancel();
            // Using timeout to avoid hangs in the tests if something is wrong with the logic.
            var result = await ExecuteGetCheckpointInfoAsync(context, dbAdapter).WithTimeoutAsync(TimeSpan.FromSeconds(1));

            result.IsCancelled.Should().BeTrue();
        }
        public async Task TheClientReconnectsWhenTheNumberOfConnectionIssuesExceedsTheLimit()
        {
            // This test checks that if the client fails to connect to redis, it'll successfully reconnect to it.

            var testDb = new FailureInjectingRedisDatabase(SystemClock.Instance, InitialTestData);

            int connectionCount = 0;

            bool failWithRedisConnectionError = false;
            // Setup Redis DB adapter
            Func <IConnectionMultiplexer> connectionMultiplexerFactory = () =>
            {
                connectionCount++;
                // Failing connection only when the local is true;
                return(MockRedisDatabaseFactory.CreateConnection(
                           testDb,
                           testBatch: null,
                           throwConnectionExceptionOnGet: () => failWithRedisConnectionError));
            };

            var redisDatabaseFactory = await RedisDatabaseFactory.CreateAsync(connectionMultiplexerFactory, connectionMultiplexer => BoolResult.SuccessTask);

            var adapterConfiguration = new RedisDatabaseAdapterConfiguration(DefaultKeySpace,
                                                                             // If the operation fails we'll retry once and after that we should reset the connection multiplexer so the next operation should create a new one.
                                                                             redisConnectionErrorLimit: 2,
                                                                             retryCount: 1);
            var dbAdapter = new RedisDatabaseAdapter(redisDatabaseFactory, adapterConfiguration);

            connectionCount.Should().Be(1);

            // The first execution should fail with the connectivity issue.
            failWithRedisConnectionError = true;
            await ExecuteBatchAsync(dbAdapter).ShouldBeError();

            failWithRedisConnectionError = false;

            // The second execution should recreate the connection.
            await ExecuteBatchAsync(dbAdapter).ShouldBeSuccess();

            connectionCount.Should().Be(2);

            // The connection was recently recreated.
            // Introducing the connectivity issue again.
            failWithRedisConnectionError = true;
            await ExecuteBatchAsync(dbAdapter).ShouldBeError();

            // The previous call set the flag to reconnect, but the actual reconnect happening on the next call.
            connectionCount.Should().Be(2);

            await ExecuteBatchAsync(dbAdapter).ShouldBeError();

            connectionCount.Should().Be(3);

            await ExecuteBatchAsync(dbAdapter).ShouldBeError();

            connectionCount.Should().Be(4);
        }
Example #4
0
        public async Task TheClientReconnectsWhenTheNumberOfConnectionIssuesExceedsTheLimit()
        {
            // This test checks that if the client fails to connect to redis, it'll successfully reconnect to it.

            // Setup test DB configured to fail 2nd query with normal Exception
            var testDb = new FailureInjectingRedisDatabase(SystemClock.Instance, InitialTestData)
            {
                // No queries will fail, instead GetDatabase will throw with RedisConnectionException.
                FailingQuery = -1,
            };

            int numberOfFactoryCalls = 0;

            // Setup Redis DB adapter
            Func <IConnectionMultiplexer> connectionMultiplexerFactory = () =>
            {
                numberOfFactoryCalls++;
                // Failing connection error only from the first instance.
                return(MockRedisDatabaseFactory.CreateConnection(testDb, testBatch: null, throwConnectionExceptionOnGet: numberOfFactoryCalls == 1));
            };

            var redisDatabaseFactory = await RedisDatabaseFactory.CreateAsync(connectionMultiplexerFactory, connectionMultiplexer => BoolResult.SuccessTask);

            var adapterConfiguration = new RedisDatabaseAdapterConfiguration(DefaultKeySpace,
                                                                             // If the operation fails we'll retry once and after that we should reset the connection multiplexer so the next operation should create a new one.
                                                                             redisConnectionErrorLimit: 2,
                                                                             retryCount: 1);
            var dbAdapter = new RedisDatabaseAdapter(redisDatabaseFactory, adapterConfiguration);

            // Create a batch query
            var redisBatch = dbAdapter.CreateBatchOperation(RedisOperation.All);

            // Execute the batch
            var result = await dbAdapter.ExecuteBatchOperationAsync(new Context(TestGlobal.Logger), redisBatch, default(CancellationToken));

            // The first execute batch should fail with the connectivity issue.
            result.ShouldBeError();
            numberOfFactoryCalls.Should().Be(1);

            var redisBatch2 = dbAdapter.CreateBatchOperation(RedisOperation.All);
            // Then we should recreate the connection and the second one should be successful.
            await dbAdapter.ExecuteBatchOperationAsync(new Context(TestGlobal.Logger), redisBatch2, default(CancellationToken)).ShouldBeSuccess();

            numberOfFactoryCalls.Should().Be(2);
        }
        public async Task BatchIsCancelledOnReconnectForOtherOperation()
        {
            // The test checks that if the connection is lost and the new connection is established,
            // all the pending operations are cancelled.

            var testDb  = new FailureInjectingRedisDatabase(SystemClock.Instance, InitialTestData);
            var context = new OperationContext(new Context(TestGlobal.Logger));

            int connectionCount = 0;

            bool failWithRedisConnectionErrorOnce = false;

            // Setup Redis DB adapter
            Func <IConnectionMultiplexer> connectionMultiplexerFactory = () =>
            {
                connectionCount++;
                // Failing connection only when the local is true;
                return(MockRedisDatabaseFactory.CreateConnection(
                           testDb,
                           testBatch: null,
                           throwConnectionExceptionOnGet: () =>
                {
                    var oldValue = failWithRedisConnectionErrorOnce;
                    failWithRedisConnectionErrorOnce = false;
                    return oldValue;
                }));
            };

            var redisDatabaseFactory = await RedisDatabaseFactory.CreateAsync(connectionMultiplexerFactory, connectionMultiplexer => BoolResult.SuccessTask);

            var adapterConfiguration = new RedisDatabaseAdapterConfiguration(DefaultKeySpace,
                                                                             // If the operation fails we'll retry once and after that we should reset the connection multiplexer so the next operation should create a new one.
                                                                             redisConnectionErrorLimit: 1,
                                                                             // No retries: should fail the operation immediately.
                                                                             retryCount: 0,
                                                                             cancelBatchWhenMultiplexerIsClosed: true);
            var dbAdapter = new RedisDatabaseAdapter(redisDatabaseFactory, adapterConfiguration);

            connectionCount.Should().Be(1);

            // Causing HashGetAllAsync operation to hang that will cause ExecuteGetCheckpointInfoAsync operation to "hang".
            var taskCompletionSource = new TaskCompletionSource <HashEntry[]>();

            testDb.HashGetAllAsyncTask = taskCompletionSource.Task;

            // Running two operations at the same time:
            // The first one should get stuck on task completion source's task
            // and the second one will fail with connectivity issue, will cause the restart of the multiplexer,
            // and will cancel all the existing operations 9including the first one).

            var task1 = ExecuteGetCheckpointInfoAsync(context, dbAdapter);

            failWithRedisConnectionErrorOnce = true;
            var task2 = ExecuteGetCheckpointInfoAsync(context, dbAdapter);

            Output.WriteLine("Waiting for the redis operations to finish.");
            await Task.WhenAll(task1, task2);

            var results    = new[] { task1.Result, task2.Result };
            var errorCount = results.Count(r => !r.Succeeded && r.ErrorMessage?.Contains("RedisConnectionException") == true);

            errorCount.Should().Be(1, $"Should have 1 error with RedisConnectionException. Results: {string.Join(Environment.NewLine, results.Select(r => r.ToString()))}");

            var cancelledCount = results.Count(r => r.IsCancelled);

            cancelledCount.Should().Be(1, $"Should have 1 cancellation. Results: {string.Join(Environment.NewLine, results.Select(r => r.ToString()))}");
        }
        public async Task DoNotReconnectTooFrequently()
        {
            var memoryClock = new MemoryClock();

            memoryClock.UtcNow = DateTime.UtcNow;

            // This test checks that if the client fails to connect to redis, it'll successfully reconnect to it.

            var testDb = new FailureInjectingRedisDatabase(SystemClock.Instance, InitialTestData);

            int      connectionCount              = 0;
            TimeSpan reconnectInterval            = TimeSpan.FromSeconds(10);
            bool     failWithRedisConnectionError = false;
            // Setup Redis DB adapter
            Func <IConnectionMultiplexer> connectionMultiplexerFactory = () =>
            {
                connectionCount++;
                // Failing connection only when the local is true;
                return(MockRedisDatabaseFactory.CreateConnection(
                           testDb,
                           testBatch: null,
                           throwConnectionExceptionOnGet: () => failWithRedisConnectionError));
            };

            var redisDatabaseFactory = await RedisDatabaseFactory.CreateAsync(connectionMultiplexerFactory, connectionMultiplexer => BoolResult.SuccessTask);

            var adapterConfiguration = new RedisDatabaseAdapterConfiguration(DefaultKeySpace,
                                                                             // If the operation fails we'll retry once and after that we should reset the connection multiplexer so the next operation should create a new one.
                                                                             redisConnectionErrorLimit: 2,
                                                                             retryCount: 1,
                                                                             minReconnectInterval: reconnectInterval);
            var dbAdapter = new RedisDatabaseAdapter(redisDatabaseFactory, adapterConfiguration, memoryClock);

            connectionCount.Should().Be(1);

            // The first execution should fail with the connectivity issue.
            failWithRedisConnectionError = true;
            await ExecuteBatchAsync(dbAdapter).ShouldBeError();

            failWithRedisConnectionError = false;

            // The second execution should recreate the connection.
            await ExecuteBatchAsync(dbAdapter).ShouldBeSuccess();

            connectionCount.Should().Be(2);

            // The connection was recently recreated.
            // Introducing the connectivity issue first.
            failWithRedisConnectionError = true;
            await ExecuteBatchAsync(dbAdapter).ShouldBeError();

            // The next call should not trigger the reconnect
            await ExecuteBatchAsync(dbAdapter).ShouldBeError();

            connectionCount.Should().Be(2);

            // Moving the clock forward and the next call should cause a reconnect.
            memoryClock.UtcNow += reconnectInterval.Multiply(2);
            await ExecuteBatchAsync(dbAdapter).ShouldBeError();

            // The previous execution should set the flag to reconnect,
            // but the reconnect count is still the same.
            connectionCount.Should().Be(2);

            // And only during the next call the multiplexer is actually recreated
            await ExecuteBatchAsync(dbAdapter).ShouldBeError();

            connectionCount.Should().Be(3);
        }