From 83c517b67e6db7f6dd705893bff82cd09aaafce2 Mon Sep 17 00:00:00 2001 From: tangjie Date: Wed, 21 Jan 2026 17:42:45 +0800 Subject: [PATCH 1/9] issue #3513 --- .../java/com/alibaba/csp/sentinel/CtSph.java | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java index 88a76f750d..ee24350469 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java @@ -48,11 +48,9 @@ public class CtSph implements Sph { * Same resource({@link ResourceWrapper#equals(Object)}) will share the same * {@link ProcessorSlotChain}, no matter in which {@link Context}. */ - private static volatile Map chainMap + private static final Map CHAIN_MAP = new HashMap(); - private static final Object LOCK = new Object(); - private AsyncEntry asyncEntryWithNoChain(ResourceWrapper resourceWrapper, Context context) { AsyncEntry entry = new AsyncEntry(resourceWrapper, null, context); entry.initAsyncContext(); @@ -192,22 +190,18 @@ public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) t * @return {@link ProcessorSlotChain} of the resource */ ProcessorSlot lookProcessChain(ResourceWrapper resourceWrapper) { - ProcessorSlotChain chain = chainMap.get(resourceWrapper); + ProcessorSlotChain chain = CHAIN_MAP.get(resourceWrapper); if (chain == null) { - synchronized (LOCK) { - chain = chainMap.get(resourceWrapper); + synchronized (CHAIN_MAP) { + chain = CHAIN_MAP.get(resourceWrapper); if (chain == null) { // Entry size limit. - if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) { + if (CHAIN_MAP.size() >= Constants.MAX_SLOT_CHAIN_SIZE) { return null; } chain = SlotChainProvider.newSlotChain(); - Map newMap = new HashMap( - chainMap.size() + 1); - newMap.putAll(chainMap); - newMap.put(resourceWrapper, chain); - chainMap = newMap; + CHAIN_MAP.put(resourceWrapper, chain); } } } @@ -221,7 +215,9 @@ ProcessorSlot lookProcessChain(ResourceWrapper resourceWrapper) { * @since 0.2.0 */ public static int entrySize() { - return chainMap.size(); + synchronized (CHAIN_MAP){ + return CHAIN_MAP.size(); + } } /** @@ -230,7 +226,9 @@ public static int entrySize() { * @since 0.2.0 */ static void resetChainMap() { - chainMap.clear(); + synchronized (CHAIN_MAP){ + CHAIN_MAP.clear(); + } } /** @@ -239,7 +237,7 @@ static void resetChainMap() { * @since 0.2.0 */ static Map getChainMap() { - return chainMap; + return CHAIN_MAP; } /** From 082a9ecd115126f2ec84bcef2e74b345a3e453f9 Mon Sep 17 00:00:00 2001 From: tangjie Date: Tue, 14 Jul 2026 20:33:15 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E5=B0=86=20HashMap=20=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=20ConcurrentHashMap=EF=BC=8C=E6=98=AF=E5=9B=A0?= =?UTF-8?q?=E4=B8=BA=20lookProcessChain=20=E5=AD=98=E5=9C=A8=E9=94=81?= =?UTF-8?q?=E5=A4=96=20get=20=E4=B8=8E=E9=94=81=E5=86=85=20put=20=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E6=89=A7=E8=A1=8C=E7=9A=84=E5=9C=BA=E6=99=AF=E3=80=82?= =?UTF-8?q?=E7=8E=B0=E6=9C=89=20double-check=20=E5=8F=AF=E4=BB=A5=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=B4=E6=97=B6=E8=AF=BB=E5=8F=96=E4=B8=BA=20null?= =?UTF-8?q?=20=E7=9A=84=E6=83=85=E5=86=B5=EF=BC=8C=E4=BD=86=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E4=BF=9D=E8=AF=81=E9=9D=9E=E7=A9=BA=20ProcessorSlotCh?= =?UTF-8?q?ain=20=E5=8F=8A=E5=85=B6=E5=86=85=E9=83=A8=20slot=20=E9=93=BE?= =?UTF-8?q?=E8=A2=AB=E5=AE=89=E5=85=A8=E5=8F=91=E5=B8=83=E3=80=82Concurren?= =?UTF-8?q?tHashMap=20=E7=9A=84=E9=9D=9E=E7=A9=BA=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E4=B8=8E=E5=AF=B9=E5=BA=94=E5=86=99=E5=85=A5=E4=B9=8B=E9=97=B4?= =?UTF-8?q?=E5=85=B7=E5=A4=87=20happens-before=EF=BC=8C=E5=90=8C=E6=97=B6?= =?UTF-8?q?=20get=20=E4=B8=8D=E9=9C=80=E8=A6=81=E5=8A=A0=E4=BA=92=E6=96=A5?= =?UTF-8?q?=E9=94=81=E3=80=82=E7=8E=B0=E6=9C=89=E5=90=8C=E6=AD=A5=E5=9D=97?= =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E8=B4=9F=E8=B4=A3=E9=98=B2=E6=AD=A2=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=88=9B=E5=BB=BA=E3=80=81=E7=B2=BE=E7=A1=AE=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=206000=20=E5=AE=B9=E9=87=8F=E4=B8=8A=E9=99=90?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E4=BF=9D=E6=8A=A4=20SlotChainProvider.newSlo?= =?UTF-8?q?tChain()=E3=80=82=E8=BF=99=E6=A0=B7=E6=97=A2=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=8E=9F=20Copy-on-Write=20=E6=AF=8F=E6=AC=A1=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=B5=84=E6=BA=90=E9=83=BD=E5=A4=8D=E5=88=B6=E6=95=B4?= =?UTF-8?q?=E5=BC=A0=20Map=20=E7=9A=84=20CPU=20=E5=92=8C=20GC=20=E5=BC=80?= =?UTF-8?q?=E9=94=80=EF=BC=8C=E4=B9=9F=E4=BF=9D=E7=95=99=E4=BA=86=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E6=AD=A3=E7=A1=AE=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/alibaba/csp/sentinel/CtSph.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java index ee24350469..eb144aa33a 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java @@ -16,8 +16,8 @@ package com.alibaba.csp.sentinel; import java.lang.reflect.Method; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.context.Context; @@ -49,7 +49,7 @@ public class CtSph implements Sph { * {@link ProcessorSlotChain}, no matter in which {@link Context}. */ private static final Map CHAIN_MAP - = new HashMap(); + = new ConcurrentHashMap(); private AsyncEntry asyncEntryWithNoChain(ResourceWrapper resourceWrapper, Context context) { AsyncEntry entry = new AsyncEntry(resourceWrapper, null, context); From a11fff8983531e83133443ab31b6198e7fe49ea5 Mon Sep 17 00:00:00 2001 From: tangjie Date: Wed, 15 Jul 2026 11:31:53 +0800 Subject: [PATCH 3/9] docs: add CtSph concurrency test design --- ...26-07-15-ctsph-concurrency-tests-design.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-15-ctsph-concurrency-tests-design.md diff --git a/docs/superpowers/specs/2026-07-15-ctsph-concurrency-tests-design.md b/docs/superpowers/specs/2026-07-15-ctsph-concurrency-tests-design.md new file mode 100644 index 0000000000..95c2ddf0ed --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-ctsph-concurrency-tests-design.md @@ -0,0 +1,57 @@ +# CtSph 并发回归测试设计 + +## 背景 + +PR #3592 将 `CtSph` 的 slot chain 缓存从 Copy-on-Write `HashMap` 改为 `ConcurrentHashMap` 原地写入。生产实现仍通过外层同步块串行化首次创建,以保证相同资源只生成一个 `ProcessorSlotChain`、资源总数不超过 `Constants.MAX_SLOT_CHAIN_SIZE`,并保护 `SlotChainProvider.newSlotChain()` 的非线程安全初始化过程。 + +现有 `CtSphTest` 只验证串行查找和容量限制,没有覆盖多线程同时进入 `lookProcessChain` 时必须保持的行为。 + +## 范围 + +本次只修改以下文件: + +- `sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java` +- `sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java` 中两处既有格式问题 + +不新增 JMH 性能基准,不调整生产并发模型,不修改公开 API。 + +## 测试方案 + +### 并发缓存类型契约 + +验证测试可见的缓存实现满足 `ConcurrentMap` 契约,而不是绑定到某一个具体实现类。这个断言直接保护锁外读取所依赖的安全发布机制,并用于执行回归测试的红绿验证。 + +### 同一资源并发查找 + +创建固定大小线程池,让多个任务通过同一个 `CountDownLatch` 同时调用 `lookProcessChain`。收集所有 `Future` 返回值并验证: + +- 所有结果都非空; +- 所有结果都是同一个 `ProcessorSlotChain` 实例; +- `CtSph.entrySize()` 最终等于 1。 + +该用例保护“同资源全局共享同一条 slot chain”的契约。 + +### 容量边界并发创建 + +先将缓存填充到 `MAX_SLOT_CHAIN_SIZE - 1`,然后让多个线程同时用不同资源调用 `lookProcessChain`。验证: + +- 只有一个新资源能够获得非空 chain; +- 其余资源返回 `null`; +- `CtSph.entrySize()` 最终严格等于 `MAX_SLOT_CHAIN_SIZE`,不能越界。 + +该用例保护容量检查与插入必须在同一临界区完成的契约,同时避免实际并发创建 6000 条完整 slot chain,缩短测试时间。 + +## 并发测试稳定性 + +- 使用固定线程池,避免无界创建线程; +- 使用起跑锁存器扩大并发重叠窗口; +- 所有 `Future#get` 都设置超时,失败时不会永久挂住构建; +- 在 `finally` 中关闭线程池; +- 继续依赖现有 `setUp` 和 `tearDown` 清理静态缓存,保证用例隔离。 + +## 验证方式 + +1. 先运行并发缓存类型契约测试。 +2. 临时将缓存退回普通 `HashMap`,确认该测试能够拒绝非并发 Map;随后恢复 `ConcurrentHashMap`。 +3. 运行完整 `CtSphTest`。 +4. 运行 `sentinel-core` 测试和 Java 8 目标级别编译检查。 From 7cc7b44f5250eb5945489718f86ef606ff189bcc Mon Sep 17 00:00:00 2001 From: tangjie Date: Wed, 15 Jul 2026 14:50:44 +0800 Subject: [PATCH 4/9] docs: add CtSph concurrency test plan --- ...2026-07-15-ctsph-concurrency-tests-plan.md | 331 ++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-15-ctsph-concurrency-tests-plan.md diff --git a/docs/superpowers/plans/2026-07-15-ctsph-concurrency-tests-plan.md b/docs/superpowers/plans/2026-07-15-ctsph-concurrency-tests-plan.md new file mode 100644 index 0000000000..92d71bfe5d --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-ctsph-concurrency-tests-plan.md @@ -0,0 +1,331 @@ +# CtSph 并发回归测试实施计划 + +> **供智能代理执行:** 必须使用 `superpowers:executing-plans`,按任务逐项实施并在检查点复核。 + +**目标:** 为 `CtSph.lookProcessChain` 补充确定性的并发缓存、同资源唯一性和容量上限回归测试。 + +**架构:** 测试继续放在现有 `CtSphTest` 中,通过固定线程池、就绪锁存器和起跑锁存器让任务形成真实并发。生产并发模型保持不变,只修复两处已有格式问题;每个测试通过临时错误变体完成红测,随后恢复当前正确实现完成绿测。 + +**技术栈:** Java 8、JUnit 4、`java.util.concurrent`、Maven Surefire。 + +## 全局约束 + +- 只修改 `CtSphTest.java` 和 `CtSph.java` 的两处格式。 +- 不新增 JMH benchmark,不新增依赖,不修改公开 API。 +- 所有并发等待必须设置超时,线程池必须在 `finally` 中关闭。 +- 临时错误变体只能用于红测,验证后必须恢复,不能进入最终差异。 + +--- + +### 任务一:并发缓存类型契约 + +**文件:** + +- 修改:`sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java` +- 临时变体:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java` + +**接口:** + +- 使用:`CtSph.getChainMap()` +- 产出:测试方法 `testChainMapSupportsConcurrentAccess()` + +- [ ] **步骤 1:添加并发 Map 契约测试** + +在测试文件中引入: + +```java +import java.util.concurrent.ConcurrentMap; +``` + +添加测试: + +```java +@Test +public void testChainMapSupportsConcurrentAccess() { + assertTrue("Chain map should support lock-free concurrent reads", + CtSph.getChainMap() instanceof ConcurrentMap); +} +``` + +- [ ] **步骤 2:验证红测** + +临时将 `CtSph` 的缓存实例改为普通 `HashMap`,运行: + +```bash +mvn -pl sentinel-core -am -Dtest=CtSphTest#testChainMapSupportsConcurrentAccess \ + -Dsurefire.failIfNoSpecifiedTests=false test +``` + +预期:断言失败,提示缓存不支持锁外并发读取。 + +- [ ] **步骤 3:恢复 `ConcurrentHashMap` 并验证绿测** + +恢复当前生产实现后运行相同命令。 + +预期:1 个测试通过,0 个失败。 + +- [ ] **步骤 4:提交并发 Map 契约测试** + +```bash +git add sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java +git commit -m "test: verify CtSph uses concurrent chain map" +``` + +--- + +### 任务二:并发执行辅助方法与同资源唯一性 + +**文件:** + +- 修改:`sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java` +- 临时变体:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java` + +**接口:** + +- 产出:`private List invokeConcurrently(List> tasks) throws Exception` +- 产出:测试方法 `testLookUpSameSlotChainConcurrently()` + +- [ ] **步骤 1:添加并发执行辅助方法** + +增加以下 Java 标准库导入: + +```java +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +``` + +添加辅助方法: + +```java +private List invokeConcurrently(List> tasks) throws Exception { + final int taskCount = tasks.size(); + final ExecutorService executor = Executors.newFixedThreadPool(taskCount); + final CountDownLatch ready = new CountDownLatch(taskCount); + final CountDownLatch start = new CountDownLatch(1); + final List> futures = new ArrayList>(taskCount); + + try { + for (final Callable task : tasks) { + futures.add(executor.submit(new Callable() { + @Override + public T call() throws Exception { + ready.countDown(); + if (!start.await(5, TimeUnit.SECONDS)) { + throw new TimeoutException("Timed out waiting for concurrent test start"); + } + return task.call(); + } + })); + } + + assertTrue("Concurrent tasks were not ready in time", ready.await(5, TimeUnit.SECONDS)); + start.countDown(); + + List results = new ArrayList(taskCount); + for (Future future : futures) { + results.add(future.get(10, TimeUnit.SECONDS)); + } + return results; + } finally { + start.countDown(); + executor.shutdownNow(); + assertTrue("Executor did not terminate in time", executor.awaitTermination(5, TimeUnit.SECONDS)); + } +} +``` + +- [ ] **步骤 2:添加同资源并发测试** + +```java +@Test +public void testLookUpSameSlotChainConcurrently() throws Exception { + final int taskCount = 32; + final ResourceWrapper resource = new StringResourceWrapper("concurrent-resource", EntryType.IN); + List>> tasks = + new ArrayList>>(taskCount); + for (int i = 0; i < taskCount; i++) { + tasks.add(new Callable>() { + @Override + public ProcessorSlot call() { + return ctSph.lookProcessChain(resource); + } + }); + } + + List> chains = invokeConcurrently(tasks); + ProcessorSlot expected = chains.get(0); + assertNotNull(expected); + for (ProcessorSlot chain : chains) { + assertSame("Same resource should share one slot chain", expected, chain); + } + assertEquals(1, CtSph.entrySize()); +} +``` + +- [ ] **步骤 3:验证红测** + +临时把 `lookProcessChain` 改成每次调用都创建并返回新 chain 的错误变体,运行: + +```bash +mvn -pl sentinel-core -am -Dtest=CtSphTest#testLookUpSameSlotChainConcurrently \ + -Dsurefire.failIfNoSpecifiedTests=false test +``` + +预期:`assertSame` 失败。 + +- [ ] **步骤 4:恢复生产实现并验证绿测** + +恢复双重检查和 `ConcurrentHashMap` 写入后运行相同命令。 + +预期:1 个测试通过,0 个失败。 + +- [ ] **步骤 5:提交同资源并发测试** + +```bash +git add sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java +git commit -m "test: cover concurrent CtSph chain lookup" +``` + +--- + +### 任务三:容量边界并发创建 + +**文件:** + +- 修改:`sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java` +- 临时变体:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java` + +**接口:** + +- 产出:`private void fillResources(int count)` +- 产出:测试方法 `testLookUpSlotChainAtCapacityConcurrently()` + +- [ ] **步骤 1:提取指定数量的缓存填充方法** + +```java +private void fillFullResources() { + fillResources(Constants.MAX_SLOT_CHAIN_SIZE); +} + +private void fillResources(int count) { + for (int i = 0; i < count; i++) { + ResourceWrapper resourceWrapper = new StringResourceWrapper("test-resource-" + i, EntryType.IN); + CtSph.getChainMap().put(resourceWrapper, SlotChainProvider.newSlotChain()); + } +} +``` + +- [ ] **步骤 2:添加容量边界并发测试** + +```java +@Test +public void testLookUpSlotChainAtCapacityConcurrently() throws Exception { + fillResources(Constants.MAX_SLOT_CHAIN_SIZE - 1); + final int taskCount = 16; + List>> tasks = + new ArrayList>>(taskCount); + for (int i = 0; i < taskCount; i++) { + final ResourceWrapper resource = + new StringResourceWrapper("concurrent-capacity-resource-" + i, EntryType.IN); + tasks.add(new Callable>() { + @Override + public ProcessorSlot call() { + return ctSph.lookProcessChain(resource); + } + }); + } + + int createdCount = 0; + for (ProcessorSlot chain : invokeConcurrently(tasks)) { + if (chain != null) { + createdCount++; + } + } + assertEquals("Only one resource should be created at the capacity boundary", 1, createdCount); + assertEquals(Constants.MAX_SLOT_CHAIN_SIZE, CtSph.entrySize()); +} +``` + +- [ ] **步骤 3:验证红测** + +临时将容量判断从 `>=` 改为 `>`,运行: + +```bash +mvn -pl sentinel-core -am -Dtest=CtSphTest#testLookUpSlotChainAtCapacityConcurrently \ + -Dsurefire.failIfNoSpecifiedTests=false test +``` + +预期:创建数量为 2 且缓存数量达到 6001,断言失败。 + +- [ ] **步骤 4:恢复 `>=` 并验证绿测** + +恢复正确判断后运行相同命令。 + +预期:1 个测试通过,0 个失败。 + +- [ ] **步骤 5:提交容量边界并发测试** + +```bash +git add sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java +git commit -m "test: cover concurrent CtSph capacity limit" +``` + +--- + +### 任务四:格式整理与完整验证 + +**文件:** + +- 修改:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java:218` +- 修改:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java:229` + +**接口:** 无公开接口变化。 + +- [ ] **步骤 1:修复同步块格式** + +```java +synchronized (CHAIN_MAP) { +``` + +- [ ] **步骤 2:运行完整 `CtSphTest`** + +```bash +mvn -pl sentinel-core -am -Dtest=CtSphTest \ + -Dsurefire.failIfNoSpecifiedTests=false test +``` + +预期:全部测试通过,0 个失败。 + +- [ ] **步骤 3:运行 `sentinel-core` 完整测试** + +```bash +mvn -pl sentinel-core -am test +``` + +预期:构建成功,0 个测试失败。 + +- [ ] **步骤 4:运行 Java 8 编译与差异检查** + +```bash +javac --release 8 -cp sentinel-core/target/classes \ + -d /tmp/sentinel-review-final-classes \ + sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java +git diff --check +git status --short +``` + +预期:Java 编译退出码为 0,差异检查无错误,只包含设计、计划、测试和两处格式变更。 + +- [ ] **步骤 5:提交格式整理** + +```bash +git add sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java +git commit -m "style: fix CtSph synchronized block formatting" +``` From acc7e612d6ad51e6e2cae6de83eb24ce079b719e Mon Sep 17 00:00:00 2001 From: tangjie Date: Wed, 15 Jul 2026 14:52:44 +0800 Subject: [PATCH 5/9] test: verify CtSph uses concurrent chain map --- .../test/java/com/alibaba/csp/sentinel/CtSphTest.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java index 1d910ee76a..1bce718d45 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java @@ -1,5 +1,7 @@ package com.alibaba.csp.sentinel; +import java.util.concurrent.ConcurrentMap; + import com.alibaba.csp.sentinel.context.Context; import com.alibaba.csp.sentinel.context.ContextTestUtil; import com.alibaba.csp.sentinel.context.ContextUtil; @@ -269,6 +271,12 @@ public void testLookUpSlotChain() { assertNull(ctSph.lookProcessChain(r2)); } + @Test + public void testChainMapSupportsConcurrentAccess() { + assertTrue("Chain map should support lock-free concurrent reads", + CtSph.getChainMap() instanceof ConcurrentMap); + } + private void fillFullContext() { for (int i = 0; i < Constants.MAX_CONTEXT_NAME_SIZE; i++) { ContextUtil.enter("test-context-" + i); @@ -362,4 +370,4 @@ public void tearDown() throws Exception { ContextTestUtil.resetContextMap(); CtSph.resetChainMap(); } -} \ No newline at end of file +} From 62c6e5f8f19c9758e7d9e8fc418e1c88e81fcf59 Mon Sep 17 00:00:00 2001 From: tangjie Date: Wed, 15 Jul 2026 14:53:58 +0800 Subject: [PATCH 6/9] test: cover concurrent CtSph chain lookup --- .../com/alibaba/csp/sentinel/CtSphTest.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java index 1bce718d45..dd34b93db0 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java @@ -1,6 +1,15 @@ package com.alibaba.csp.sentinel; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import com.alibaba.csp.sentinel.context.Context; import com.alibaba.csp.sentinel.context.ContextTestUtil; @@ -277,6 +286,66 @@ public void testChainMapSupportsConcurrentAccess() { CtSph.getChainMap() instanceof ConcurrentMap); } + @Test + public void testLookUpSameSlotChainConcurrently() throws Exception { + final int taskCount = 32; + final ResourceWrapper resource = new StringResourceWrapper("concurrent-resource", EntryType.IN); + List>> tasks = + new ArrayList>>(taskCount); + for (int i = 0; i < taskCount; i++) { + tasks.add(new Callable>() { + @Override + public ProcessorSlot call() { + return ctSph.lookProcessChain(resource); + } + }); + } + + List> chains = invokeConcurrently(tasks); + ProcessorSlot expected = chains.get(0); + assertNotNull(expected); + for (ProcessorSlot chain : chains) { + assertSame("Same resource should share one slot chain", expected, chain); + } + assertEquals(1, CtSph.entrySize()); + } + + private List invokeConcurrently(List> tasks) throws Exception { + final int taskCount = tasks.size(); + final ExecutorService executor = Executors.newFixedThreadPool(taskCount); + final CountDownLatch ready = new CountDownLatch(taskCount); + final CountDownLatch start = new CountDownLatch(1); + final List> futures = new ArrayList>(taskCount); + + try { + for (final Callable task : tasks) { + futures.add(executor.submit(new Callable() { + @Override + public T call() throws Exception { + ready.countDown(); + if (!start.await(5, TimeUnit.SECONDS)) { + throw new TimeoutException("Timed out waiting for concurrent test start"); + } + return task.call(); + } + })); + } + + assertTrue("Concurrent tasks were not ready in time", ready.await(5, TimeUnit.SECONDS)); + start.countDown(); + + List results = new ArrayList(taskCount); + for (Future future : futures) { + results.add(future.get(10, TimeUnit.SECONDS)); + } + return results; + } finally { + start.countDown(); + executor.shutdownNow(); + assertTrue("Executor did not terminate in time", executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + private void fillFullContext() { for (int i = 0; i < Constants.MAX_CONTEXT_NAME_SIZE; i++) { ContextUtil.enter("test-context-" + i); From d3c9780448964305fb13660814eaa17a026a2d79 Mon Sep 17 00:00:00 2001 From: tangjie Date: Wed, 15 Jul 2026 14:55:02 +0800 Subject: [PATCH 7/9] test: cover concurrent CtSph capacity limit --- .../com/alibaba/csp/sentinel/CtSphTest.java | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java index dd34b93db0..f88cb87ef8 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java @@ -310,6 +310,33 @@ public ProcessorSlot call() { assertEquals(1, CtSph.entrySize()); } + @Test + public void testLookUpSlotChainAtCapacityConcurrently() throws Exception { + fillResources(Constants.MAX_SLOT_CHAIN_SIZE - 1); + final int taskCount = 16; + List>> tasks = + new ArrayList>>(taskCount); + for (int i = 0; i < taskCount; i++) { + final ResourceWrapper resource = + new StringResourceWrapper("concurrent-capacity-resource-" + i, EntryType.IN); + tasks.add(new Callable>() { + @Override + public ProcessorSlot call() { + return ctSph.lookProcessChain(resource); + } + }); + } + + int createdCount = 0; + for (ProcessorSlot chain : invokeConcurrently(tasks)) { + if (chain != null) { + createdCount++; + } + } + assertEquals("Only one resource should be created at the capacity boundary", 1, createdCount); + assertEquals(Constants.MAX_SLOT_CHAIN_SIZE, CtSph.entrySize()); + } + private List invokeConcurrently(List> tasks) throws Exception { final int taskCount = tasks.size(); final ExecutorService executor = Executors.newFixedThreadPool(taskCount); @@ -354,7 +381,11 @@ private void fillFullContext() { } private void fillFullResources() { - for (int i = 0; i < Constants.MAX_SLOT_CHAIN_SIZE; i++) { + fillResources(Constants.MAX_SLOT_CHAIN_SIZE); + } + + private void fillResources(int count) { + for (int i = 0; i < count; i++) { ResourceWrapper resourceWrapper = new StringResourceWrapper("test-resource-" + i, EntryType.IN); CtSph.getChainMap().put(resourceWrapper, SlotChainProvider.newSlotChain()); } From 09eee6980c5e4eaa33d69b78a3b2e2e44276ce59 Mon Sep 17 00:00:00 2001 From: tangjie Date: Wed, 15 Jul 2026 15:01:23 +0800 Subject: [PATCH 8/9] style: fix CtSph synchronized block formatting --- .../src/main/java/com/alibaba/csp/sentinel/CtSph.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java index eb144aa33a..c756c349a3 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java @@ -215,7 +215,7 @@ ProcessorSlot lookProcessChain(ResourceWrapper resourceWrapper) { * @since 0.2.0 */ public static int entrySize() { - synchronized (CHAIN_MAP){ + synchronized (CHAIN_MAP) { return CHAIN_MAP.size(); } } @@ -226,7 +226,7 @@ public static int entrySize() { * @since 0.2.0 */ static void resetChainMap() { - synchronized (CHAIN_MAP){ + synchronized (CHAIN_MAP) { CHAIN_MAP.clear(); } } From a885c6e469bc72110bd85d22f161a2b719800faa Mon Sep 17 00:00:00 2001 From: tangjie Date: Wed, 15 Jul 2026 15:10:49 +0800 Subject: [PATCH 9/9] test --- ...2026-07-15-ctsph-concurrency-tests-plan.md | 331 ------------------ ...26-07-15-ctsph-concurrency-tests-design.md | 57 --- 2 files changed, 388 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-15-ctsph-concurrency-tests-plan.md delete mode 100644 docs/superpowers/specs/2026-07-15-ctsph-concurrency-tests-design.md diff --git a/docs/superpowers/plans/2026-07-15-ctsph-concurrency-tests-plan.md b/docs/superpowers/plans/2026-07-15-ctsph-concurrency-tests-plan.md deleted file mode 100644 index 92d71bfe5d..0000000000 --- a/docs/superpowers/plans/2026-07-15-ctsph-concurrency-tests-plan.md +++ /dev/null @@ -1,331 +0,0 @@ -# CtSph 并发回归测试实施计划 - -> **供智能代理执行:** 必须使用 `superpowers:executing-plans`,按任务逐项实施并在检查点复核。 - -**目标:** 为 `CtSph.lookProcessChain` 补充确定性的并发缓存、同资源唯一性和容量上限回归测试。 - -**架构:** 测试继续放在现有 `CtSphTest` 中,通过固定线程池、就绪锁存器和起跑锁存器让任务形成真实并发。生产并发模型保持不变,只修复两处已有格式问题;每个测试通过临时错误变体完成红测,随后恢复当前正确实现完成绿测。 - -**技术栈:** Java 8、JUnit 4、`java.util.concurrent`、Maven Surefire。 - -## 全局约束 - -- 只修改 `CtSphTest.java` 和 `CtSph.java` 的两处格式。 -- 不新增 JMH benchmark,不新增依赖,不修改公开 API。 -- 所有并发等待必须设置超时,线程池必须在 `finally` 中关闭。 -- 临时错误变体只能用于红测,验证后必须恢复,不能进入最终差异。 - ---- - -### 任务一:并发缓存类型契约 - -**文件:** - -- 修改:`sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java` -- 临时变体:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java` - -**接口:** - -- 使用:`CtSph.getChainMap()` -- 产出:测试方法 `testChainMapSupportsConcurrentAccess()` - -- [ ] **步骤 1:添加并发 Map 契约测试** - -在测试文件中引入: - -```java -import java.util.concurrent.ConcurrentMap; -``` - -添加测试: - -```java -@Test -public void testChainMapSupportsConcurrentAccess() { - assertTrue("Chain map should support lock-free concurrent reads", - CtSph.getChainMap() instanceof ConcurrentMap); -} -``` - -- [ ] **步骤 2:验证红测** - -临时将 `CtSph` 的缓存实例改为普通 `HashMap`,运行: - -```bash -mvn -pl sentinel-core -am -Dtest=CtSphTest#testChainMapSupportsConcurrentAccess \ - -Dsurefire.failIfNoSpecifiedTests=false test -``` - -预期:断言失败,提示缓存不支持锁外并发读取。 - -- [ ] **步骤 3:恢复 `ConcurrentHashMap` 并验证绿测** - -恢复当前生产实现后运行相同命令。 - -预期:1 个测试通过,0 个失败。 - -- [ ] **步骤 4:提交并发 Map 契约测试** - -```bash -git add sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java -git commit -m "test: verify CtSph uses concurrent chain map" -``` - ---- - -### 任务二:并发执行辅助方法与同资源唯一性 - -**文件:** - -- 修改:`sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java` -- 临时变体:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java` - -**接口:** - -- 产出:`private List invokeConcurrently(List> tasks) throws Exception` -- 产出:测试方法 `testLookUpSameSlotChainConcurrently()` - -- [ ] **步骤 1:添加并发执行辅助方法** - -增加以下 Java 标准库导入: - -```java -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -``` - -添加辅助方法: - -```java -private List invokeConcurrently(List> tasks) throws Exception { - final int taskCount = tasks.size(); - final ExecutorService executor = Executors.newFixedThreadPool(taskCount); - final CountDownLatch ready = new CountDownLatch(taskCount); - final CountDownLatch start = new CountDownLatch(1); - final List> futures = new ArrayList>(taskCount); - - try { - for (final Callable task : tasks) { - futures.add(executor.submit(new Callable() { - @Override - public T call() throws Exception { - ready.countDown(); - if (!start.await(5, TimeUnit.SECONDS)) { - throw new TimeoutException("Timed out waiting for concurrent test start"); - } - return task.call(); - } - })); - } - - assertTrue("Concurrent tasks were not ready in time", ready.await(5, TimeUnit.SECONDS)); - start.countDown(); - - List results = new ArrayList(taskCount); - for (Future future : futures) { - results.add(future.get(10, TimeUnit.SECONDS)); - } - return results; - } finally { - start.countDown(); - executor.shutdownNow(); - assertTrue("Executor did not terminate in time", executor.awaitTermination(5, TimeUnit.SECONDS)); - } -} -``` - -- [ ] **步骤 2:添加同资源并发测试** - -```java -@Test -public void testLookUpSameSlotChainConcurrently() throws Exception { - final int taskCount = 32; - final ResourceWrapper resource = new StringResourceWrapper("concurrent-resource", EntryType.IN); - List>> tasks = - new ArrayList>>(taskCount); - for (int i = 0; i < taskCount; i++) { - tasks.add(new Callable>() { - @Override - public ProcessorSlot call() { - return ctSph.lookProcessChain(resource); - } - }); - } - - List> chains = invokeConcurrently(tasks); - ProcessorSlot expected = chains.get(0); - assertNotNull(expected); - for (ProcessorSlot chain : chains) { - assertSame("Same resource should share one slot chain", expected, chain); - } - assertEquals(1, CtSph.entrySize()); -} -``` - -- [ ] **步骤 3:验证红测** - -临时把 `lookProcessChain` 改成每次调用都创建并返回新 chain 的错误变体,运行: - -```bash -mvn -pl sentinel-core -am -Dtest=CtSphTest#testLookUpSameSlotChainConcurrently \ - -Dsurefire.failIfNoSpecifiedTests=false test -``` - -预期:`assertSame` 失败。 - -- [ ] **步骤 4:恢复生产实现并验证绿测** - -恢复双重检查和 `ConcurrentHashMap` 写入后运行相同命令。 - -预期:1 个测试通过,0 个失败。 - -- [ ] **步骤 5:提交同资源并发测试** - -```bash -git add sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java -git commit -m "test: cover concurrent CtSph chain lookup" -``` - ---- - -### 任务三:容量边界并发创建 - -**文件:** - -- 修改:`sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java` -- 临时变体:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java` - -**接口:** - -- 产出:`private void fillResources(int count)` -- 产出:测试方法 `testLookUpSlotChainAtCapacityConcurrently()` - -- [ ] **步骤 1:提取指定数量的缓存填充方法** - -```java -private void fillFullResources() { - fillResources(Constants.MAX_SLOT_CHAIN_SIZE); -} - -private void fillResources(int count) { - for (int i = 0; i < count; i++) { - ResourceWrapper resourceWrapper = new StringResourceWrapper("test-resource-" + i, EntryType.IN); - CtSph.getChainMap().put(resourceWrapper, SlotChainProvider.newSlotChain()); - } -} -``` - -- [ ] **步骤 2:添加容量边界并发测试** - -```java -@Test -public void testLookUpSlotChainAtCapacityConcurrently() throws Exception { - fillResources(Constants.MAX_SLOT_CHAIN_SIZE - 1); - final int taskCount = 16; - List>> tasks = - new ArrayList>>(taskCount); - for (int i = 0; i < taskCount; i++) { - final ResourceWrapper resource = - new StringResourceWrapper("concurrent-capacity-resource-" + i, EntryType.IN); - tasks.add(new Callable>() { - @Override - public ProcessorSlot call() { - return ctSph.lookProcessChain(resource); - } - }); - } - - int createdCount = 0; - for (ProcessorSlot chain : invokeConcurrently(tasks)) { - if (chain != null) { - createdCount++; - } - } - assertEquals("Only one resource should be created at the capacity boundary", 1, createdCount); - assertEquals(Constants.MAX_SLOT_CHAIN_SIZE, CtSph.entrySize()); -} -``` - -- [ ] **步骤 3:验证红测** - -临时将容量判断从 `>=` 改为 `>`,运行: - -```bash -mvn -pl sentinel-core -am -Dtest=CtSphTest#testLookUpSlotChainAtCapacityConcurrently \ - -Dsurefire.failIfNoSpecifiedTests=false test -``` - -预期:创建数量为 2 且缓存数量达到 6001,断言失败。 - -- [ ] **步骤 4:恢复 `>=` 并验证绿测** - -恢复正确判断后运行相同命令。 - -预期:1 个测试通过,0 个失败。 - -- [ ] **步骤 5:提交容量边界并发测试** - -```bash -git add sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java -git commit -m "test: cover concurrent CtSph capacity limit" -``` - ---- - -### 任务四:格式整理与完整验证 - -**文件:** - -- 修改:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java:218` -- 修改:`sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java:229` - -**接口:** 无公开接口变化。 - -- [ ] **步骤 1:修复同步块格式** - -```java -synchronized (CHAIN_MAP) { -``` - -- [ ] **步骤 2:运行完整 `CtSphTest`** - -```bash -mvn -pl sentinel-core -am -Dtest=CtSphTest \ - -Dsurefire.failIfNoSpecifiedTests=false test -``` - -预期:全部测试通过,0 个失败。 - -- [ ] **步骤 3:运行 `sentinel-core` 完整测试** - -```bash -mvn -pl sentinel-core -am test -``` - -预期:构建成功,0 个测试失败。 - -- [ ] **步骤 4:运行 Java 8 编译与差异检查** - -```bash -javac --release 8 -cp sentinel-core/target/classes \ - -d /tmp/sentinel-review-final-classes \ - sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java -git diff --check -git status --short -``` - -预期:Java 编译退出码为 0,差异检查无错误,只包含设计、计划、测试和两处格式变更。 - -- [ ] **步骤 5:提交格式整理** - -```bash -git add sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java -git commit -m "style: fix CtSph synchronized block formatting" -``` diff --git a/docs/superpowers/specs/2026-07-15-ctsph-concurrency-tests-design.md b/docs/superpowers/specs/2026-07-15-ctsph-concurrency-tests-design.md deleted file mode 100644 index 95c2ddf0ed..0000000000 --- a/docs/superpowers/specs/2026-07-15-ctsph-concurrency-tests-design.md +++ /dev/null @@ -1,57 +0,0 @@ -# CtSph 并发回归测试设计 - -## 背景 - -PR #3592 将 `CtSph` 的 slot chain 缓存从 Copy-on-Write `HashMap` 改为 `ConcurrentHashMap` 原地写入。生产实现仍通过外层同步块串行化首次创建,以保证相同资源只生成一个 `ProcessorSlotChain`、资源总数不超过 `Constants.MAX_SLOT_CHAIN_SIZE`,并保护 `SlotChainProvider.newSlotChain()` 的非线程安全初始化过程。 - -现有 `CtSphTest` 只验证串行查找和容量限制,没有覆盖多线程同时进入 `lookProcessChain` 时必须保持的行为。 - -## 范围 - -本次只修改以下文件: - -- `sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java` -- `sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java` 中两处既有格式问题 - -不新增 JMH 性能基准,不调整生产并发模型,不修改公开 API。 - -## 测试方案 - -### 并发缓存类型契约 - -验证测试可见的缓存实现满足 `ConcurrentMap` 契约,而不是绑定到某一个具体实现类。这个断言直接保护锁外读取所依赖的安全发布机制,并用于执行回归测试的红绿验证。 - -### 同一资源并发查找 - -创建固定大小线程池,让多个任务通过同一个 `CountDownLatch` 同时调用 `lookProcessChain`。收集所有 `Future` 返回值并验证: - -- 所有结果都非空; -- 所有结果都是同一个 `ProcessorSlotChain` 实例; -- `CtSph.entrySize()` 最终等于 1。 - -该用例保护“同资源全局共享同一条 slot chain”的契约。 - -### 容量边界并发创建 - -先将缓存填充到 `MAX_SLOT_CHAIN_SIZE - 1`,然后让多个线程同时用不同资源调用 `lookProcessChain`。验证: - -- 只有一个新资源能够获得非空 chain; -- 其余资源返回 `null`; -- `CtSph.entrySize()` 最终严格等于 `MAX_SLOT_CHAIN_SIZE`,不能越界。 - -该用例保护容量检查与插入必须在同一临界区完成的契约,同时避免实际并发创建 6000 条完整 slot chain,缩短测试时间。 - -## 并发测试稳定性 - -- 使用固定线程池,避免无界创建线程; -- 使用起跑锁存器扩大并发重叠窗口; -- 所有 `Future#get` 都设置超时,失败时不会永久挂住构建; -- 在 `finally` 中关闭线程池; -- 继续依赖现有 `setUp` 和 `tearDown` 清理静态缓存,保证用例隔离。 - -## 验证方式 - -1. 先运行并发缓存类型契约测试。 -2. 临时将缓存退回普通 `HashMap`,确认该测试能够拒绝非并发 Map;随后恢复 `ConcurrentHashMap`。 -3. 运行完整 `CtSphTest`。 -4. 运行 `sentinel-core` 测试和 Java 8 目标级别编译检查。