feat(mq-starter-redis-stream): 启动期 PING 探测 Redis 连通性(①② fail-fast)

This commit is contained in:
2026-07-09 10:49:23 +08:00
parent 5c4a29e1e1
commit 46b0c13462
3 changed files with 119 additions and 1 deletions

View File

@@ -0,0 +1,61 @@
package com.njcn.mq.driver.redis;
import com.njcn.mq.autoconfig.MqStartupChecks;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
/**
* ①② redis 探测mq.type=redis-stream 时,在 bean 初始化阶段主动 PING 一次 Redis。
*
* <p>lettuce 默认懒连接(启动不真正连、运行时才连);此处主动 PING连不上就 fail-fast。
*/
@Configuration
@ConditionalOnProperty(name = "mq.type", havingValue = "redis-stream")
public class RedisStreamStartupProbeAutoConfiguration implements InitializingBean {
private final RedisConnectionFactory connectionFactory;
private final Environment env;
public RedisStreamStartupProbeAutoConfiguration(RedisConnectionFactory connectionFactory, Environment env) {
this.connectionFactory = connectionFactory;
this.env = env;
}
@Override
public void afterPropertiesSet() {
if (!MqStartupChecks.enabled(env)) {
return;
}
try {
RedisConnection conn = connectionFactory.getConnection();
try {
conn.ping();
} finally {
conn.close(); // 探测用连接必须释放,避免泄漏
}
} catch (Exception e) {
throw new IllegalStateException(
"MQ 启动失败mq.type=redis-stream无法连接 Redis" + target() + ")。原因:"
+ e.getMessage()
+ "。请检查 spring.data.redis.host/port/password或确认 Redis 服务已启动。", e);
}
}
/** 尽力取 host:port 用于提示;取不到(非 Lettuce / cluster / sentinel则回退 unknown不再抛新异常。 */
private String target() {
try {
if (connectionFactory instanceof LettuceConnectionFactory) {
LettuceConnectionFactory lf = (LettuceConnectionFactory) connectionFactory;
return lf.getHostName() + ":" + lf.getPort();
}
} catch (Exception ignore) {
// 取地址失败不能影响主流程
}
return "unknown";
}
}

View File

@@ -1,2 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.njcn.mq.driver.redis.RedisStreamMqDriverAutoConfiguration
com.njcn.mq.driver.redis.RedisStreamMqDriverAutoConfiguration,\
com.njcn.mq.driver.redis.RedisStreamStartupProbeAutoConfiguration

View File

@@ -0,0 +1,56 @@
package com.njcn.mq.driver.redis;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class RedisStreamStartupProbeAutoConfigurationTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisStreamStartupProbeAutoConfiguration.class));
@Test
void pingOk_startsUp_andReleasesConnection() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection conn = mock(RedisConnection.class);
when(factory.getConnection()).thenReturn(conn);
when(conn.ping()).thenReturn("PONG");
runner.withPropertyValues("mq.type=redis-stream")
.withBean(RedisConnectionFactory.class, () -> factory)
.run(ctx -> assertThat(ctx).hasNotFailed());
verify(conn).close();
}
@Test
void pingFails_startupFails() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
when(factory.getConnection()).thenThrow(new RuntimeException("Connection refused"));
runner.withPropertyValues("mq.type=redis-stream")
.withBean(RedisConnectionFactory.class, () -> factory)
.run(ctx -> {
assertThat(ctx).hasFailed();
assertThat(ctx).getFailure().hasMessageContaining("Redis");
assertThat(ctx).getFailure().hasMessageContaining("Connection refused");
});
}
@Test
void disabled_skipsProbe() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
when(factory.getConnection()).thenThrow(new RuntimeException("should not be called"));
runner.withPropertyValues("mq.type=redis-stream", "mq.startup-check.enabled=false")
.withBean(RedisConnectionFactory.class, () -> factory)
.run(ctx -> assertThat(ctx).hasNotFailed());
}
}