fix(rocketmq): 优化消费组加载/连接并修复积压、FIFO 与 Docker remap
* feat(rocketmq): 优化 RocketMQ 消费者组管理和性能监控 - 添加并行订阅组配置收集功能,支持超时控制和并发限制 - 实现消费者滞后监控功能,支持按主题过滤的滞后统计 - 重构消费滞后结果构建逻辑,添加 Broker 客户端映射功能 - 优化 Admin 客户端构建,确保请求超时参数正确传递 - 为 RabbitMQ 面板组件添加 MQ 写入保护确认机制 - 统一 MQ 面板样式表引入,清理重复按钮样式定义 - 增强连接对话框中的生产环境保护逻辑,支持 MQ 类型范围 - 更新测试用例以验证新的消费者滞后计算和客户端映射功能 - 为 RocketMQ 订阅面板添加过滤计数功能和测试覆盖 * feat(rocketmq): 优化 RocketMQ 客户端连接和消费者组管理 - 移除硬编码的 5 秒收集超时,改用连接配置的请求超时 - 添加从第一个可用主节点收集消费者组配置的方法以提高速度 - 重构内部方法访问客户端 API 实现和超时配置 - 在主题过滤模式下保留一个主节点转储以恢复 FIFO 类型准确性 - 改进消费延迟计算,失败时不设置总延迟而是保持未定义 - 为消费者运行信息获取添加 Docker 地址映射支持 - 更新命名服务器探测逻辑以更好地处理高可用配置 - 增加消息队列保活机制的查询超时支持 - 在前端界面中显示部分积压加载失败的错误信息 - 添加卸载主题前的操作确认保护 - 改进空结果集的用户界面提示信息 * fix(rocketmq): 解决消费组延迟统计失败时显示为健康零延迟的问题 - 在 examineConsumeStatsRemapped 中添加成功计数和最后错误记录 - 添加 ensureConsumeStatsProbeSucceeded 方法确保没有主节点响应时抛出异常 - 修改 TopicConsumeDetail 接口使 delay 字段可选以表示探针失败 - 更新 UI 显示逻辑在延迟不可用时显示破折号而不是零 - 添加测试用例验证所有代理失败时的异常处理 - 在 Rust 代码中检查 totalLag 是否存在以避免将缺失值视为零 - 添加消息队列适配器的标识检查防止过期的保活连接被错误清理 * fix(rocketmq): 修复RocketMQ消费组详情加载和统计查询问题 - 为examineConsumeStats调用添加超时参数以支持Docker地址映射 - 更新ensureConsumeStatsProbeSucceeded方法以处理部分失败情况下的空合并结果 - 在消费者组对话框中添加序列号防止陈旧详情/配置响应覆盖新数据 - 修复消费延迟显示逻辑,将null延迟值显示为"-" - 改进错误处理,确保仅在所有代理成功时才接受空偏移表作为有效结果 * fix(rocketmq): 修复消费者组配置收集和消息积压显示问题 - 修改 collectConsumerGroupConfigsForTopicFilter 方法以合并多个主节点的订阅配置 - 优化 Topic-filtered 列表的配置收集逻辑,确保 FIFO 组类型准确性 - 添加 totalLagFailed 标志以正确处理消息积压探测失败情况 - 更新前端组件以在探测失败时显示 "-" 而非 0 - 修复 Docker 环境下多代理地址映射冲突问题 - 添加 RFC1918 私有 IP 验证方法并改进主机可达性判断 - 为积压不可用状态添加数据模型支持 * fix(rocketmq): 修复RocketMQ消费者组配置收集中的FIFO组识别问题 - 重构订阅组配置合并逻辑,确保FIFO组不会被误识别为NORMAL组 - 添加mergeSubscriptionGroupConfigs方法处理重复和冲突的订阅组配置 - 修改collectConsumerGroupConfigsForTopicFilter方法使用新的合并策略 - 添加测试用例验证FIFO组优先级和后续组保持功能 - 修复中间代理返回重复转储时丢失FIFO-only组的问题 * refactor(rocketmq): 优化消费组统计和名称服务器连接逻辑 - 引入 MasterBrokerAddrPlan 类来管理重映射和冲突回退地址 - 将 examineConsumeStatsRemapped 重构为 examineConsumeStatsOnMasters 方法 - 实现对 Docker 环境中重映射地址的更好支持和故障转移处理 - 在连接前预先探测 NameServer 可达性以避免 JVM 启动超时 - 优化消费者组在线成员数显示,未知状态时显示为 '-' - 添加相关单元测试验证地址解析和冲突处理逻辑 * fix(rocketmq): 修复Docker重映射碰撞和消费者组配置问题 - 修复订阅组配置查找逻辑,支持FIFO合并避免顺序消息被隐藏 - 添加超时参数到订阅组查询方法 - 实现Docker重映射碰撞检测和失败关闭机制 - 添加测试验证碰撞空合并失败关闭逻辑 - 修复RocketMQ跟踪详情对话框加载序列控制问题 - 更新消费延迟标签为积压(backlog) - 添加连接缺失和只读写拒绝的国际化消息提示 - 修复CSS样式中按钮主题颜色应用问题 - 优化NameServer探测预算计算逻辑 * fix(rocketmq): 修复消费者组类型分类和成员计数显示问题 - 当订阅配置缺失时返回 UNKNOWN 类型而不是默认 NORMAL - 在删除和更新消费者组时对所有主节点执行操作确保一致性 - 成员计数查询失败时保持字段为空避免显示错误的离线状态 - 添加 UNKNOWN 类型的国际化支持和UI样式适配 - 修改测试用例验证缺失配置时的类型分类逻辑 * style(mq): 移除MQ面板中的主按钮样式覆盖 - 删除了 .mq-btn-primary 和 .btn-primary 的填充颜色样式定义 - 保留了悬停效果样式以维持用户体验一致性 - 更新注释说明MQ Chrome在浅色主题中保持页面表面样式 - 避免与工具栏和表格操作按钮产生视觉冲突 * test(composables): 为 useMqMutationGuard 添加测试并更新实现 - 添加了测试用例验证MQ面板使用的getter连接ID解析 - 将 unref 替换为 toValue 以正确处理getter参数 - 更新类型导入使用 MaybeRefOrGetter 替代 MaybeRef - 添加注释说明使用 toValue 的原因 * fix(rocketmq): 修复消费者连接检查和订阅组变更的容错机制 - 重构 examineConsumerConnectionInfoRemapped 方法以正确处理无主节点响应的情况 - 添加 requireConsumerConnectionProbeResult 方法确保连接探针结果的准确性 - 修改 mutateSubscriptionGroupOnMasters 方法实现全地址覆盖要求防止部分更新 - 新增 ensureSubscriptionGroupMutationSucceeded 和 shouldFailClosedOnCollisionPartialMutation 方法增强订阅组变更的错误处理 - 更新 rocketmq_subscription_from_group 函数默认类型为 UNKNOWN 而非 NORMAL - 添加多个单元测试验证连接探针和订阅组变更的各种异常场景 * test(rocketmq): 为消费者连接探测测试方法添加异常声明 - 在requireConsumerConnectionProbeResultFailsClosedWhenNoMasterAnswers测试方法中添加throws Exception声明 - 确保测试方法能够正确处理可能抛出的异常情况 * fix(i18n): MQ 生产确认 source 本地化并清理 fieldStatus 死键 --------- Co-authored-by: AiLi <aili@aili.com> Co-authored-by: t8y2 <t8y2@users.noreply.github.com> Co-authored-by: skyler <1156263951@qq.com>
This commit is contained in:
61 files changed
+3400
-1099
No files matched your search
File diff suppressed because it is too large.
Load diff
+332
-2
@@ -2,14 +2,18 @@ package com.dbx.agent.rocketmq;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.apache.rocketmq.client.exception.MQBrokerException;
|
||||
import org.apache.rocketmq.client.exception.MQClientException;
|
||||
import org.apache.rocketmq.common.TopicConfig;
|
||||
import org.apache.rocketmq.common.MixAll;
|
||||
import org.apache.rocketmq.common.message.Message;
|
||||
import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;
|
||||
import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats;
|
||||
import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper;
|
||||
import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable;
|
||||
import org.apache.rocketmq.remoting.protocol.admin.TopicOffset;
|
||||
import org.apache.rocketmq.remoting.protocol.body.ProducerInfo;
|
||||
@@ -23,6 +27,7 @@ import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -129,6 +134,21 @@ class RocketMqAgentTest {
|
||||
assertEquals("127.0.0.1:10911", RocketMqAgent.remapBrokerAddrForClient("172.18.0.3:10911", conn));
|
||||
assertEquals("127.0.0.1:10911", RocketMqAgent.remapBrokerAddrForClient("10.0.0.5:10911", conn));
|
||||
assertEquals("broker.example.com:10911", RocketMqAgent.remapBrokerAddrForClient("broker.example.com:10911", conn));
|
||||
// 172.15/172.32 are not RFC1918 — leave reachable public/non-private hosts alone.
|
||||
assertEquals("172.15.0.1:10911", RocketMqAgent.remapBrokerAddrForClient("172.15.0.1:10911", conn));
|
||||
assertEquals("172.32.0.1:10911", RocketMqAgent.remapBrokerAddrForClient("172.32.0.1:10911", conn));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isRfc1918PrivateIpv4MatchesOnlyPrivateRanges() {
|
||||
assertTrue(RocketMqAgent.isRfc1918PrivateIpv4("10.0.0.1"));
|
||||
assertTrue(RocketMqAgent.isRfc1918PrivateIpv4("172.16.0.1"));
|
||||
assertTrue(RocketMqAgent.isRfc1918PrivateIpv4("172.31.255.255"));
|
||||
assertTrue(RocketMqAgent.isRfc1918PrivateIpv4("192.168.1.1"));
|
||||
assertFalse(RocketMqAgent.isRfc1918PrivateIpv4("172.15.0.1"));
|
||||
assertFalse(RocketMqAgent.isRfc1918PrivateIpv4("172.32.0.1"));
|
||||
assertFalse(RocketMqAgent.isRfc1918PrivateIpv4("8.8.8.8"));
|
||||
assertFalse(RocketMqAgent.isRfc1918PrivateIpv4("broker.example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -255,6 +275,8 @@ class RocketMqAgentTest {
|
||||
SubscriptionGroupConfig normal = new SubscriptionGroupConfig();
|
||||
normal.setConsumeMessageOrderly(false);
|
||||
assertEquals("NORMAL", RocketMqAgent.classifyConsumerGroupType("MyGroup", normal));
|
||||
// Missing dump must not look like NORMAL (FIFO groups would be mislabeled).
|
||||
assertEquals("UNKNOWN", RocketMqAgent.classifyConsumerGroupType("MissingConfigGroup", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -291,13 +313,94 @@ class RocketMqAgentTest {
|
||||
|
||||
assertTrue(maxActive.get() > 1);
|
||||
for (Map<String, Object> row : rows) {
|
||||
// Successful empty connection probe → genuine offline 0.
|
||||
assertEquals(0, row.get("memberCount"));
|
||||
assertEquals(List.of(), row.get("topics"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void enrichConsumerGroupRowsReturnsDefaultsWhenBudgetExpires() {
|
||||
void requireConsumerConnectionProbeResultFailsClosedWhenNoMasterAnswers() throws Exception {
|
||||
assertThrows(
|
||||
MQClientException.class,
|
||||
() -> RocketMqAgent.requireConsumerConnectionProbeResult(
|
||||
1, 0, 0, 0, null, new IllegalStateException("timeout"), "g1")
|
||||
);
|
||||
ConsumerConnection offline = new ConsumerConnection();
|
||||
assertSame(
|
||||
offline,
|
||||
RocketMqAgent.requireConsumerConnectionProbeResult(1, 1, 0, 0, offline, null, "g1")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void requireConsumerConnectionProbeResultFailsClosedOnRemapped206WithUnreachableFallback() {
|
||||
Exception cause = new IllegalStateException("fallback unreachable");
|
||||
MQClientException err = assertThrows(
|
||||
MQClientException.class,
|
||||
() -> RocketMqAgent.requireConsumerConnectionProbeResult(
|
||||
1, 1, 1, 0, new ConsumerConnection(), cause, "g1")
|
||||
);
|
||||
assertTrue(err.getMessage().contains("Docker remap address collision"));
|
||||
assertSame(cause, err.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requireConsumerConnectionProbeResultFailsClosedOnPartialRemappedOffline() {
|
||||
Exception cause = new IllegalStateException("broker-b timeout");
|
||||
MQClientException err = assertThrows(
|
||||
MQClientException.class,
|
||||
() -> RocketMqAgent.requireConsumerConnectionProbeResult(
|
||||
2, 1, 0, 0, new ConsumerConnection(), cause, "g1")
|
||||
);
|
||||
assertTrue(err.getMessage().contains("partial remapped probe"));
|
||||
assertSame(cause, err.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requireConsumerConnectionProbeResultAllowsFullOfflineCoverage() throws Exception {
|
||||
ConsumerConnection offline = new ConsumerConnection();
|
||||
assertSame(
|
||||
offline,
|
||||
RocketMqAgent.requireConsumerConnectionProbeResult(2, 2, 0, 0, offline, null, "g1")
|
||||
);
|
||||
assertSame(
|
||||
offline,
|
||||
RocketMqAgent.requireConsumerConnectionProbeResult(1, 1, 1, 1, offline, null, "g1")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isConsumerGroupNotOnlineDetectsBrokerCode206() {
|
||||
assertTrue(RocketMqAgent.isConsumerGroupNotOnline(
|
||||
new MQBrokerException(206, "the consumer group[g1] not online")));
|
||||
assertTrue(RocketMqAgent.isConsumerGroupNotOnline(
|
||||
new MQClientException(206, "the consumer group[g1] not online")));
|
||||
assertTrue(RocketMqAgent.isConsumerGroupNotOnline(
|
||||
new RuntimeException(new MQBrokerException(206, "not online"))));
|
||||
assertFalse(RocketMqAgent.isConsumerGroupNotOnline(
|
||||
new MQBrokerException(1, "system error")));
|
||||
assertFalse(RocketMqAgent.isConsumerGroupNotOnline(new IllegalStateException("timeout")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void enrichConsumerGroupRowsOmitsMemberCountWhenConnectionProbeFails() {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("groupId", "unreachable-group");
|
||||
rows.add(row);
|
||||
|
||||
RocketMqAgent.enrichConsumerGroupRows(rows, groupId -> {
|
||||
throw new MQClientException("Failed to examine consumer connection on all masters", null);
|
||||
}, 1, 1_000);
|
||||
|
||||
// All-master connection probe failure must not look like offline 0.
|
||||
assertFalse(row.containsKey("memberCount"));
|
||||
assertEquals(List.of(), row.get("topics"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void enrichConsumerGroupRowsOmitsMemberCountWhenBudgetExpires() {
|
||||
List<Map<String, Object>> rows = IntStream.range(0, 4)
|
||||
.mapToObj(index -> {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
@@ -316,7 +419,8 @@ class RocketMqAgentTest {
|
||||
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt);
|
||||
assertTrue(elapsedMs < 1_000, "enrichment exceeded its response budget: " + elapsedMs + "ms");
|
||||
for (Map<String, Object> row : rows) {
|
||||
assertEquals(0, row.get("memberCount"));
|
||||
// Cancelled probes must not look offline (0); UI shows '-' when memberCount is absent.
|
||||
assertFalse(row.containsKey("memberCount"));
|
||||
assertEquals(List.of(), row.get("topics"));
|
||||
}
|
||||
}
|
||||
@@ -575,4 +679,230 @@ class RocketMqAgentTest {
|
||||
assertTrue(RocketMqAgent.connectionMatches(base, same));
|
||||
assertFalse(RocketMqAgent.connectionMatches(base, differentNamesrv));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildConsumerLagResultIncludesBrokerClientAndTimestamp() {
|
||||
MessageQueue mq0 = new MessageQueue("TX_TOPIC", "broker-a", 0);
|
||||
MessageQueue mq1 = new MessageQueue("TX_TOPIC", "broker-a", 1);
|
||||
OffsetWrapper offset0 = new OffsetWrapper();
|
||||
offset0.setBrokerOffset(100);
|
||||
offset0.setConsumerOffset(90);
|
||||
offset0.setLastTimestamp(1_725_000_000_000L);
|
||||
OffsetWrapper offset1 = new OffsetWrapper();
|
||||
offset1.setBrokerOffset(50);
|
||||
offset1.setConsumerOffset(50);
|
||||
offset1.setLastTimestamp(0L);
|
||||
|
||||
ConsumeStats stats = new ConsumeStats();
|
||||
stats.getOffsetTable().put(mq0, offset0);
|
||||
stats.getOffsetTable().put(mq1, offset1);
|
||||
|
||||
Map<MessageQueue, String> clients = new HashMap<>();
|
||||
clients.put(mq0, "172.18.2.212@7#1");
|
||||
|
||||
Map<String, Object> result = RocketMqAgent.buildConsumerLagResult(stats, clients);
|
||||
assertEquals(10L, result.get("totalLag"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> partitions = (List<Map<String, Object>>) result.get("partitions");
|
||||
assertEquals(2, partitions.size());
|
||||
assertEquals("broker-a", partitions.get(0).get("brokerName"));
|
||||
assertEquals(0, partitions.get(0).get("partition"));
|
||||
assertEquals(90L, partitions.get(0).get("currentOffset"));
|
||||
assertEquals(100L, partitions.get(0).get("endOffset"));
|
||||
assertEquals(10L, partitions.get(0).get("lag"));
|
||||
assertEquals(1_725_000_000_000L, partitions.get(0).get("lastTimestamp"));
|
||||
assertEquals("172.18.2.212@7#1", partitions.get(0).get("consumerClient"));
|
||||
// Missing client mapping still returns offsets with empty consumerClient.
|
||||
assertEquals("", partitions.get(1).get("consumerClient"));
|
||||
assertEquals(0L, partitions.get(1).get("lastTimestamp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildConsumerLagResultHandlesNullStatsAndEmptyClientMap() {
|
||||
Map<String, Object> empty = RocketMqAgent.buildConsumerLagResult(null, null);
|
||||
assertEquals(0L, empty.get("totalLag"));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> partitions = (List<Map<String, Object>>) empty.get("partitions");
|
||||
assertTrue(partitions.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeSubscriptionGroupConfigsPrefersFifoAndKeepsLaterOnlyGroups() {
|
||||
Map<String, SubscriptionGroupConfig> merged = new LinkedHashMap<>();
|
||||
|
||||
SubscriptionGroupConfig normalA = new SubscriptionGroupConfig();
|
||||
normalA.setGroupName("group-a");
|
||||
normalA.setConsumeMessageOrderly(false);
|
||||
|
||||
SubscriptionGroupConfig fifoA = new SubscriptionGroupConfig();
|
||||
fifoA.setGroupName("group-a");
|
||||
fifoA.setConsumeMessageOrderly(true);
|
||||
|
||||
SubscriptionGroupConfig onlyOnLater = new SubscriptionGroupConfig();
|
||||
onlyOnLater.setGroupName("group-b");
|
||||
onlyOnLater.setConsumeMessageOrderly(true);
|
||||
|
||||
// Intermediate broker repeats group-a as NORMAL (size would not grow), then later adds FIFO-only group-b.
|
||||
RocketMqAgent.mergeSubscriptionGroupConfigs(merged, Map.of("group-a", normalA));
|
||||
RocketMqAgent.mergeSubscriptionGroupConfigs(merged, Map.of("group-a", normalA));
|
||||
RocketMqAgent.mergeSubscriptionGroupConfigs(
|
||||
merged,
|
||||
Map.of("group-a", fifoA, "group-b", onlyOnLater)
|
||||
);
|
||||
|
||||
assertEquals(2, merged.size());
|
||||
assertTrue(merged.get("group-a").isConsumeMessageOrderly());
|
||||
assertTrue(merged.get("group-b").isConsumeMessageOrderly());
|
||||
assertEquals("FIFO", RocketMqAgent.classifyConsumerGroupType("group-a", merged.get("group-a")));
|
||||
assertEquals("FIFO", RocketMqAgent.classifyConsumerGroupType("group-b", merged.get("group-b")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeSubscriptionGroupConfigsDoesNotDowngradeFifoToNormal() {
|
||||
Map<String, SubscriptionGroupConfig> merged = new LinkedHashMap<>();
|
||||
SubscriptionGroupConfig fifo = new SubscriptionGroupConfig();
|
||||
fifo.setGroupName("ordered");
|
||||
fifo.setConsumeMessageOrderly(true);
|
||||
SubscriptionGroupConfig normal = new SubscriptionGroupConfig();
|
||||
normal.setGroupName("ordered");
|
||||
normal.setConsumeMessageOrderly(false);
|
||||
|
||||
RocketMqAgent.mergeSubscriptionGroupConfigs(merged, Map.of("ordered", fifo));
|
||||
RocketMqAgent.mergeSubscriptionGroupConfigs(merged, Map.of("ordered", normal));
|
||||
|
||||
assertTrue(merged.get("ordered").isConsumeMessageOrderly());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureConsumeStatsProbeSucceededRejectsAllBrokerFailures() {
|
||||
MQClientException noMasters = assertThrows(
|
||||
MQClientException.class,
|
||||
() -> RocketMqAgent.ensureConsumeStatsProbeSucceeded(0, 0, true, null, "GID_A"));
|
||||
assertTrue(noMasters.getMessage().contains("No reachable RocketMQ master"));
|
||||
|
||||
Exception cause = new RuntimeException("broker down");
|
||||
MQClientException allFailed = assertThrows(
|
||||
MQClientException.class,
|
||||
() -> RocketMqAgent.ensureConsumeStatsProbeSucceeded(2, 0, true, cause, "GID_A"));
|
||||
assertTrue(allFailed.getMessage().contains("Failed to examine consume stats"));
|
||||
assertSame(cause, allFailed.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureConsumeStatsProbeSucceededRejectsPartialFailureWithEmptyMerge() {
|
||||
Exception cause = new RuntimeException("one broker down");
|
||||
MQClientException partialEmpty = assertThrows(
|
||||
MQClientException.class,
|
||||
() -> RocketMqAgent.ensureConsumeStatsProbeSucceeded(3, 1, true, cause, "GID_A"));
|
||||
assertTrue(partialEmpty.getMessage().contains("partial failure"));
|
||||
assertSame(cause, partialEmpty.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureConsumeStatsProbeSucceededAllowsEmptyOffsetsWhenAllBrokersSucceed() throws Exception {
|
||||
// Genuine offline / unused group: every broker answered, offset table empty.
|
||||
RocketMqAgent.ensureConsumeStatsProbeSucceeded(1, 1, true, null, "GID_A");
|
||||
RocketMqAgent.ensureConsumeStatsProbeSucceeded(3, 3, true, null, "GID_A");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureConsumeStatsProbeSucceededAllowsPartialFailureWhenMergeHasOffsets() throws Exception {
|
||||
// One broker failed but another returned queue offsets — keep partial lag.
|
||||
RocketMqAgent.ensureConsumeStatsProbeSucceeded(
|
||||
3, 1, false, new RuntimeException("partial"), "GID_A");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailClosedOnCollisionEmptyWhenSiblingFallbacksUnreachable() {
|
||||
assertTrue(RocketMqAgent.shouldFailClosedOnCollisionEmpty(true, 1, 0));
|
||||
assertTrue(RocketMqAgent.shouldFailClosedOnCollisionEmpty(true, 2, 1));
|
||||
assertFalse(RocketMqAgent.shouldFailClosedOnCollisionEmpty(false, 1, 0));
|
||||
assertFalse(RocketMqAgent.shouldFailClosedOnCollisionEmpty(true, 0, 0));
|
||||
assertFalse(RocketMqAgent.shouldFailClosedOnCollisionEmpty(true, 2, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailClosedOnCollisionPartialMutationWhenSiblingFallbacksUnreachable() {
|
||||
assertTrue(RocketMqAgent.shouldFailClosedOnCollisionPartialMutation(1, 0));
|
||||
assertTrue(RocketMqAgent.shouldFailClosedOnCollisionPartialMutation(2, 1));
|
||||
assertFalse(RocketMqAgent.shouldFailClosedOnCollisionPartialMutation(0, 0));
|
||||
assertFalse(RocketMqAgent.shouldFailClosedOnCollisionPartialMutation(2, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureSubscriptionGroupMutationSucceededRejectsCollisionPartialUpdate() {
|
||||
Exception cause = new RuntimeException("fallback unreachable");
|
||||
MQClientException err = assertThrows(
|
||||
MQClientException.class,
|
||||
() -> RocketMqAgent.ensureSubscriptionGroupMutationSucceeded(
|
||||
1, 1, 1, 0, cause, "delete", "GID_A"));
|
||||
assertTrue(err.getMessage().contains("Docker remap address collision"));
|
||||
assertSame(cause, err.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureSubscriptionGroupMutationSucceededRejectsPartialRemappedUpdate() {
|
||||
Exception cause = new RuntimeException("broker-b down");
|
||||
MQClientException err = assertThrows(
|
||||
MQClientException.class,
|
||||
() -> RocketMqAgent.ensureSubscriptionGroupMutationSucceeded(
|
||||
2, 1, 0, 0, cause, "update", "GID_A"));
|
||||
assertTrue(err.getMessage().contains("partial remapped update"));
|
||||
assertSame(cause, err.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureSubscriptionGroupMutationSucceededAllowsFullCoverage() throws Exception {
|
||||
RocketMqAgent.ensureSubscriptionGroupMutationSucceeded(2, 2, 0, 0, null, "delete", "GID_A");
|
||||
RocketMqAgent.ensureSubscriptionGroupMutationSucceeded(1, 1, 1, 1, null, "update", "GID_A");
|
||||
}
|
||||
|
||||
@Test
|
||||
void masterBrokerAddrsFromClusterInfoKeepsCollisionFallbacksSeparate() {
|
||||
org.apache.rocketmq.remoting.protocol.body.ClusterInfo clusterInfo =
|
||||
new org.apache.rocketmq.remoting.protocol.body.ClusterInfo();
|
||||
HashMap<String, org.apache.rocketmq.remoting.protocol.route.BrokerData> table = new HashMap<>();
|
||||
table.put("broker-a", brokerData("broker-a", "172.18.0.2:10911"));
|
||||
table.put("broker-b", brokerData("broker-b", "172.18.0.3:10911"));
|
||||
clusterInfo.setBrokerAddrTable(table);
|
||||
|
||||
JsonObject conn = JsonParser.parseString("""
|
||||
{"namesrvAddr":"127.0.0.1:9876"}
|
||||
""").getAsJsonObject();
|
||||
RocketMqAgent.MasterBrokerAddrPlan plan =
|
||||
RocketMqAgent.masterBrokerAddrsFromClusterInfo(clusterInfo, conn, null);
|
||||
|
||||
// Both masters remap to the same published host:port; one remapped + one fallback.
|
||||
assertEquals(1, plan.remappedCount());
|
||||
assertEquals(1, plan.fallbackCount());
|
||||
assertTrue(plan.allAddrs().contains("127.0.0.1:10911"));
|
||||
assertTrue(plan.isCollisionFallback("172.18.0.3:10911") || plan.isCollisionFallback("172.18.0.2:10911"));
|
||||
// Remapped answered with offsets: collision fallbacks may stay unreachable on host agents.
|
||||
try {
|
||||
RocketMqAgent.ensureConsumeStatsProbeSucceeded(
|
||||
plan.remappedCount(), 1, false, new RuntimeException("fallback unreachable"), "GID_A");
|
||||
} catch (MQClientException e) {
|
||||
throw new AssertionError("non-empty remapped merge should be allowed", e);
|
||||
}
|
||||
// Empty merge is still allowed by the shared gate when remappedCount==success; the
|
||||
// examineConsumeStatsOnMasters collision check rejects empty+unreachable siblings.
|
||||
try {
|
||||
RocketMqAgent.ensureConsumeStatsProbeSucceeded(
|
||||
plan.remappedCount(), 1, true, null, "GID_A");
|
||||
} catch (MQClientException e) {
|
||||
throw new AssertionError("empty remapped-only success remains allowed at gate layer", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static org.apache.rocketmq.remoting.protocol.route.BrokerData brokerData(
|
||||
String name, String masterAddr) {
|
||||
org.apache.rocketmq.remoting.protocol.route.BrokerData data =
|
||||
new org.apache.rocketmq.remoting.protocol.route.BrokerData();
|
||||
data.setBrokerName(name);
|
||||
HashMap<Long, String> addrs = new HashMap<>();
|
||||
addrs.put(0L, masterAddr);
|
||||
data.setBrokerAddrs(addrs);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user