Differentiate between initial exception handling, recovery and recovery after subscription.

We now differentiate exception handling regarding the recovery state. Initial listen fails if the connection is unavailable. Upon recovery after a preceeding subscription we now log the success to create a counterpart to our error logging.

Closes: #2782
Original Pull Request: #2808
This commit is contained in:
Mark Paluch
2023-12-14 14:00:11 +01:00
committed by Christoph Strobl
parent da44aa2d2d
commit 2e29a999f8
2 changed files with 147 additions and 15 deletions

View File

@@ -19,10 +19,15 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.RedisConnection;
@@ -32,6 +37,7 @@ import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.listener.adapter.RedisListenerExecutionFailedException;
import org.springframework.util.backoff.FixedBackOff;
/**
* Unit tests for {@link RedisMessageListenerContainer}.
@@ -147,6 +153,70 @@ class RedisMessageListenerContainerUnitTests {
assertThat(container.isListening()).isFalse();
}
@Test // GH-2335
void shouldRecoverFromConnectionFailure() throws Exception {
AtomicInteger requestCount = new AtomicInteger();
AtomicBoolean shouldThrowSubscriptionException = new AtomicBoolean();
container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactoryMock);
container.setBeanName("container");
container.setTaskExecutor(new SyncTaskExecutor());
container.setSubscriptionExecutor(new SimpleAsyncTaskExecutor());
container.setMaxSubscriptionRegistrationWaitingTime(1000);
container.setRecoveryBackoff(new FixedBackOff(1, 5));
container.afterPropertiesSet();
doAnswer(it -> {
int req = requestCount.incrementAndGet();
if (req == 1 || req == 3) {
return connectionMock;
}
throw new RedisConnectionFailureException("Booh");
}).when(connectionFactoryMock).getConnection();
CountDownLatch exceptionWait = new CountDownLatch(1);
CountDownLatch armed = new CountDownLatch(1);
CountDownLatch recoveryArmed = new CountDownLatch(1);
doAnswer(it -> {
SubscriptionListener listener = it.getArgument(0);
when(connectionMock.isSubscribed()).thenReturn(true);
listener.onChannelSubscribed("a".getBytes(StandardCharsets.UTF_8), 1);
armed.countDown();
exceptionWait.await();
if (shouldThrowSubscriptionException.compareAndSet(true, false)) {
when(connectionMock.isSubscribed()).thenReturn(false);
throw new RedisConnectionFailureException("Disconnected");
}
recoveryArmed.countDown();
return null;
}).when(connectionMock).subscribe(any(), any());
container.start();
container.addMessageListener(new MessageListenerAdapter(handler), new ChannelTopic("a"));
armed.await();
// let an exception happen
shouldThrowSubscriptionException.set(true);
exceptionWait.countDown();
// wait for subscription recovery
recoveryArmed.await();
assertThat(recoveryArmed.getCount()).isZero();
}
@Test // GH-964
void failsOnDuplicateInit() {
assertThatIllegalStateException().isThrownBy(() -> container.afterPropertiesSet());