+ 一份面向 Java 后端工程师的源码阅读地图:从 JDK / JUC 到 Spring、Netty、Kafka、RocketMQ,按核心链路拆解框架设计与底层实现。
+
+
Date: Tue, 7 Jul 2026 23:32:54 +0800
Subject: [PATCH 20/37] merged
---
README.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 7ba7704..ceb6041 100644
--- a/README.md
+++ b/README.md
@@ -29,17 +29,17 @@
-
+
## 项目亮点
-| 你能看到什么 | 重点能力 |
-| --- | --- |
-| JDK / JUC 源码 | 集合、并发、线程池、内存模型、CompletableFuture |
-| Spring / SpringBoot 源码 | IOC 容器、事件机制、启动流程、自动装配、扩展点 |
-| SpringAOP / Security / OAuth2 | 代理机制、过滤器链、安全认证授权主流程 |
+| 你能看到什么 | 重点能力 |
+| --- |--------------------------------------------|
+| JDK / JUC 源码 | 集合、并发、线程池、内存模型、CompletableFuture |
+| Spring / SpringBoot 源码 | IOC 容器、事件机制、启动流程、自动装配、扩展点 |
+| SpringAOP / Security / OAuth2 | 代理机制、过滤器链、安全认证授权主流程 |
| Netty 源码 | Reactor 模型、EventLoop、ChannelPipeline、网络通信链路 |
| Kafka / RocketMQ 源码 | Broker、Producer、Consumer、Rebalance、消息存储与复制 |
From 4c51768d5e0968484c61f7a041506bef41838a7e Mon Sep 17 00:00:00 2001
From: CoderBruis <37364336+coderbruis@users.noreply.github.com>
Date: Wed, 8 Jul 2026 06:32:36 +0800
Subject: [PATCH 21/37] Update README.md
---
README.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/README.md b/README.md
index ceb6041..174d040 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-
-
Java Source Code Learning
From b41b9afef32065e2f2ddc2052cc22d399920438f Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Thu, 9 Jul 2026 22:39:05 +0800
Subject: [PATCH 22/37] =?UTF-8?q?=E8=A1=A5=E5=85=85kafka=E6=9E=B6=E6=9E=84?=
=?UTF-8?q?=E5=9B=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.md b/README.md
index 174d040..01817ba 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,9 @@ Kafka / RocketMQ 消息系统源码
## 精选图解
+### Kafka架构图
+
+
### Kafka Rebalance

From de5de4482c23c14a6d41269159433be351358d9b Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Fri, 10 Jul 2026 20:21:27 +0800
Subject: [PATCH 23/37] =?UTF-8?q?=E6=9B=B4=E6=96=B0HashMap=E5=8E=9F?=
=?UTF-8?q?=E7=90=86=E5=88=86=E6=9E=90=E6=96=87=E7=AB=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 32 +-
...20\347\240\201\345\210\206\346\236\220.md" | 630 ++++++++++++++++++
2 files changed, 646 insertions(+), 16 deletions(-)
create mode 100644 "note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md"
diff --git a/README.md b/README.md
index 01817ba..5fffc49 100644
--- a/README.md
+++ b/README.md
@@ -63,21 +63,6 @@ Kafka / RocketMQ 消息系统源码
这个项目会围绕主流 Java 后端框架的核心路径持续整理源码分析、学习笔记和关键图解。
-## 精选图解
-
-### Kafka架构图
-
-
-### Kafka Rebalance
-
-
-
-
-
-### Kafka ISR / HW / LEO
-
-
-
## 内容目录
@@ -91,6 +76,18 @@ Kafka / RocketMQ 消息系统源码
- [Kafka Rebalance 核心逻辑分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20rebalance%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91%E5%88%86%E6%9E%90.md)
- [Kafka ISR 原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%20ISR%20%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md)
+Kafka架构图
+
+
+Kafka Rebalance流程图
+
+
+
+
+Kafka ISR / HW / LEO关系图
+
+
+
@@ -103,10 +100,13 @@ Kafka / RocketMQ 消息系统源码
- [深入分析 ThreadLocal](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90ThreadLocal.md)
- [深入学习 Java volatile 关键字](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Java%20volatile%E5%85%B3%E9%94%AE%E5%AD%97.md)
- [深入学习 Thread 底层原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Thread%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81.md)
-- 深入学习 JDK1.7 / 1.8 HashMap 扩容原理
+- [深入学习HashMap 底层源码与原理]()
- [开源项目里那些看不懂的位运算分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E9%87%8C%E9%82%A3%E4%BA%9B%E7%9C%8B%E4%B8%8D%E6%87%82%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%E5%88%86%E6%9E%90.md)
- [ThreadPoolExecutor 源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90ThreadPoolExecutor%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md)
+HashMap原理图
+
+
diff --git "a/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md"
new file mode 100644
index 0000000..61386e6
--- /dev/null
+++ "b/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md"
@@ -0,0 +1,630 @@
+当前HashMap版本:JDK1.8
+
+# HashMap的重要成员变量以及内部类
+> **默认容量:DEFAULT_INITIAL_CAPACITY**
+>
+
+```java
+static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
+```
+
+当HashMap没有设置大小时,调用HashMap的put方法时,会进行初始值,并使用DEFAULT_INITIAL_CAPACITY设置默认大小。调用位置在:
+
+```java
+final Node[] resize() {
+ ...
+ else { // zero initial threshold signifies using defaults
+ // HashMap默认大小,16
+ newCap = DEFAULT_INITIAL_CAPACITY;
+ // 扩容阈值,12
+ newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+ }
+ ...
+}
+```
+
+
+
+> **扩容阈值:DEFAULT_LOAD_FACTOR**
+>
+
+```java
+static final float DEFAULT_LOAD_FACTOR = 0.75f;
+```
+
+在resize()中可以看到,当HashMap首次添加元素,调用put时,会计算第一次扩容阈值12,也就是说HashMap中元素=12即触发扩容。
+
+
+
+扩容实际发生在putVal()中,源码如下:
+
+```java
+final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+ boolean evict) {
+ ...
+ if (++size > threshold)
+ resize();
+ ...
+}
+```
+
+当HashMap中添加了新元素,size递增之后判断是否大于扩容阈值。
+
+
+
+> **树化相关配置:TREEIFY_THRESHOLD、UNTREEIFY_THRESHOLD、MIN_TREEIFY_CAPACITY**
+>
+
+```java
+// 桶内链表长度达到 8,考虑红黑树化
+static final int TREEIFY_THRESHOLD = 8;
+// 数组容量至少 64,才真正允许红黑树化
+static final int UNTREEIFY_THRESHOLD = 6;
+// 红黑树节点减少到 6 个或更少,考虑退化回链表
+static final int MIN_TREEIFY_CAPACITY = 64;
+```
+
+链表长到 8 时,如果数组容量小于 64,先扩容;如果数组容量已经至少 64,才转红黑树。树节点少到 6 时,再退化回链表。
+
+
+
+```java
+final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+ boolean evict) {
+ ...
+ if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
+ treeifyBin(tab, hash);
+ ...
+}
+
+final void treeifyBin(Node[] tab, int hash) {
+ ...
+ if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
+ // 先扩容
+ resize();
+ else if ((e = tab[index = (n - 1) & hash]) != null) {
+ // 红黑树化
+ }
+ ...
+}
+```
+
+可以看到,当HashMap中元素小于MIN_TREEIFY_CAPACITY时,是先进行的扩容,而非直接红黑树化。
+
+
+
+> **HashMap内部类Node**
+>
+
+这个类就是HashMap桶中存储的基本单元类,HashMap其实可以理解为一个数组,数组里每个位置叫一个桶bucket。
+
+```java
+static class Node implements Map.Entry {
+ final int hash;
+ final K key;
+ V value;
+ Node next;
+}
+```
+
+在HashMap中通过一个Node数组来存储Node节点。
+
+```java
+transient Node[] table;
+```
+
+如果多个 key 经过 hash 计算后落到同一个桶,就会通过 `next` 串起来,形成链表:
+
+```plain
+table[3]
+ |
+ v
+Node(key1, value1)
+ |
+ next
+ v
+Node(key2, value2)
+ |
+ next
+ v
+Node(key3, value3)
+```
+
+HashMap 的每个桶位保存一个头节点引用;发生 hash 冲突时,新节点通过 next 挂在这个桶的链表后面;链表过长时可能转成红黑树。
+
+还有一个细节需要注意,Node不仅存了key、value最核心的键值对信息,还存储了这个Node的hash值,这个hash值的作用是:**用于快速比较、查找、扩容迁移和树化查找,避免反复计算 hashCode,也保证节点定位稳定。**
+
+hash核心作用代码:
+
+```java
+final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+ boolean evict) {
+ ...
+ Node e; K k;
+ if (p.hash == hash &&
+ ((k = p.key) == key || (key != null && key.equals(k))))
+ e = p;
+ ...
+}
+```
+
+putVal() 里比较 key 是否已经存在,这里先比较hash值,如果hash值不一样,则key一定不相等,可以直接跳过,避免频繁调用 equals()。
+
+
+
+查找时也会用到hash:
+
+```java
+final Node getNode(int hash, Object key) {
+ ...
+ if (first.hash == hash && // always check first node
+ ((k = first.key) == key || (key != null && key.equals(k))))
+ return first;
+ ...
+}
+```
+
+**总结:Node.hash 是 key 的缓存 hash 值,用于快速比较、查找、扩容迁移和树化查找,避免反复计算 hashCode,也保证节点定位稳定。**
+
+
+
+
+
+
+
+# HashMap核心方法源码分析
+## putVal()
+```java
+final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+ boolean evict) {
+
+ // tab:HashMap 底层数组
+ // p:当前桶的第一个节点
+ // n:数组长度
+ // i:key 对应的桶下标
+ Node[] tab; Node p; int n, i;
+
+ // 如果 table 还没初始化,或者长度为 0,则先 resize 初始化数组
+ // new HashMap<>() 第一次 put 时,会在这里创建默认长度 16 的数组
+ if ((tab = table) == null || (n = tab.length) == 0)
+ n = (tab = resize()).length;
+
+ // 计算桶下标:(n - 1) & hash
+ // 因为 n 是 2 的幂,所以等价于 hash % n,但效率更高
+ // 如果这个桶为空,直接放入新 Node
+ if ((p = tab[i = (n - 1) & hash]) == null)
+ tab[i] = newNode(hash, key, value, null);
+
+ else {
+ // e:最终找到的旧节点;如果为 null,说明是新增 key
+ // k:临时保存已有节点的 key
+ Node e; K k;
+
+ // 先检查桶中第一个节点是否就是目标 key
+ // 先比 hash,再比 key 引用或 equals
+ if (p.hash == hash &&
+ ((k = p.key) == key || (key != null && key.equals(k))))
+ e = p;
+
+ // 如果桶已经是红黑树结构,走红黑树插入/查找逻辑
+ else if (p instanceof TreeNode)
+ e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
+
+ else {
+ // 桶是普通链表,遍历链表
+ for (int binCount = 0; ; ++binCount) {
+
+ // 如果下一个节点为空,说明没有找到相同 key
+ // 把新节点追加到链表尾部
+ if ((e = p.next) == null) {
+ p.next = newNode(hash, key, value, null);
+
+ // 链表长度达到树化阈值 8 时,尝试树化
+ // 注意:treeifyBin 内部还会判断 table 长度是否 >= 64
+ // 如果小于 64,优先扩容,不会真正树化
+ if (binCount >= TREEIFY_THRESHOLD - 1)
+ treeifyBin(tab, hash);
+
+ break;
+ }
+
+ // 找到 hash 和 key 都相同的旧节点,停止遍历
+ if (e.hash == hash &&
+ ((k = e.key) == key || (key != null && key.equals(k))))
+ break;
+
+ // 继续向后遍历链表
+ p = e;
+ }
+ }
+
+ // e != null 表示找到了旧 key,不是新增,而是更新 value
+ if (e != null) {
+ V oldValue = e.value;
+
+ // onlyIfAbsent 为 false:直接覆盖旧值
+ // onlyIfAbsent 为 true:只有旧值为 null 时才覆盖
+ // put() 传 false,putIfAbsent() 传 true
+ if (!onlyIfAbsent || oldValue == null)
+ e.value = value;
+
+ // LinkedHashMap 扩展点:访问节点后回调
+ // HashMap 中是空实现
+ afterNodeAccess(e);
+
+ // 返回旧值
+ return oldValue;
+ }
+ }
+
+ // 结构性修改次数 +1
+ // 用于 fail-fast,比如迭代时检测并发修改
+ ++modCount;
+
+ // 新增节点后 size +1
+ // 如果 size 超过扩容阈值 threshold,则扩容
+ if (++size > threshold)
+ resize();
+
+ // LinkedHashMap 扩展点:插入节点后回调
+ // HashMap 中是空实现
+ afterNodeInsertion(evict);
+
+ // 新增 key 时返回 null
+ return null;
+}
+```
+
+从源码中可以看到几个细节。
+
+### 1)**JDK8 的 HashMap 链表插入用的是尾插法**。
+```java
+if ((e = p.next) == null) {
+ p.next = newNode(hash, key, value, null);
+ ...
+ break;
+}
+```
+
+```plain
+table[i] -> A -> B
+```
+
+插入新元素之后
+
+```plain
+table[i] -> A -> B -> C
+```
+
+对比 JDK7,JDK7 HashMap 扩容迁移时使用头插法,可能在并发扩容下形成环链表,导致死循环。JDK8 改了扩容迁移逻辑,并且普通链表插入也是尾插,能保持链表相对顺序。
+
+JDK8 HashMap 仍然不是线程安全的。尾插法解决不了所有并发问题。并发 put 仍可能出现数据覆盖、丢数据、size 不准、扩容状态异常等问题。
+
+
+
+### 2)JDK8 HashMap线程不安全原因分析
+JDK8 HashMap 不安全,不是因为还会像 JDK7 那样容易成环,而是因为 put、size++、resize、table 发布、链表/红黑树修改都没有加锁或 CAS,多线程并发读写会发生覆盖、丢数据、计数错误和可见性问题。
+
+
+
+> **桶为空时,多线程操作桶,会直接覆盖table[i]**
+>
+
+```plain
+if ((p = tab[i = (n - 1) & hash]) == null)
+ tab[i] = newNode(hash, key, value, null);
+```
+
+此处最核心原因是操作同一个桶位置,没有加锁,也没有进行CAS,线程不安全。
+
+
+
+> **链表尾插时,p.next 可能互相覆盖**
+>
+
+```java
+if ((e = p.next) == null) {
+ p.next = newNode(hash, key, value, null);
+ ...
+ break;
+}
+```
+
+多线程尾插法容易导致p.next正确结果被覆盖。
+
+
+
+> **++size不是原子操作**
+>
+
+```java
+if (++size > threshold)
+ resize();
+```
+
+此处++size不是原子操作,会导致最终size结果不准确。
+
+
+
+
+
+### 3)JDK7 HashMap线程不安全原因分析
+JDK7 并发扩容时,头插法会反转链表,两个线程交叉修改同一批 Entry 的 next 指针,就可能把 A.next 指向 B,同时又把 B.next 指回 A,形成死循环。
+
+```plain
+T1 线程处理原始链表:
+
+A -> B -> null
+
+
+T2 线程头插迁移后,把指针改成:
+
+B -> A -> null
+
+
+T1 线程继续按旧进度迁移,但读到了 T2 线程改过的 B.next:
+
+B.next = A
+
+
+最后成了循环链表,变成下图:
+
+A -> B
+^ |
+|____|
+```
+
+## getNode()
+getNode()方法源码如下
+
+```java
+final Node getNode(int hash, Object key) {
+ // tab:底层数组
+ // first:桶里的第一个节点
+ // e:遍历链表时的当前节点
+ // n:数组长度
+ // k:临时保存节点 key
+ Node[] tab; Node first, e; int n; K k;
+
+ // table 不为空、长度大于 0,并且目标桶不为空,才继续查找
+ if ((tab = table) != null && (n = tab.length) > 0 &&
+ (first = tab[(n - 1) & hash]) != null) {
+
+ // 先检查桶里的第一个节点
+ // 先比 hash,再比 key 引用或 equals
+ if (first.hash == hash &&
+ ((k = first.key) == key || (key != null && key.equals(k))))
+ return first;
+
+ // 第一个节点不是目标 key,并且后面还有节点
+ if ((e = first.next) != null) {
+
+ // 如果桶已经树化,走红黑树查找
+ if (first instanceof TreeNode)
+ return ((TreeNode)first).getTreeNode(hash, key);
+
+ // 普通链表,依次向后遍历
+ do {
+ // 找到 hash 和 key 都匹配的节点,直接返回
+ if (e.hash == hash &&
+ ((k = e.key) == key || (key != null && key.equals(k))))
+ return e;
+
+ // 继续访问下一个节点
+ } while ((e = e.next) != null);
+ }
+ }
+
+ // table 为空、桶为空,或者遍历完没找到
+ return null;
+}
+```
+
+ 核心流程:
+
+1.table 为空,直接返回 null。
+
+2.根据 hash 定位桶下标。
+
+3.先查桶里的第一个节点。
+
+4.如果是红黑树,走树查找。
+
+5.否则遍历链表。
+
+6.找不到返回 null。
+
+## hash()
+hash()方法是HashMap中的hash扰动函数,作用是:把 key 的 hashCode() 再处理一下,让高位信息也参与到低位计算,减少哈希冲突。
+
+```java
+static final int hash(Object key) {
+ int h;
+ return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
+}
+```
+
+ 最核心的是这一段:(h = key.hashCode()) ^ (h >>> 16)
+
+> **为什么要这么做?**
+>
+
+在HashMap中计算桶下标都需要通过:(n - 1) & hash 来计算。又因为n是2的幂,所以这个计算主要以来的是hash的**低位**,高位一直没有利用到。HashMap初始容量为16,则n-1=15,15的二进制位:0000 1111,那么:(n - 1) & hash由于与操作的特性,这实际上只看hash的低4位。**这会导致:如果很多 key 的低位相同,即使高位不同,也会落到同一个桶里。**
+
+所以 HashMap 做了这个扰动:h ^ (h >>> 16)。把高 16 位右移到低 16 位,再和原 hash 异或,让高位信息参与低位计算。
+
+举例:
+
+```java
+>>>是无符号右移:整体向右移动,左边补 0,右边被移出去的低位丢弃。
+
+原始 hash:
+
+高 16 位 低 16 位
+AAAA AAAA BBBB BBBB
+
+h >>> 16:
+
+0000 0000 AAAA AAAA
+
+异或后:
+
+AAAA AAAA (BBBB BBBB ^ AAAA AAAA)
+```
+
+## resize()
+resize也是HashMap的核心方法之一,源码如下:
+
+```java
+final Node[] resize() {
+ // 旧数组
+ Node[] oldTab = table;
+
+ // 旧容量,table 为空则为 0
+ int oldCap = (oldTab == null) ? 0 : oldTab.length;
+ // 旧扩容阈值
+ int oldThr = threshold;
+ // 新容量、新阈值
+ int newCap, newThr = 0;
+
+ // 情况一:旧数组已经存在,说明是正常扩容
+ if (oldCap > 0) {
+ // 已经达到最大容量,不能再扩容
+ if (oldCap >= MAXIMUM_CAPACITY) {
+ threshold = Integer.MAX_VALUE;
+ return oldTab;
+ }
+ // 容量扩大 2 倍
+ // 阈值也扩大 2 倍
+ else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
+ oldCap >= DEFAULT_INITIAL_CAPACITY)
+ newThr = oldThr << 1;
+ }
+
+ // 情况二:数组还没创建,但 threshold > 0
+ // 说明构造 HashMap 时指定了初始容量
+ // 例如 new HashMap<>(32)
+ // 此时 threshold 暂时存的是 tableSizeFor(initialCapacity)
+ else if (oldThr > 0)
+ newCap = oldThr;
+
+ // 情况三:无参构造 new HashMap<>()
+ // 第一次 put 时走这里,使用默认容量 16,默认阈值 12
+ else {
+ newCap = DEFAULT_INITIAL_CAPACITY;
+ newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+ }
+
+ // 如果上面没有算出新阈值,则按 newCap * loadFactor 计算
+ if (newThr == 0) {
+ float ft = (float)newCap * loadFactor;
+ newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
+ (int)ft : Integer.MAX_VALUE);
+ }
+
+ // 更新扩容阈值
+ threshold = newThr;
+ // 创建新数组
+ @SuppressWarnings({"rawtypes","unchecked"})
+ Node[] newTab = (Node[])new Node[newCap];
+ // table 指向新数组
+ table = newTab;
+
+ // 如果旧数组不为空,需要迁移旧数据
+ if (oldTab != null) {
+
+ // 遍历旧数组每个桶
+ for (int j = 0; j < oldCap; ++j) {
+ Node e;
+ // 如果当前桶不为空
+ if ((e = oldTab[j]) != null) {
+
+ // 旧桶置空,帮助 GC
+ oldTab[j] = null;
+ // 情况一:桶里只有一个节点,直接重新计算下标放入新数组
+ if (e.next == null)
+ newTab[e.hash & (newCap - 1)] = e;
+ // 情况二:桶里是红黑树,走红黑树拆分逻辑
+ else if (e instanceof TreeNode)
+ ((TreeNode)e).split(this, newTab, j, oldCap);
+ // 情况三:桶里是链表
+ else {
+ // lo 链:扩容后仍然留在原下标 j
+ Node loHead = null, loTail = null;
+ // hi 链:扩容后移动到 j + oldCap
+ Node hiHead = null, hiTail = null;
+ Node next;
+
+ // 遍历旧链表,把节点拆成 lo 和 hi 两条链
+ do {
+ // 先保存下一个节点
+ next = e.next;
+ // 判断扩容后位置是否不变
+ if ((e.hash & oldCap) == 0) {
+ if (loTail == null)
+ loHead = e;
+ else
+ loTail.next = e;
+ loTail = e;
+ }
+ // 扩容后位置变为 原下标 + oldCap
+ else {
+ if (hiTail == null)
+ hiHead = e;
+ else
+ hiTail.next = e;
+ hiTail = e;
+ }
+
+ } while ((e = next) != null);
+
+ // lo 链放回原下标 j
+ if (loTail != null) {
+ loTail.next = null;
+ newTab[j] = loHead;
+ }
+ // hi 链放到新下标 j + oldCap
+ if (hiTail != null) {
+ hiTail.next = null;
+ newTab[j + oldCap] = hiHead;
+ }
+ }
+ }
+ }
+ }
+
+ // 返回新数组
+ return newTab;
+}
+```
+
+
+
+
+
+# 总结
+HashMap中最核心的概念如下
+
+```java
+数组 + 链表 + 红黑树
+默认容量 16
+负载因子 0.75
+容量始终是 2 的幂
+链表长度 >= 8 且 table 容量 >= 64 时树化
+树节点过少时退化回链表
+允许 null key / null value
+非线程安全
+```
+
+
+
+JDK8和JDK7相比:
+
+```java
+JDK7:
+数组 + 链表
+
+JDK8:
+数组 + 链表 + 红黑树
+```
+
From a1dedcd58aa5cc4c6e0fcfeb18f477f9f61ed30b Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Fri, 10 Jul 2026 20:33:38 +0800
Subject: [PATCH 24/37] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 6 +-
...20\347\240\201\345\210\206\346\236\220.md" | 6 +-
...25\345\261\202\345\216\237\347\220\206.md" | 4 +
...73\350\276\221\345\210\206\346\236\220.md" | 136 ++++++++++++++----
4 files changed, 120 insertions(+), 32 deletions(-)
rename "note/kafka/kafka rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" => "note/kafka/kafka Rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" (81%)
diff --git a/README.md b/README.md
index 5fffc49..b426ccb 100644
--- a/README.md
+++ b/README.md
@@ -80,10 +80,10 @@ Kafka / RocketMQ 消息系统源码

Kafka Rebalance流程图
-
-

+
+
Kafka ISR / HW / LEO关系图

@@ -100,7 +100,7 @@ Kafka / RocketMQ 消息系统源码
- [深入分析 ThreadLocal](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90ThreadLocal.md)
- [深入学习 Java volatile 关键字](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Java%20volatile%E5%85%B3%E9%94%AE%E5%AD%97.md)
- [深入学习 Thread 底层原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Thread%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81.md)
-- [深入学习HashMap 底层源码与原理]()
+- [深入学习HashMap 底层源码与原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/HashMap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md)
- [开源项目里那些看不懂的位运算分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E9%87%8C%E9%82%A3%E4%BA%9B%E7%9C%8B%E4%B8%8D%E6%87%82%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%E5%88%86%E6%9E%90.md)
- [ThreadPoolExecutor 源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90ThreadPoolExecutor%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md)
diff --git "a/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md"
index 61386e6..a586cb2 100644
--- "a/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md"
+++ "b/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md"
@@ -1,4 +1,8 @@
-当前HashMap版本:JDK1.8
++ 当前HashMap版本:JDK1.8
++ 转载请标明出处
+
+# HashMap底层原理图
+
# HashMap的重要成员变量以及内部类
> **默认容量:DEFAULT_INITIAL_CAPACITY**
diff --git "a/note/kafka/Kafka ISR \345\272\225\345\261\202\345\216\237\347\220\206.md" "b/note/kafka/Kafka ISR \345\272\225\345\261\202\345\216\237\347\220\206.md"
index 02e5bd2..93a9b36 100644
--- "a/note/kafka/Kafka ISR \345\272\225\345\261\202\345\216\237\347\220\206.md"
+++ "b/note/kafka/Kafka ISR \345\272\225\345\261\202\345\216\237\347\220\206.md"
@@ -1,6 +1,10 @@
+ 当前分析版本是kafka最新版本(版本随时变化,最新分析代码请关注仓库:https://github.com/coderbruis/kafka source_code_analysis分支,底层原理持续更新)
+ 转载请标明出处
+# Kafka HW,ISR,LEO关系图
+
+
+
# Kafka ISR是什么?解决什么问题?
## 是什么?
Kafka ISR是In-Sync Replicas,意思是“与leader保持同步的副本集合”。在Kafka中会有leader副本和follower副本,下面举例:
diff --git "a/note/kafka/kafka rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" "b/note/kafka/kafka Rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md"
similarity index 81%
rename from "note/kafka/kafka rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md"
rename to "note/kafka/kafka Rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md"
index 409bc93..bf3bde3 100644
--- "a/note/kafka/kafka rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md"
+++ "b/note/kafka/kafka Rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md"
@@ -1,6 +1,12 @@
+ 当前分析版本是kafka最新版本(版本随时变化,最新分析代码请关注仓库:[https://github.com/coderbruis/kafka](https://github.com/coderbruis/kafka) **source_code_analysis分支**,底层原理持续更新)
+ 转载请标明出处
+# Kafka Rebalance流程图
+
+
+
+
+
# Kafka Rebalance 核心流程
`KafkaConsumer.poll()` 是消费者触发 rebalance 的主要入口。
@@ -629,20 +635,47 @@ if (protocol == RebalanceProtocol.COOPERATIVE &&
# EAGER 和 COOPERATIVE 的差异
## EAGER
-EAGER rebalance 的特点是简单直接。
+EAGER核心特点:全量停、全量分、再恢复
+
+### EAGER Rebalance流程
+
+第一步:触发 Rebalance
+1. 成员变化:join / leave / session timeout / max.poll.interval 超时等。
+2. 订阅或元数据变化:订阅 topic 变化、正则订阅匹配变化、partition 数变化等。
+
+第二步:Coordinator 进入 Rebalance 状态
+1. Coordinator 将 group 状态切到 PreparingRebalance(可能不是第一次进入rebalance,所以这里状态已经是PreparingRebalance了,正常第一次是在JoinGroup进入Coordinator里,会将状态变更为PreparingRebalance。)
+2. 现有成员会通过 heartbeat 或 poll 流程感知需要重新加入 group。
+3. 新成员或需要重分配的成员准备发送 JoinGroupRequest。
+
+第三步:EAGER 全员撤销旧 assignment,然后 JoinGroup
+1. 每个 consumer 在 JoinGroup 前执行 onJoinPrepare。
+2. EAGER 协议下 revoke 当前持有的所有 partitions。
+3. 调用 onPartitionsRevoked(allAssignedPartitions)。
+4. 清空本地 assignment,停止这些 partitions 的消费。全组成员进入消费暂停状态(STW)。
+5. 向 coordinator 发送 JoinGroupRequest,携带 subscription 和支持的 assignor。
+
+第四步:JoinGroup 完成成员协商
+1. Coordinator 收集本轮成员的 JoinGroupRequest。
+2. 选择 leader consumer。
+3. 选择 assignment strategy / protocol,leader consumer生成新的assignment。
+4. 生成新的 generationId,同时将group状态变更为COMPLETING_REBALANCE。
+5. JoinGroupResponse 返回给成员。
+6. Leader 会拿到所有成员的 subscription metadata。
+
+第五步:分配 + SyncGroup
+1. Leader consumer 执行分配算法,比如 Range / Sticky。
+2. Leader 通过 SyncGroupRequest 把全组 assignment 发给 coordinator。
+3. Follower 也发送 SyncGroupRequest,但通常不带 assignment。
+4. Coordinator 保存本轮 assignment,group状态变更为STABLE。
+5. Coordinator 通过各自的 SyncGroupResponse 返回每个 consumer 自己的 assignment。
+
+第六步:恢复消费
+1. Consumer 收到自己的 assignment。
+2. 更新本地 assignment。
+3. 调用 onPartitionsAssigned(newAssignedPartitions)。
+4. 从对应 offset 开始拉取消息,恢复消费。
-```plain
-onJoinPrepare:
- revoke all partitions
- clear local assignment
-
-leader assign:
- assign all partitions again
-
-onJoinComplete:
- assign new partitions
- trigger onPartitionsAssigned
-```
优点:
@@ -655,23 +688,70 @@ onJoinComplete:
+ 即使某些 partition 仍然分给同一个 consumer,也会先 revoke 再 assign。
## COOPERATIVE
-COOPERATIVE rebalance 的特点是渐进迁移。
-
-```plain
-onJoinPrepare:
- keep still-owned partitions
- revoke only obviously invalid partitions
+COOPERATIVE rebalance 的特点是渐进迁移,不会撤销所有分区导致全部分区STW。
+
+### COOPERATIVE Rebalance流程
+
+第一轮:标记迁移,旧 owner revoke(onwer表示持有parition的consumer)
+1. 成员变化、订阅变化或元数据变化触发 rebalance。
+2. consumer 发送 JoinGroup 给 coordinator。
+ JoinGroup metadata 里包含:
+ ○ 当前 subscription
+ ○ 当前本地已分配分区,也就是 ownedPartitions
+3. coordinator 收集所有成员的 JoinGroup。
+ coordinator 负责:
+ ○ 选 leader
+ ○ 选 assignor/protocol
+ ○ 把所有成员 subscription metadata 返回给 leader
+ coordinator收集完所有的JoinGroup请求之后,会进入新的generationId(递增)。
+4. leader consumer 执行 assignor。
+ CooperativeStickyAssignor 会:
+ ○ 先计算目标 assignment
+ ○ 如果某个分区要从旧 owner 转给新 owner
+ ○ 但旧 owner 在本轮 JoinGroup 里仍上报该分区为 owned
+ ○ 那么本轮不会把该分区分给新 owner
+ ○ 同时旧 owner 的本轮 assignment 不再包含该分区
+5. leader Consumer 通过 SyncGroup 把全组 assignment 提交给 coordinator。其他consumer也会发送一个空的SyncGroup给coorinator
+6. coordinator 保存 assignment,并通过 SyncGroupResponse 返回每个 consumer 自己的 assignment。
+7. consumer 处理 SyncGroupResponse。
+ 本地计算:
+```text
+ owned = 当前本地 assignment
+ assigned = 本轮收到的 assignment
+
+ revoked = owned - assigned
+ added = assigned - owned
+```
+
+需要注意,第一轮主要是做撤销,但是也可能直接添加新的分区分配结果。因为如果这一轮有某个分区本来就没有旧onwer(旧的持有这个parition的consumer),这一轮就可以直接分配给新onwer。
+8. 如果 revoked 非空:
+ ○ 调用 onPartitionsRevoked(revoked)
+ ○ 调用 requestRejoin()
+ ○ 后续把本地 assignment 更新为 assigned
+9. 如果 added 非空:
+ ○ 调用 onPartitionsAssigned(added)
+ 注意:正在从旧 owner 转移给新 owner 的分区,第一轮不会出现在新 owner 的 added 里。
+ 第二轮:旧owner撤销分区,新owner获得第一轮撤销的分区
+1. 因为第一轮有 consumer 调用了 requestRejoin(),group 再次 rebalance。
+2. consumer 再次发送 JoinGroup。
+ 此时旧 owner 的本地 assignment 已经更新,所以它上报的 ownedPartitions 不再包含刚刚 revoked 的分区。
+ 被撤销的分区不会通过JoinGroup发送给Coordinator,revokeList和addLIst都是通过集合计算来得出的。
+ coordinator收集完所有的JoinGroup请求之后,会进入新的generationId(递增)。
+3. coordinator 再次收集成员,选 leader,选协议。
+ 然后将所有成员 subscription metadata 返回给 leader consumer。
+4. leader 再次执行 assignor。
+ 这次 assignor 发现:
+ ○ 该分区已经没有旧 owner 上报 owned
+ ○ 可以安全分配给新 owner
+5. leader 通过 SyncGroup 提交新的全组 assignment。
+6. coordinator 通过 SyncGroupResponse 返回各成员自己的 assignment。
+7. 新 owner 处理 assignment。
+ 本地计算:
+ added = assigned - owned
+8. 新 owner 对新增分区调用:
+ onPartitionsAssigned(added)
-leader assign:
- do not immediately reassign partitions still owned by others
-onJoinComplete:
- revoke partitions that need migration
- request another rejoin if needed
-
-next rebalance:
- assign released partitions to new owners
-```
优点:
From 8ab30cd0f7d23e527f5d77fbf8c6b2d93fd17a4f Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Sat, 11 Jul 2026 10:50:31 +0800
Subject: [PATCH 25/37] =?UTF-8?q?=E6=96=B0=E5=A2=9EAQS=E5=8E=9F=E7=90=86?=
=?UTF-8?q?=E5=88=86=E6=9E=90=E6=96=87=E7=AB=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 3 +
...20\347\240\201\345\210\206\346\236\220.md" | 1060 +++++++++++++++++
2 files changed, 1063 insertions(+)
create mode 100644 "note/JDK/AQS\346\272\220\347\240\201\345\210\206\346\236\220.md"
diff --git a/README.md b/README.md
index b426ccb..158573c 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,9 @@ Kafka / RocketMQ 消息系统源码
HashMap原理图

+AQS架构图
+
+
diff --git "a/note/JDK/AQS\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/note/JDK/AQS\346\272\220\347\240\201\345\210\206\346\236\220.md"
new file mode 100644
index 0000000..8fb00bc
--- /dev/null
+++ "b/note/JDK/AQS\346\272\220\347\240\201\345\210\206\346\236\220.md"
@@ -0,0 +1,1060 @@
+AQS版本:JDK 1.8
+
+# AQS灵魂三问
+## 是什么?
+AQS 全称是 AbstractQueuedSynchronizer,中文通常翻译为:**抽象队列同步器**。
+
+AQS 本身不是一把可以直接使用的锁,而是一个用于构建锁和同步器的基础框架。
+
+JDK 中很多并发工具都基于 AQS 实现:
+
+```java
+ReentrantLock
+ReentrantReadWriteLock
+Semaphore
+CountDownLatch
+ThreadPoolExecutor.Worker
+```
+
+## 解决什么问题?
+AQS 主要解决两类通用问题:
+
+1. 如何通过一个同步状态 state 判断资源是否可获取
+2. 获取失败后,如何让线程排队、阻塞,并在合适时机被唤醒
+
+
+
+AQS 用于统一解决线程竞争、等待队列、阻塞与唤醒问题。
+
+## 起什么作用?
+AQS是 ReentrantLock、Semaphore 等同步器的基础框架。
+
+AQS 将同步器拆成了两部分:
+
+1. 子类负责:
+ 1. 定义 state 的业务含义。
+ 2. 定义获取资源和释放资源的规则。
+2. AQS负责:
+ 1. CAS 修改状态。
+ 2. 维护等待队列。
+ 3. 阻塞线程。
+ 4. 唤醒线程。
+ 5. 处理中断、超时和取消。
+
+例如:
+
+1. ReentrantLock:state 表示锁的重入次数
+2. CountDownLatch:state 表示剩余计数
+3. Semaphore:state 表示剩余许可证数量
+
+****
+
+**AQS 的核心内容可以浓缩为:**
+
+1. 一个 volatile int state
+2. 一个双向同步等待队列
+3. CAS 原子操作
+4. LockSupport.park/unpark 阻塞与唤醒
+
+# AQS整体架构
+AQS 支持两种资源获取模式:
+
+1. 独占模式 Exclusive:同一时刻只允许一个线程成功获取资源。
+2. 共享模式 Shared:同一时刻允许多个线程成功获取资源。
+
+
+
+典型应用:
+
+1. 独占模式:ReentrantLock、ReentrantReadWriteLock 写锁。
+2. 共享模式:Semaphore、CountDownLatch、ReentrantReadWriteLock 读锁。
+
+
+
+两种模式共用同一个同步队列:
+
+```plain
+head tail
+ | |
+ v v
+[哨兵节点] <-> [独占节点 T1] <-> [共享节点 T2] <-> [独占节点 T3]
+```
+
+
+
+AQS 不理解“锁”“许可证”“计数器”等业务含义。它只负责:
+
+1. tryAcquire/tryAcquireShared 成功:线程继续执行。
+2. tryAcquire/tryAcquireShared 失败:线程进入队列并阻塞。
+3. tryRelease/tryReleaseShared 成功:唤醒后继线程。
+
+
+
+
+
+
+
+
+# AQS核心结论
+AQS 本质上是一个用于构建锁和同步器的基础框架,核心由两部分组成:一个表示同步状态的 state,以及一个保存等待线程的 FIFO 双向队列。
+
+
+
+线程获取资源时,首先通过 CAS 尝试修改 state:
+
++ 获取成功:线程继续执行。
++ 获取失败:线程封装成 Node 加入等待队列,并通过 LockSupport.park() 阻塞。
++ 资源释放:修改 state,再通过 LockSupport.unpark() 唤醒后继节点重新竞争。
+
+
+
+AQS 自身不定义资源如何获取和释放,而是通过模板方法交给子类实现:
+
++ tryAcquire()
++ tryRelease()
++ tryAcquireShared()
++ tryReleaseShared()
+
+
+
+因此,AQS 的核心可以概括为:
+
++ state 表示资源或锁状态。
++ CAS 保证状态修改的原子性。
++ 等待队列管理获取资源失败的线程。
++ park/unpark 实现线程阻塞与唤醒。
++ 模板方法定义具体同步规则。
++ 支持独占模式和共享模式。
++ ConditionObject 提供条件等待队列。
+
+
+
+AQS 将“资源竞争、线程排队、阻塞与唤醒”统一封装,子类只需要实现资源的获取和释放规则。
+
+# AQS 的重要成员变量和内部类
+## state:同步状态
+state 是 AQS 中最核心的变量:
+
+```java
+private volatile int state;
+```
+
+AQS 使用一个 volatile int 保存同步状态,并通过以下方法读取和修改:
+
+```java
+protected final int getState()
+protected final void setState(int newState)
+protected final boolean compareAndSetState(int expect, int update)
+```
+
+其中:
+
+```plain
+getState():读取当前同步状态
+setState():直接设置同步状态
+compareAndSetState():通过 CAS 原子更新同步状态
+```
+
+state 的含义由具体同步器决定。
+
+
+
+ReentrantLock 中的 state表示
+
++ state = 0:锁未被任何线程持有
++ state = 1:当前线程第一次获取锁
++ state = 2:当前线程重入一次
++ state = 3:当前线程重入两次
+
+
+
+Semaphore 中的 state表示当前剩余许可证数量。
+
+例如:
+
+```plain
+初始许可证数量为 3
+
+state = 3:还可以有 3 个线程获取
+state = 2:还可以有 2 个线程获取
+state = 0:没有剩余许可证
+```
+
+
+
+CountDownLatch中的state表示尚未完成的计数。
+
+例如:
+
+```plain
+new CountDownLatch(3)
+
+初始 state = 3
+每次 countDown(),state 减 1
+state = 0 时,所有 await() 线程可以继续执行
+```
+
+****
+
+**总结:AQS 只提供一个线程安全的状态字段,不规定状态的业务含义。**
+
+
+
+## head 和 tail:同步队列首尾节点
+```java
+private transient volatile Node head;
+private transient volatile Node tail;
+```
+
+AQS 使用一个基于 CLH 思想改造的双向链表维护获取资源失败的线程。
+
+```plain
+head tail
+ | |
+ v v
+[哨兵节点] <-> [节点 T1] <-> [节点 T2] <-> [节点 T3]
+```
+
+其中:
+
+```plain
+head:队列头节点,通常是已经获取过资源的哨兵节点
+
+tail:队列尾节点,新节点通过 CAS 追加到 tail
+```
+
+head和tail都使用 volatile 修饰,保证不同线程之间的可见性。
+
+
+
+AQS 创建时不会立刻创建队列。只有第一次发生竞争、线程需要入队时,才会初始化一个空的哨兵节点:
+
+```plain
+head == tail == new Node()
+```
+
+这样可以避免在从未发生竞争的同步器上浪费节点对象。
+
+## Node:同步队列节点
+AQS 内部通过Node表示一个等待线程。
+
+源码结构如下:
+
+```java
+static final class Node {
+ ...
+ static final Node SHARED = new Node();
+ static final Node EXCLUSIVE = null;
+
+ static final int CANCELLED = 1;
+ static final int SIGNAL = -1;
+ static final int CONDITION = -2;
+ static final int PROPAGATE = -3;
+
+ volatile int waitStatus;
+ // 指向同步队列中的前驱节点。
+ volatile Node prev;
+ volatile Node next;
+ // 保存当前节点对应的等待线程。
+ // 当节点需要被唤醒时,AQS 会执行:LockSupport.unpark()
+ volatile Thread thread;
+ Node nextWaiter;
+ ...
+}
+```
+
+### prev
+prev表示指向同步队列中的前驱节点。
+
+```java
+volatile Node prev;
+```
+
+指向同步队列中的前驱节点。
+
+AQS 判断当前节点能否尝试获取资源时,最重要的条件是:**当前节点的前驱节点是否是 head。**
+
+只有队列中的第一个有效等待节点,才有资格再次调用tryAcquire()或tryAcquireShared()竞争资源。
+
+
+
+AQS中判断源码如下:
+
+独占模式acquire:
+
+```java
+final boolean acquireQueued(final Node node, int arg) {
+ ...
+ final Node p = node.predecessor();
+ if (p == head && tryAcquire(arg)) {
+ ...
+ }
+ ...
+}
+```
+
+共享模式acquire:
+
+```java
+private void doAcquireShared(int arg) {
+ ...
+ final Node p = node.predecessor();
+ if (p == head) {
+ ...
+ }
+ ...
+}
+```
+
+
+
+**所以“当前节点的前驱节点是否是 head”是非常重要的一个条件。**
+
+
+
+### next(设计思想非常的细节)
+指向同步队列中的后继节点。
+
+```java
+volatile Node next;
+```
+
+释放资源时,AQS 通常通过 head.next 找到需要唤醒的线程。
+
+但next只是一个优化路径,不是绝对可靠的队列判断依据。
+
+原因是节点入队过程如下:
+
+1. node.prev = oldTail
+2. CAS 把 tail 从 oldTail 修改为 node
+3. oldTail.next = node
+
+```java
+// 入队操作,并返回当前node的前序节点
+private Node enq(final Node node) {
+ for (;;) {
+ Node t = tail;
+ if (t == null) {
+ if (compareAndSetHead(new Node()))
+ tail = head;
+ } else {
+ // 1. oldTail <- node
+ node.prev = t;
+ // 2. oldTail <- node(tail) 将node赋值给tail,表明当前node成为了tail
+ if (compareAndSetTail(t, node)) {
+ // oldTail -> node(tail)
+ t.next = node;
+ // 返回oldTail
+ return t;
+ }
+ }
+ }
+}
+```
+
+
+
+在线程完成第 2 步、尚未完成第 3 步时:
+
+```plain
+tail 已经指向新节点
+但 oldTail.next 仍然是 null
+```
+
+因此在部分场景下,如果判断到next=null,则AQS 会从tail沿着prev反向扫描,寻找有效后继节点。
+
+```java
+private void unparkSuccessor(Node node) {
+ ...
+ Node s = node.next;
+ if (s == null || s.waitStatus > 0) {
+ s = null;
+ // 通过next找不到要unpark的节点,则从tail开始向前遍历
+ for (Node t = tail; t != null && t != node; t = t.prev)
+ if (t.waitStatus <= 0)
+ s = t;
+ }
+ ...
+}
+```
+
+
+
+**总结:**因为有 next 指针,所以 release 时可以直接取 head,再通过 head.next 找到要唤醒的后继节点,通常是 O(1)。而如果没有next指针,则需要从tail尾结点向前通过prev遍历,时间复杂度为O(n)。所以在入队维护next指针的时候,因为:
+
+1)node.prev = t;
+
+2)compareAndSetTail(t, node)
+
+3)t.next = node;
+
+导致可能t.next会存在null的场景,所以通过next指针遍历会拿不到数据。所以如果next为null,就会退化成通过prev指针去获取目标节点。
+
+
+
+### nextWaiter
+nextWaiter 有两个用途。
+
+#### 用途一:标识节点模式
+```java
+nextWaiter == Node.SHARED:共享节点
+nextWaiter == Node.EXCLUSIVE:独占节点
+```
+
+其中 Node.EXCLUSIVE 实际是 null。
+
+#### 用途二:连接 Condition 条件队列
+Condition 条件队列是单向链表,节点之间通过 `nextWaiter` 连接:
+
+```plain
+firstWaiter
+ |
+ v
+[T1 CONDITION] -> [T2 CONDITION] -> [T3 CONDITION]
+ ^
+ |
+ lastWaiter
+```
+
+所以:
+
+```plain
+prev/next:用于 AQS 同步队列
+nextWaiter:用于 Condition 条件队列,或者标记共享模式
+```
+
+
+
+### waitStatus 状态详解
+Node 中最难理解的字段是:
+
+```java
+volatile int waitStatus;
+```
+
+它表示节点当前的等待状态。
+
+JDK 8 中主要有以下几种状态:
+
++ CANCELLED = 1
++ SIGNAL = -1
++ CONDITION = -2
++ PROPAGATE = -3
++ 默认状态 = 0
+
+****
+
+#### 0:默认状态
+新创建的同步队列节点,`waitStatus` 默认为 0。
+
+表示节点当前没有特殊状态。
+
+
+
+#### CANCELLED = 1:节点已取消
+```java
+static final int CANCELLED = 1;
+```
+
+出现以下情况时,节点可能被取消:
+
++ 线程等待超时
++ 线程在可中断等待中被中断
++ 获取资源过程中发生异常
+
+
+
+取消后的节点不会再次参与资源竞争。
+
+AQS 会在后续遍历中跳过 waitStatus > 0 的节点。
+
+需要注意:CANCELLED 是唯一的正数状态。
+
+因此源码中经常通过下面的方式判断节点是否已取消:waitStatus > 0
+
+
+
+#### SIGNAL = -1:后继节点需要被唤醒
+```java
+static final int SIGNAL = -1;
+```
+
+**这是同步队列中最重要的状态。**
+
+
+
+假设队列结构如下:pred -> node
+
+当 pred.waitStatus == SIGNAL 时,表示:
+
+```plain
+node 准备阻塞。
+当 pred 释放资源、成为无效头节点或被取消时,pred 负责唤醒 node。
+```
+
+一个非常容易混淆的点是:
+
+**SIGNAL 状态保存在前驱节点上,但它表达的是后继节点需要被唤醒。**
+
+例如:
+
+```plain
+head(waitStatus = SIGNAL) -> T1
+```
+
+表示 T1 可以安全阻塞,head 对应的资源持有者释放资源时,需要唤醒 T1。
+
+
+
+为什么不把 SIGNAL 放在当前节点上?
+
+因为释放资源时,释放线程主要操作当前 `head`,由前驱节点记录唤醒责任,可以减少对后继节点状态的竞争修改。
+
+
+
+#### CONDITION = -2:节点正在 Condition 条件队列中等待
+```java
+static final int CONDITION = -2;
+```
+
+线程调用 Condition.await() 后,会进入 Condition 条件队列,此时节点状态为 CONDITION。
+
+```plain
+Condition 条件队列:
+
+[T1 CONDITION] -> [T2 CONDITION] -> [T3 CONDITION]
+```
+
+
+
+当其他线程调用 signal() 后,节点会:
+
+```plain
+waitStatus:CONDITION -> 0
+从 Condition 条件队列转移到 AQS 同步队列
+```
+
+只有重新进入同步队列并再次获取锁后,`await()` 才会返回。
+
+
+
+#### PROPAGATE = -3:共享模式继续传播
+```java
+static final int PROPAGATE = -3;
+```
+
+PROPAGATE 只用于共享模式。
+
+它用于记录:即使当前释放动作没有直接找到需要唤醒的节点,后续共享节点仍需要继续检查并传播唤醒。
+
+
+
+该状态主要用于解决共享模式下并发获取、释放交错时可能出现的传播遗漏问题。
+
+可以先把它理解为:共享唤醒传播标记。
+
+
+
+## waitStatus 的正负设计
+AQS 中对 `waitStatus` 的判断非常精简:
+
+```plain
+waitStatus > 0:节点已取消,需要跳过
+waitStatus < 0:节点处于有效的信号、条件或传播状态
+waitStatus = 0:普通初始状态
+```
+
+这种设计让很多分支只需要判断正负,而不必逐个比较状态常量。
+
+
+
+# AQS 同步队列为什么是 CLH 变体
+AQS 的同步队列通常被称为 CLH 队列,但它并不是原始的 CLH 自旋锁队列。
+
+原始 CLH 队列的核心思想是:每个线程关注自己的前驱节点状态。
+
+
+
+AQS 保留了这个思想,但做了改造:
+
+```plain
+原始 CLH:线程主要自旋等待
+AQS:线程尝试几次后,通过 LockSupport.park 阻塞
+
+原始 CLH:通常只需要前驱引用
+AQS:增加 prev 和 next,便于取消、唤醒和队列维护
+```
+
+
+
+AQS 节点主要观察前驱节点:
+
+```plain
+前驱是 head:尝试获取资源
+前驱为 SIGNAL:当前线程可以安全 park
+前驱已取消:跳过取消节点,重新连接有效前驱
+```
+
+因此,AQS 同步队列可以理解为:基于 CLH 思想改造的 FIFO 双向阻塞队列
+
+
+
+# AQS 留给子类实现的五个方法
+AQS 将排队和阻塞机制实现好,但资源获取规则需要子类定义。
+
+核心扩展方法如下:
+
+```java
+protected boolean tryAcquire(int arg)
+protected boolean tryRelease(int arg)
+protected int tryAcquireShared(int arg)
+protected boolean tryReleaseShared(int arg)
+protected boolean isHeldExclusively()
+```
+
+默认实现都会抛出UnsupportedOperationException,子类根据需要选择实现独占模式、共享模式或两者。
+
+
+
+# AQS多线程竞争入队流程
+## 独占锁场景
+下面按**独占锁**场景流程,比如ReentrantLock.lock()。
+
+**背景:当前有T0、T1、T2三个线程竞争锁,默认T0已获得锁。**
+
+### 1. T0 已经持有锁
+```plain
+state = 1
+owner = T0
+
+同步队列:空
+head = null
+tail = null
+```
+
+****
+
+### 2. T1 来竞争,tryAcquire 失败,封装成 Node 入队
+AQS 执行:addWaiter(Node.EXCLUSIVE)
+
+
+
+队列第一次初始化,会先创建哨兵 head:
+
+```plain
+head(dummy) <-> T1
+ ^
+ tail
+```
+
+此时节点状态:
+
+```plain
+head.waitStatus = 0
+T1.waitStatus = 0
+```
+
+### 3. T1 入队后马上自旋再次竞争,失败,准备阻塞
+T1 在 acquireQueued 里判断:
+
+```plain
+p == head && tryAcquire(arg)
+```
+
+此时 `p == head`,但锁还被 T0 持有,所以失败。
+
+然后进入:
+
+```plain
+shouldParkAfterFailedAcquire(head, T1)
+```
+
+T1 会把自己的前驱 head 设置成 **SIGNAL状态**:
+
+```plain
+head(SIGNAL) <-> T1(0)
+ ^
+ tail
+```
+
+
+
+然后T1发现前序节点是SIGNAL状态,然后T1调用park()进入阻塞状态。
+
+
+
+### 4. T2 也来竞争,失败后入队
+```plain
+head(SIGNAL) <-> T1(0) <-> T2(0)
+ ^
+ tail
+```
+
+T2 的前驱是 T1。T2 获取失败后,马上自旋再次竞争锁,失败之后会把前驱 T1 设置成 `SIGNAL`:
+
+```plain
+head(SIGNAL) <-> T1(SIGNAL) <-> T2(0)
+ ^
+ tail
+```
+
+然后T2发现前序节点是SIGNAL状态,然后T2调用park()进入阻塞状态。
+
+### 5. T0 释放锁,AQS 从 head 唤醒后继
+T0 调用:
+
+```plain
+unlock()
+ -> release(1)
+ -> tryRelease(1)
+ -> unparkSuccessor(head)
+```
+
+队列:
+
+```plain
+head(SIGNAL) <-> T1(SIGNAL) <-> T2(0)
+```
+
+AQS 找:
+
+```plain
+head.next == T1
+```
+
+然后:
+
+```plain
+LockSupport.unpark(T1.thread)
+```
+
+结果:
+
+```plain
+T1 被唤醒
+T2 仍阻塞
+```
+
+### 6. T1 被唤醒后重新竞争锁,成功
+T1 醒来后回到循环:
+
+```plain
+p == head && tryAcquire(arg)
+```
+
+此时锁已经空了,所以 T1 成功获取锁。
+
+然后:
+
+```plain
+setHead(T1)
+```
+
+队列变成:
+
+```plain
+head = T1
+T1(thread=null, prev=null, SIGNAL) <-> T2(0)
+ ^
+ tail
+```
+
+旧的 dummy head 会断开,帮助 GC:
+
+```plain
+T1 成为新的 head
+```
+
+注意:
+
+```plain
+T1 成为 head 后,它本身不再代表一个等待线程;
+它代表当前成功获取过锁的节点。
+```
+
+### 7. T1 释放锁,再唤醒 T2
+T1 执行:
+
+```plain
+unlock()
+ -> release(1)
+ -> unparkSuccessor(head)
+```
+
+此时:
+
+```plain
+head(T1, SIGNAL) <-> T2(0)
+```
+
+AQS 唤醒:
+
+```plain
+head.next == T2
+```
+
+T2 醒来,重新执行:
+
+```plain
+p == head && tryAcquire(arg)
+```
+
+成功后:
+
+```plain
+setHead(T2)
+```
+
+队列变成:
+
+```plain
+head = T2
+tail = T2
+```
+
+如果后面没有等待节点:
+
+```plain
+T2.waitStatus 通常是 0
+```
+
+# AQS核心方法
+## acquire()
+当某个线程发起锁获取,比如调用ReentrantLock.lock()方法时,调用链会走到AbstractQueuedSynchronizer.acquire()方法,源码如下:
+
+```java
+public final void acquire(int arg) {
+if (!tryAcquire(arg) &&
+ acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
+ selfInterrupt();
+}
+```
+
+## nonfairTryAcquire()
+tryAcquire()逻辑在AQS子类里,当前就是在ReentrantLock中,核心逻辑:
+
+```java
+final boolean nonfairTryAcquire(int acquires) {
+ final Thread current = Thread.currentThread();
+ // 获取AQS状态
+ int c = getState();
+ // 没有线程持有这个共享状态
+ if (c == 0) {
+ // CAS 变更state状态
+ if (compareAndSetState(0, acquires)) {
+ // 标识当前线程持有state
+ setExclusiveOwnerThread(current);
+ return true;
+ }
+ }
+ // 如果是持有锁的线程
+ else if (current == getExclusiveOwnerThread()) {
+ // state加一,重复持有锁
+ int nextc = c + acquires;
+ if (nextc < 0) // overflow
+ throw new Error("Maximum lock count exceeded");
+ setState(nextc);
+ return true;
+ }
+ return false;
+}
+```
+
+nonfairTryAcquire()方法比较简单,主要就是通过CAS变更state状态,然后将持有锁的线程标识为owner,后续竞争锁则判断该线程是否持有,否则加锁失败返回false。
+
+
+
+## addWaiter()
+```java
+ private Node addWaiter(Node mode) {
+ Node node = new Node(Thread.currentThread(), mode);
+ // 尝试一次快速入队
+ Node pred = tail;
+ // 如果pred=null,则表示队列还未初始化,快速入队逻辑不负责初始化
+ if (pred != null) {
+ node.prev = pred;
+ // 并发竞争失败,会走到兜底入队逻辑
+ if (compareAndSetTail(pred, node)) {
+ pred.next = node;
+ return node;
+ }
+ }
+ // 兜底入队逻辑
+ enq(node);
+ return node;
+ }
+```
+
+addWaiter() 先用一次 CAS 快速入队;如果队列未初始化或发生并发竞争,就交给 enq() 自旋,确保当前线程节点最终进入 AQS 同步队列。
+
+
+
+enq() 方法负责同步队列的延迟初始化,以及节点竞争入队。当队列未初始化时,通过 CAS 创建哨兵头节点;当队列已经初始化时,通过 CAS 将当前节点设置为新的尾节点。如果 CAS 失败,则不断自旋重试,直到节点成功入队。
+
+```java
+ private Node enq(final Node node) {
+ for (;;) {
+ Node t = tail;
+ // 队列初始化
+ if (t == null) { // Must initialize
+ if (compareAndSetHead(new Node()))
+ tail = head;
+ } else {
+ node.prev = t;
+ // CAS往队列尾部添加节点
+ if (compareAndSetTail(t, node)) {
+ t.next = node;
+ // !返回的是入队节点的前序节点
+ return t;
+ }
+ }
+ }
+ }
+```
+
+## acquireQueued()
+
+
+```java
+/**
+ * 已入队节点以独占模式获取同步状态。
+ *
+ * @return 等待过程中是否发生过中断
+ */
+final boolean acquireQueued(final Node node, int arg) {
+ boolean failed = true;
+ try {
+ boolean interrupted = false;
+
+ // 自旋,直到成功获取同步状态
+ for (;;) {
+ final Node p = node.predecessor();
+
+ // 只有头节点的直接后继才有资格尝试获取同步状态
+ if (p == head && tryAcquire(arg)) {
+ // 获取成功,当前节点成为新的头节点
+ setHead(node);
+ p.next = null; // 断开旧头节点,帮助 GC
+ failed = false;
+ return interrupted;
+ }
+
+ // 获取失败,判断是否需要阻塞;被唤醒后检查中断状态
+ if (shouldParkAfterFailedAcquire(p, node) &&
+ parkAndCheckInterrupt()) {
+ interrupted = true;
+ }
+ }
+ } finally {
+ // 出现异常等获取失败的情况时,取消当前节点
+ if (failed) {
+ cancelAcquire(node);
+ }
+ }
+}
+```
+
+shouldParkAfterFailedAcquire()方法核心就是将入参node的前驱节点状态置为:SIGNAL,然后方法返回false,继续执行后面的parkAndCheckInterrupt(),将node节点置为阻塞状态。
+
+
+
+这里就是对应着有新线程竞争锁失败之后,先加入队列,之后自旋尝试再次竞争锁,失败了则将前序节点置为SIGNAL,然后将自己阻塞等待被唤醒。
+
+
+
+下面再分析下释放锁的流程,释放锁核心会调用AQS的release()方法。
+
+```java
+public final boolean release(int arg) {
+ // 子类负责更新锁状态,并判断锁是否已完全释放
+ if (tryRelease(arg)) {
+ Node h = head;
+
+ // 头节点存在且后继节点需要唤醒
+ if (h != null && h.waitStatus != 0)
+ unparkSuccessor(h);
+
+ return true;
+ }
+
+ // 重入次数尚未归零,本次只减少持锁次数
+ return false;
+}
+```
+
+
+
+tryRelease()方法在ReentrantLock中,源码如下:
+
+```java
+protected final boolean tryRelease(int releases) {
+ // 减少重入计数
+ int c = getState() - releases;
+
+ // 只有锁的持有线程才能释放锁
+ if (Thread.currentThread() != getExclusiveOwnerThread())
+ throw new IllegalMonitorStateException();
+
+ boolean free = false;
+
+ // 重入计数归零,锁才算完全释放
+ if (c == 0) {
+ free = true;
+ setExclusiveOwnerThread(null);
+ }
+
+ // 更新剩余重入次数
+ setState(c);
+ return free;
+}
+```
+
+
+
+AQS的unparkSuccessor()源码如下:
+
+```java
+/**
+ * 唤醒等待队列中有效的后继节点。
+ */
+private void unparkSuccessor(Node node) {
+ int ws = node.waitStatus;
+
+ // 清除当前节点的待唤醒标记
+ if (ws < 0)
+ compareAndSetWaitStatus(node, ws, 0);
+
+ Node s = node.next;
+
+ // next 无效时,从队尾反向查找最靠前的有效等待节点
+ if (s == null || s.waitStatus > 0) {
+ s = null;
+ for (Node t = tail; t != null && t != node; t = t.prev) {
+ // waitStatus > 0 表示节点已经取消
+ if (t.waitStatus <= 0)
+ s = t;
+ }
+ }
+
+ // 唤醒目标线程,让其重新参与锁竞争
+ if (s != null)
+ LockSupport.unpark(s.thread);
+}
+```
+
+
+
+释放锁流程总结:
+
+1. 当前线程调用 unlock(),最终进入 AQS 的 release(1)。
+2. release() 调用 tryRelease(1),减少 state 表示的重入次数。
+3. 如果当前线程不是锁的持有者,抛出 IllegalMonitorStateException。
+4. 如果 state 仍大于 0,说明当前线程还持有重入锁,不唤醒其他线程。
+5. 如果 state 减少到 0,清空锁的持有线程,表示锁已完全释放。
+6. release() 检查等待队列,通过 unparkSuccessor() 找到有效的后继节点。
+7. 调用 LockSupport.unpark() 唤醒对应线程,使其重新尝试获取锁。
+
+
+
+需要注意:unpark() 只是让等待线程具备继续运行的条件,并不代表它立刻获得锁。线程被唤醒后,仍然需要参与锁竞争。
+
From bfa742da0cf74f902fb2d8e3046181f4a109e416 Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Sat, 11 Jul 2026 10:52:38 +0800
Subject: [PATCH 26/37] =?UTF-8?q?=E6=9B=B4=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 158573c..05b1c54 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,7 @@ Kafka / RocketMQ 消息系统源码
- [深入学习HashMap 底层源码与原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/HashMap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md)
- [开源项目里那些看不懂的位运算分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E9%87%8C%E9%82%A3%E4%BA%9B%E7%9C%8B%E4%B8%8D%E6%87%82%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%E5%88%86%E6%9E%90.md)
- [ThreadPoolExecutor 源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90ThreadPoolExecutor%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md)
+- [AQS 源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/AQS%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md)
HashMap原理图

From 623f4101ac7c0817163af21aac5836e8655c35c0 Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Sun, 12 Jul 2026 08:54:05 +0800
Subject: [PATCH 27/37] =?UTF-8?q?=E9=87=8D=E5=B9=B3=E8=A1=A1=E4=B8=A5?=
=?UTF-8?q?=E6=A0=BC=E5=8C=BA=E5=88=86=E4=B8=8D=E5=90=8C=E5=8D=8F=E8=AE=AE?=
=?UTF-8?q?=E7=89=88=E6=9C=AC:=20Classic=20Consumer=20Group=20Protocol?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 05b1c54..41a5b54 100644
--- a/README.md
+++ b/README.md
@@ -73,13 +73,13 @@ Kafka / RocketMQ 消息系统源码
- [Kafka Broker 核心源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md)
- [Kafka Broker 源码分析:生产者篇](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E2%80%94%E2%80%94%E7%94%9F%E4%BA%A7%E8%80%85%E7%AF%87.md)
- [Kafka 消费者核心源码分析(一)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%E6%B6%88%E8%B4%B9%E8%80%85%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%80%EF%BC%89.md)
-- [Kafka Rebalance 核心逻辑分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20rebalance%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91%E5%88%86%E6%9E%90.md)
+- [Kafka Rebalance 核心逻辑分析(Classic Consumer Group Protocol)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20Rebalance%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91%E5%88%86%E6%9E%90.md)
- [Kafka ISR 原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%20ISR%20%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md)
Kafka架构图

-Kafka Rebalance流程图
+Kafka Rebalance流程图(Classic Consumer Group Protocol 经典消费者组协议)


From 39b2a718a77226f0962adf897f8bb366c03c531e Mon Sep 17 00:00:00 2001
From: CoderBruis <37364336+coderbruis@users.noreply.github.com>
Date: Mon, 13 Jul 2026 15:17:49 +0800
Subject: [PATCH 28/37] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=94=99=E5=88=AB?=
=?UTF-8?q?=E5=AD=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...tor\345\272\225\345\261\202\345\216\237\347\220\206.md" | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git "a/note/JDK/\346\267\261\345\205\245\350\247\243\346\236\220ThreadPoolExecutor\345\272\225\345\261\202\345\216\237\347\220\206.md" "b/note/JDK/\346\267\261\345\205\245\350\247\243\346\236\220ThreadPoolExecutor\345\272\225\345\261\202\345\216\237\347\220\206.md"
index ba9b143..8f55d40 100644
--- "a/note/JDK/\346\267\261\345\205\245\350\247\243\346\236\220ThreadPoolExecutor\345\272\225\345\261\202\345\216\237\347\220\206.md"
+++ "b/note/JDK/\346\267\261\345\205\245\350\247\243\346\236\220ThreadPoolExecutor\345\272\225\345\261\202\345\216\237\347\220\206.md"
@@ -11,7 +11,7 @@ private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
-在不同操作系统下,Java 中的 Integer 变量都是32位,ThreadPoolExecutor 使用前3位(31~29)表示线程池状态,用后29位(28~0)表示活跃线程数。
+在不同操作系统下,Java 中的 Integer 变量都是32位,ThreadPoolExecutor 使用前3位(31 ~ 29)表示线程池状态,用后29位(28 ~ 0)表示活跃线程数。
## COUNT_BITS
COUNT_BITS的作用:用来划分 ctl 这个 int 变量的高低位边界。
@@ -22,7 +22,7 @@ private static final int COUNT_BITS = Integer.SIZE - 3; // 29
-int的最大位值是32位,32 - 3 = 29。通过这29来控制高3位存线程池状态**(注意这里不是线程,是线程池)**,低29位表示线程池中worker数量。通过位移来实现高效操作。
+int的最大位值是32位,32 - 3 = 29。通过这29来控制高3位存线程池状态(注意这里不是线程,是线程池),低29位表示线程池中worker数量。通过位移来实现高效操作。
## CAPACITY
CAPACITY的作用:用来表示 workerCount 线程数量部分的最大容量,同时也作为低 29 位的**掩码**。
@@ -40,11 +40,10 @@ private static final int CAPACITY = (1 << COUNT_BITS) - 1;
2)对左移结果 - 1
-(1 << COUNT_BITS) - 1:就是00100000 00000000 00000000 00000000 - 1,对于二进制,如果高位减1,会借位到对应位置-1。举例如下:
+(1 << COUNT_BITS) - 1:就是00100000 00000000 00000000 00000000 - 1,对于二进制,如果高位减1,会借位到对应位置减1。举例如下:
+ 01000 - 1 = 01000 - 01111 = 00111
+ 00100000 00000000 00000000 00000000 - 1 = 00100000 00000000 00000000 00000000 - 00111111 11111111 11111111 11111111 = 00011111 11111111 11111111 11111111
-+ 口诀:对于这种数:00100000 00000000 00000000 00000000,它是“某一位是 1,右边全是 0”。
### 【扩展】掩码
掩码 Mask,就是一串二进制位,用来通过位运算“筛选”出你想要的部分。在ThreadPoolExecutor里:
From 17d5b480db8c127ed08fb9227f7ab0d9277b247b Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Wed, 15 Jul 2026 20:25:47 +0800
Subject: [PATCH 29/37] =?UTF-8?q?=E6=96=B0=E5=A2=9ENetty=E6=9E=B6=E6=9E=84?=
=?UTF-8?q?=E5=9B=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 41a5b54..5f8a663 100644
--- a/README.md
+++ b/README.md
@@ -160,7 +160,7 @@ Kafka / RocketMQ 消息系统源码
Netty 底层源码解析
-- Netty 版本:4.1.43.Final
+- Netty 版本:4.2
- [Netty 概念扫盲](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E6%A6%82%E5%BF%B5%E6%89%AB%E7%9B%B2.md)
- [二进制运算以及源码、反码以及补码学习](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/%E4%BA%8C%E8%BF%9B%E5%88%B6.md)
- [Netty 源码包结构](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/Netty%E6%BA%90%E7%A0%81%E5%8C%85%E7%BB%93%E6%9E%84.md)
@@ -178,6 +178,12 @@ Kafka / RocketMQ 消息系统源码
Netty 实战课相关代码位于 `Spring-Netty` 模块下的 `com/bruis/learnnetty/im` 包。
+Netty架构图
+
+
+Netty主从Reactor架构图
+
+
From 59e16a86a1e61d46b1d8548d11e542f88bfdb79e Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Thu, 16 Jul 2026 13:58:31 +0800
Subject: [PATCH 30/37] =?UTF-8?q?=E6=9B=B4=E6=96=B0ThreadLocal=E5=86=85?=
=?UTF-8?q?=E5=AD=98=E6=B3=84=E6=BC=8F=E4=BC=AA=E4=BB=A3=E7=A0=81=E5=88=86?=
=?UTF-8?q?=E6=9E=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...245\345\210\206\346\236\220ThreadLocal.md" | 142 +++++++++++++++++-
1 file changed, 138 insertions(+), 4 deletions(-)
diff --git "a/note/JDK/\346\267\261\345\205\245\345\210\206\346\236\220ThreadLocal.md" "b/note/JDK/\346\267\261\345\205\245\345\210\206\346\236\220ThreadLocal.md"
index ff6c108..44f8c01 100644
--- "a/note/JDK/\346\267\261\345\205\245\345\210\206\346\236\220ThreadLocal.md"
+++ "b/note/JDK/\346\267\261\345\205\245\345\210\206\346\236\220ThreadLocal.md"
@@ -150,7 +150,7 @@ Entry是ThreadLocalMap数组中的核心元素,它继承了WeakReference。核
Thread -> ThreadLocalMap -> Entry -> ThreadLocal
```
-在线程池场景下,线程可能长期存活。只要线程不结束,ThreadLocalMap 就还在,Entry 也还在,那么 ThreadLocal 对象就永远无法被 GC。即使业务代码已经不再持有这个 ThreadLocal 变量了,它也会被 Entry 强行引用住。这会导致:**ThreadLocal 对象无法回收,对应的 value 也无法回收。**
+在线程池场景下,线程可能长期存活。只要线程不结束,ThreadLocalMap 就还在,Entry 也还在,那么 ThreadLocal 对象就永远无法被 GC。即使业务代码已经不再持有这个 ThreadLocal 变量了,它也会被 Entry 强行引用住。这会导致:**ThreadLocal 对象无法回收,对应的 value 也无法回收。**
这个后果就是线程长期持有已经没用的 ThreadLocal 和 value,导致内存释放不了,严重时内存泄漏、数据串用、甚至 OOM。
@@ -516,7 +516,9 @@ new Thread(() -> {
}).start();
```
- 而 InheritableThreadLocal 可以让子线程继承父线程的值:
+```plain
+而 InheritableThreadLocal 可以让子线程继承父线程的值:
+```
```java
InheritableThreadLocal local = new InheritableThreadLocal<>();
@@ -576,7 +578,7 @@ private ThreadLocalMap(ThreadLocalMap parentMap) {
-但是现在基本都没有直接通过new Thread()的方式创建线程了,基本都是通过线程池来管理线程。而在常规业务线程池里,InheritableThreadLocal 基本不适合作为上下文传递方案。它的继承时机是**“创建线程时”**,而线程池的线程通常早就创建好了,任务提交时不会重新继承父线程上下文。
+但是现在基本都没有直接通过new Thread()的方式创建线程了,基本都是通过线程池来管理线程。而在常规业务线程池里,InheritableThreadLocal 基本不适合作为上下文传递方案。它的继承时机是**“创建线程时”**,而线程池的线程通常早就创建好了,任务提交时不会重新继承父线程上下文。
线程池上下文传递方案,用的最多的就是阿里的TransmittableThreadLocal,简称 TTL。
@@ -620,7 +622,139 @@ class TtlRunnable implements Runnable {
}
```
-
+
总结:TransmittableThreadLocal在线程池里传值,是通过包装任务,在任务提交时捕获父线程的 TTL 快照,在工作线程执行前恢复这份快照,执行结束后再还原工作线程原上下文来实现的。
+# ThreadLocal内存泄漏代码分析
+下面是一段ThreadLocal内存泄漏的伪代码,通过这段伪代码加深ThreadLocal底层原理的理解。
+
+```java
+static final ThreadLocal USER_CONTEXT = new ThreadLocal<>();
+
+void handleRequest(Request request) {
+ UserInfo userInfo = getUserInfo(request);
+ USER_CONTEXT.set(userInfo);
+
+ doBusiness();
+
+ // 忘记执行 USER_CONTEXT.remove()
+}
+```
+
+上述这段代码是用户登录之后,获取用户信息并存到ThreadLocal中,但是方法结束后并未执行:USER_CONTEXT.remove()移除ThreadLocal中的用户信息,这会造成内存泄漏,下面通过引用链来分析下内存泄漏的原因。
+
+方法在USER_CONTEXT.set(userInfo)之后,引用关系如下图:
+
+```java
+GC Roots
+│
+├── ClassLoader
+│ │ 强引用
+│ ▼
+│ Class对象
+│ │ 静态字段强引用
+│ ▼
+│ USER_CONTEXT
+│ │ 强引用
+│ ▼
+│ ThreadLocal对象 ◀-------------------┐
+│ │
+└── 工作线程 Thread │
+ │ │
+ ├── 线程栈 │
+ │ │ │
+ │ ▼ │
+ │ handleRequest()栈帧 │
+ │ │ │
+ │ ▼ │
+ │ 局部变量 userInfo │
+ │ │ 强引用 │
+ │ ▼ │
+ │ UserInfo对象 ◀──────────┐ │
+ │ │ │
+ └── threadLocals │ │
+ │ 强引用 │ │
+ ▼ │ │
+ ThreadLocalMap │ │
+ │ 强引用 │ │
+ ▼ │ │
+ Entry │ │
+ ├── value强引用 ─────┘ │
+ │ │
+ └---- key弱引用 -----------┘
+```
+
+这里有个细节,调用了USER_CONTEXT.set(userInfo)之后,ThreadLocal对象被USER_CONTEXT强引用引用,同时还被Entry这个弱引用给引用了。
+
+
+
+当handleRequest()方法调用完之后,userInfo局部变量消失,此时引用关系图如下
+
+```java
+GC Roots
+│
+├── ClassLoader
+│ │ 强引用
+│ ▼
+│ Class对象
+│ │ 静态字段强引用
+│ ▼
+│ USER_CONTEXT
+│ │ 强引用
+│ ▼
+│ ThreadLocal对象 ◀--------------------┐
+│ │
+└── 工作线程 Thread │
+ │ │
+ ├── 线程栈 │
+ │ │ │
+ │ └── handleRequest栈帧已消失 │
+ │ │
+ └── threadLocals │
+ │ 强引用 │
+ ▼ │
+ ThreadLocalMap │
+ │ 强引用 │
+ ▼ │
+ Entry │
+ ├---- key弱引用 -----------┘
+ │
+ └── value强引用
+ │
+ ▼
+ UserInfo对象
+```
+
+此时发生变化的是,局部变量对应的引用已经断开。
+
+```java
+局部变量userInfo ──X──> UserInfo对象
+```
+
+
+
+因此,即使局部变量消失,`UserInfo` 仍然无法被 GC 回收。只有执行 `USER_CONTEXT.remove()` 清理对应 Entry,才能断开这条引用链。
+
+
+
+所以正确的写法如下
+
+```java
+private static final ThreadLocal USER_CONTEXT =
+ new ThreadLocal<>();
+
+public void handleRequest(Request request) {
+ try {
+ UserInfo userInfo = getUserInfo(request);
+ USER_CONTEXT.set(userInfo);
+
+ // 处理具体业务
+ doBusiness();
+ } finally {
+ // 无论正常结束还是发生异常,都必须清理
+ USER_CONTEXT.remove();
+ }
+}
+```
+
From 7cf7a1b788b793999baa86c4734b2230f48aa3d5 Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Fri, 17 Jul 2026 22:11:02 +0800
Subject: [PATCH 31/37] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=A9=BA=E6=96=87?=
=?UTF-8?q?=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...15\350\257\225\346\234\272\345\210\266.md" | 0
...23\345\255\230\346\234\272\345\210\266.md" | 0
...20\347\240\201\346\220\255\345\273\272.md" | 52 -------------------
...bo\351\235\242\350\257\225\351\242\230.md" | 22 --------
...05\350\257\273\351\241\272\345\272\217.md" | 11 ----
5 files changed, 85 deletions(-)
delete mode 100644 "note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\224\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\351\207\215\350\257\225\346\234\272\345\210\266.md"
delete mode 100644 "note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\347\274\223\345\255\230\346\234\272\345\210\266.md"
delete mode 100644 "note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md"
delete mode 100644 "note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md"
delete mode 100644 "note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md"
diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\224\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\351\207\215\350\257\225\346\234\272\345\210\266.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\224\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\351\207\215\350\257\225\346\234\272\345\210\266.md"
deleted file mode 100644
index e69de29..0000000
diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\347\274\223\345\255\230\346\234\272\345\210\266.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\347\274\223\345\255\230\346\234\272\345\210\266.md"
deleted file mode 100644
index e69de29..0000000
diff --git "a/note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md" "b/note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md"
deleted file mode 100644
index 70b098d..0000000
--- "a/note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md"
+++ /dev/null
@@ -1,52 +0,0 @@
-## 前言
-
-想要深入学习Dubbo,最好的方式就是阅读并调用Dubbo源码,接下来先来动手搭建一个Dubbo源码环境。
-
-## 正文
-
-### 1. 下载源码
-
-步骤:
-
-1. 先从dubbo源码官网github中fork一份到自己的github仓库中。
-
- ```
- git clone git@github.com:xxxxxxxx/dubbo.git
- ```
-
-2. 使用命令:git branch v2.7.8。 切换到分支2.7.8。
-
- ```
- git checkout -b dubbo-2.7.8 dubbo-2.7.8
- ```
-
-3. 导入方式(IDEA导入方式)
-
- 可以通过IDEA ——> File ——> Open ——> pom.xml ——> open as project
-
- 然后让IDEA下载相关的依赖,等下载完成即可。
-
-4. mvn命令导入
-
- ```
- mvn clean install -Dmaven.test.skip=true
- ```
-
- 然后执行下面的命令转换成 IDEA 项目:
-
- ```
- mvn idea:idea
- ```
-
- 如果执行报错了,则执行:
- ```
- mvn idea:workspace
- ```
-
-### 2. 分支切换
-
-本人Fork了官方Dubbo源码到本地仓库,并且新建了一个分支名为:SourceCode-of-Translation
-
-该分支主要用于进行源码注释,每个核心功能代码都有详细注释,欢迎大家Fork到本地,然后进行注释查看。
-
-> 地址为:[SourceCode-of-Translation](https://github.com/coderbruis/dubbo) 下载到本地后,只需要切换到SourceCode-of-Translation分支即可。
diff --git "a/note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" "b/note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md"
deleted file mode 100644
index a8a3d5b..0000000
--- "a/note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md"
+++ /dev/null
@@ -1,22 +0,0 @@
-## 前言
-
-Dubbo面试题
-
-## 正文
-
-RPC非常重要,很多人面试的时候都挂在了这个地方!你要是还不懂RPC是什么?他的基本原理是什么?你一定要把下边的内容记起来!好好研究一下!特别是文中给出的一张关于RPC的基本流程图,重点中的重点,Dubbo RPC的基本执行流程就是他,RPC框架的基本原理也是他,别说我没告诉你!看了下边的内容你要掌握的内容如下,当然还有很多:
-
-1. RPC的由来,是怎么一步一步演进出来的;
-2. RPC的基本架构是什么;
-3. RPC的基本实现原理, 重点;
-4. REST和SOAP、PRC的区别;
-5. 整个调用的过程经历了哪几部?和SpringMVC流程区别?
-
-
-### 1. 为什么要有RPC
-
-随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。
-
-- [PRC原理图](https://github.com/coderbruis/JavaSourceLearning/blob/master/images/PRC/rpc.jpg)
-
-
diff --git "a/note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md" "b/note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md"
deleted file mode 100644
index 18929b7..0000000
--- "a/note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md"
+++ /dev/null
@@ -1,11 +0,0 @@
-## 前言
-
-在真正深入Dubbo底层源码前,先梳理好阅读路线,这样才能够事半功倍。
-
-## 正文
-
-### 1. 配置加载流程?
-
-[配置加载流程](http://dubbo.apache.org/zh-cn/docs/user/configuration/configuration-load-process.html)
-
-### 2.
\ No newline at end of file
From a7f505d5bbcb7f19b523626c40a2e71a7a129e67 Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Sat, 18 Jul 2026 22:03:26 +0800
Subject: [PATCH 32/37] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9E=B6=E6=9E=84?=
=?UTF-8?q?=E6=96=B9=E6=A1=88=E4=BC=AA=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
architecture/README.md | 46 +++++
architecture/pom.xml | 78 ++++++++
.../arch/policy/ActiveSnapshotRegistry.java | 61 +++++++
.../arch/policy/DefaultSnapshotValidator.java | 15 ++
.../policy/FileSystemSnapshotDirectory.java | 45 +++++
.../java/com/arch/policy/MessagePosition.java | 20 +++
.../java/com/arch/policy/PolicyChange.java | 30 ++++
.../java/com/arch/policy/PolicyRecord.java | 27 +++
.../arch/policy/PolicySearchApplication.java | 15 ++
.../java/com/arch/policy/PolicySnapshot.java | 170 ++++++++++++++++++
.../arch/policy/PolicySnapshotService.java | 80 +++++++++
.../java/com/arch/policy/SnapshotBuilder.java | 56 ++++++
.../java/com/arch/policy/SnapshotPorts.java | 28 +++
.../aggregation/AsyncSearchCoordinator.java | 76 ++++++++
.../aggregation/LocalSearchWaiters.java | 32 ++++
.../arch/policy/aggregation/SearchState.java | 3 +
.../policy/aggregation/SearchStateStore.java | 13 ++
.../aggregation/SupplierCallbackService.java | 38 ++++
.../aggregation/SupplierTaskDispatcher.java | 9 +
.../com/arch/policy/api/CallbackResponse.java | 10 ++
.../arch/policy/api/PolicySearchRequest.java | 22 +++
.../arch/policy/api/PolicySearchResponse.java | 22 +++
.../policy/api/PolicySearchRpcService.java | 7 +
.../policy/api/SupplierCallbackRequest.java | 25 +++
.../api/SupplierCallbackRpcService.java | 5 +
.../application/PolicyIncrementalUpdater.java | 16 ++
.../application/PolicyStartupRunner.java | 19 ++
.../config/PolicySearchConfiguration.java | 109 +++++++++++
.../demo/DemoSupplierTaskDispatcher.java | 47 +++++
.../kafka/KafkaPolicyChangeListener.java | 45 +++++
.../policy/kafka/PolicyChangeMessage.java | 23 +++
.../policy/redis/RedisSearchStateStore.java | 84 +++++++++
.../redis/SearchFinishedSubscriber.java | 15 ++
.../policy/rpc/DubboPolicySearchService.java | 34 ++++
.../rpc/DubboSupplierCallbackService.java | 23 +++
.../src/main/resources/application.yml | 32 ++++
.../policy/AsyncSearchCoordinatorTest.java | 99 ++++++++++
.../arch/policy/SnapshotLifecycleTest.java | 119 ++++++++++++
38 files changed, 1598 insertions(+)
create mode 100644 architecture/README.md
create mode 100644 architecture/pom.xml
create mode 100644 architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java
create mode 100644 architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java
create mode 100644 architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java
create mode 100644 architecture/src/main/java/com/arch/policy/MessagePosition.java
create mode 100644 architecture/src/main/java/com/arch/policy/PolicyChange.java
create mode 100644 architecture/src/main/java/com/arch/policy/PolicyRecord.java
create mode 100644 architecture/src/main/java/com/arch/policy/PolicySearchApplication.java
create mode 100644 architecture/src/main/java/com/arch/policy/PolicySnapshot.java
create mode 100644 architecture/src/main/java/com/arch/policy/PolicySnapshotService.java
create mode 100644 architecture/src/main/java/com/arch/policy/SnapshotBuilder.java
create mode 100644 architecture/src/main/java/com/arch/policy/SnapshotPorts.java
create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java
create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java
create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/SearchState.java
create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java
create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java
create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java
create mode 100644 architecture/src/main/java/com/arch/policy/api/CallbackResponse.java
create mode 100644 architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java
create mode 100644 architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java
create mode 100644 architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java
create mode 100644 architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java
create mode 100644 architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java
create mode 100644 architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java
create mode 100644 architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java
create mode 100644 architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java
create mode 100644 architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java
create mode 100644 architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java
create mode 100644 architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java
create mode 100644 architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java
create mode 100644 architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java
create mode 100644 architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java
create mode 100644 architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java
create mode 100644 architecture/src/main/resources/application.yml
create mode 100644 architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java
create mode 100644 architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java
diff --git a/architecture/README.md b/architecture/README.md
new file mode 100644
index 0000000..ed7e14c
--- /dev/null
+++ b/architecture/README.md
@@ -0,0 +1,46 @@
+# Policy snapshot lifecycle
+
+This module implements version-isolated policy snapshots. Each snapshot owns a RocksDB detail
+store, a bitmap index, and the incremental-message position at which both were built.
+
+## Integration flow
+
+1. Implement `FullPolicyLoader` to stream the full-data cut into `PolicySnapshot.upsert` and
+ return the cut's message position.
+2. Implement `IncrementalReplayer` to apply upserts/deletes after that position. The builder
+ repeats replay until it reaches a stable latest position.
+3. Supply `DefaultSnapshotValidator` (or a stricter domain validator), a
+ `FileSystemSnapshotDirectory`, and construct `SnapshotBuilder`.
+4. Call `PolicySnapshotService.start(version)` during startup. Keep the application's readiness
+ probe bound to `service.isReady()`; failures remain unready and retry.
+5. Call `refresh(newVersion)` at runtime. A failed candidate is discarded. A valid candidate is
+ atomically activated while the old snapshot continues serving existing leases.
+6. Every search must use `try (SnapshotLease lease = registry.acquire())`. Releasing the last old
+ lease closes RocksDB and deletes that retired version's directory.
+
+The module intentionally leaves message-broker and full-data-source clients behind interfaces so
+the snapshot consistency rules are independent of Kafka, HTTP, database, or framework choices.
+
+## Package layout
+
+- `api`: serializable Dubbo contract and request/response DTOs.
+- `rpc`: Dubbo search provider and supplier callback provider.
+- `aggregation`: Redis-backed fan-out/fan-in search coordination and local waiters.
+- `redis`: atomic Lua state transitions and Pub/Sub early wake-up.
+- `demo`: asynchronous downstream supplier simulation.
+- `kafka`: JSON policy-change consumer and message DTO.
+- `application`: startup and incremental-update use cases.
+- root `policy` package: versioned RocksDB/Bitmap snapshot domain and lifecycle.
+
+Kafka messages use a globally monotonic `position` so duplicate/out-of-order delivery is ignored.
+If the topic has multiple partitions, the producer must supply this global sequence; otherwise the
+position model should be replaced with a per-partition offset map.
+
+## Async supplier search
+
+`asyncSearch` initializes pending suppliers, state and result TTL atomically in Redis, registers a
+local waiter, double-checks Redis, then dispatches all supplier tasks. Supplier callbacks append
+result chunks and remove a supplier from the pending set only on its final callback. The Lua script
+sets `COMPLETED` and publishes `search-finished` when the last supplier finishes. Pub/Sub only wakes
+the local waiter early; Redis remains the source of truth and is checked every 200 ms. Timeout is
+also a Lua state transition and returns all partial results already recorded.
diff --git a/architecture/pom.xml b/architecture/pom.xml
new file mode 100644
index 0000000..4f7f396
--- /dev/null
+++ b/architecture/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+ com.arch
+ architecture
+ 1.0-SNAPSHOT
+
+
+ 8
+ 8
+ UTF-8
+ 5.10.2
+ 2.7.18
+ 3.2.15
+
+
+
+
+ org.rocksdb
+ rocksdbjni
+ 8.11.3
+
+
+ org.roaringbitmap
+ RoaringBitmap
+ 0.9.47
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter
+ ${spring-boot.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+ ${spring-boot.version}
+
+
+ org.springframework.kafka
+ spring-kafka
+ 2.9.13
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.13.5
+
+
+ org.apache.dubbo
+ dubbo-spring-boot-starter
+ ${dubbo.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version}
+
+
+
+
diff --git a/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java b/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java
new file mode 100644
index 0000000..f086e2c
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java
@@ -0,0 +1,61 @@
+package com.arch.policy;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Atomic activation plus draining of queries that still hold the old version. */
+public final class ActiveSnapshotRegistry implements AutoCloseable {
+ private Entry active;
+
+ public synchronized boolean isReady() { return active != null; }
+
+ public synchronized SnapshotLease acquire() {
+ if (active == null) throw new IllegalStateException("policy snapshot is not ready");
+ active.references++;
+ return new SnapshotLease(active);
+ }
+
+ public synchronized void activate(PolicySnapshot snapshot) {
+ Entry previous = active;
+ active = new Entry(snapshot);
+ if (previous != null) retire(previous);
+ }
+
+ @Override public synchronized void close() {
+ Entry previous = active;
+ active = null;
+ if (previous != null) retire(previous);
+ }
+
+ private void release(Entry entry) {
+ synchronized (this) {
+ entry.references--;
+ closeWhenDrained(entry);
+ }
+ }
+
+ private void retire(Entry entry) {
+ entry.retired = true;
+ closeWhenDrained(entry);
+ }
+
+ private void closeWhenDrained(Entry entry) {
+ if (entry.retired && entry.references == 0) entry.snapshot.closeAndDelete();
+ }
+
+ private static final class Entry {
+ private final PolicySnapshot snapshot;
+ private int references;
+ private boolean retired;
+ private Entry(PolicySnapshot snapshot) { this.snapshot = snapshot; }
+ }
+
+ public final class SnapshotLease implements AutoCloseable {
+ private final Entry entry;
+ private final AtomicBoolean released = new AtomicBoolean();
+ private SnapshotLease(Entry entry) { this.entry = entry; }
+ public PolicySnapshot snapshot() { return entry.snapshot; }
+ @Override public void close() {
+ if (released.compareAndSet(false, true)) release(entry);
+ }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java b/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java
new file mode 100644
index 0000000..2c8c848
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java
@@ -0,0 +1,15 @@
+package com.arch.policy;
+
+import static com.arch.policy.SnapshotPorts.SnapshotValidator;
+
+/** Baseline invariants; domain-specific checks can be supplied through SnapshotValidator. */
+public final class DefaultSnapshotValidator implements SnapshotValidator {
+ @Override public void validate(PolicySnapshot candidate, MessagePosition expectedPosition) throws Exception {
+ if (!candidate.getPosition().equals(expectedPosition)) {
+ throw new IllegalStateException("message position mismatch");
+ }
+ if (candidate.policyCount() != candidate.indexedPolicyCount()) {
+ throw new IllegalStateException("RocksDB and bitmap policy counts differ");
+ }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java b/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java
new file mode 100644
index 0000000..64c0bb1
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java
@@ -0,0 +1,45 @@
+package com.arch.policy;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+import static com.arch.policy.SnapshotPorts.SnapshotDirectory;
+
+/** Keeps every version in an isolated directory and removes abandoned candidates. */
+public final class FileSystemSnapshotDirectory implements SnapshotDirectory {
+ private final Path root;
+
+ public FileSystemSnapshotDirectory(Path root) { this.root = root; }
+
+ @Override public Path create(String version) throws IOException {
+ Path directory = root.resolve(safeVersion(version));
+ delete(directory);
+ return Files.createDirectories(directory);
+ }
+
+ @Override public void delete(Path directory) throws IOException {
+ if (!Files.exists(directory)) return;
+ try (Stream paths = Files.walk(directory)) {
+ paths.sorted(Comparator.reverseOrder()).forEach(path -> {
+ try { Files.deleteIfExists(path); }
+ catch (IOException failure) { throw new DeleteFailure(failure); }
+ });
+ } catch (DeleteFailure failure) {
+ throw (IOException) failure.getCause();
+ }
+ }
+
+ private static String safeVersion(String version) {
+ if (version == null || !version.matches("[A-Za-z0-9._-]+")) {
+ throw new IllegalArgumentException("invalid snapshot version: " + version);
+ }
+ return version;
+ }
+
+ private static final class DeleteFailure extends RuntimeException {
+ private DeleteFailure(IOException cause) { super(cause); }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/MessagePosition.java b/architecture/src/main/java/com/arch/policy/MessagePosition.java
new file mode 100644
index 0000000..50794e1
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/MessagePosition.java
@@ -0,0 +1,20 @@
+package com.arch.policy;
+
+public final class MessagePosition implements Comparable {
+ public static final MessagePosition BEGINNING = new MessagePosition(0);
+ private final long value;
+
+ public MessagePosition(long value) {
+ if (value < 0) throw new IllegalArgumentException("position must be non-negative");
+ this.value = value;
+ }
+
+ public long getValue() { return value; }
+
+ @Override public int compareTo(MessagePosition other) { return Long.compare(value, other.value); }
+ @Override public boolean equals(Object other) {
+ return other instanceof MessagePosition && value == ((MessagePosition) other).value;
+ }
+ @Override public int hashCode() { return Long.valueOf(value).hashCode(); }
+ @Override public String toString() { return Long.toString(value); }
+}
diff --git a/architecture/src/main/java/com/arch/policy/PolicyChange.java b/architecture/src/main/java/com/arch/policy/PolicyChange.java
new file mode 100644
index 0000000..5dfcecc
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/PolicyChange.java
@@ -0,0 +1,30 @@
+package com.arch.policy;
+
+public final class PolicyChange {
+ public enum Type { UPSERT, DELETE }
+
+ private final Type type;
+ private final int policyId;
+ private final PolicyRecord policy;
+ private final MessagePosition position;
+
+ private PolicyChange(Type type, int policyId, PolicyRecord policy, MessagePosition position) {
+ this.type = type;
+ this.policyId = policyId;
+ this.policy = policy;
+ this.position = position;
+ }
+
+ public static PolicyChange upsert(PolicyRecord policy, MessagePosition position) {
+ return new PolicyChange(Type.UPSERT, policy.getId(), policy, position);
+ }
+
+ public static PolicyChange delete(int policyId, MessagePosition position) {
+ return new PolicyChange(Type.DELETE, policyId, null, position);
+ }
+
+ public Type getType() { return type; }
+ public int getPolicyId() { return policyId; }
+ public PolicyRecord getPolicy() { return policy; }
+ public MessagePosition getPosition() { return position; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/PolicyRecord.java b/architecture/src/main/java/com/arch/policy/PolicyRecord.java
new file mode 100644
index 0000000..42371e3
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/PolicyRecord.java
@@ -0,0 +1,27 @@
+package com.arch.policy;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public final class PolicyRecord {
+ private final int id;
+ private final byte[] detail;
+ private final Set indexTerms;
+
+ public PolicyRecord(int id, byte[] detail, Set indexTerms) {
+ if (id < 0) {
+ throw new IllegalArgumentException("policy id must be non-negative");
+ }
+ this.id = id;
+ this.detail = Arrays.copyOf(detail, detail.length);
+ this.indexTerms = Collections.unmodifiableSet(new HashSet(indexTerms));
+ }
+
+ public int getId() { return id; }
+
+ public byte[] getDetail() { return Arrays.copyOf(detail, detail.length); }
+
+ public Set getIndexTerms() { return indexTerms; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/PolicySearchApplication.java b/architecture/src/main/java/com/arch/policy/PolicySearchApplication.java
new file mode 100644
index 0000000..9bdfd8b
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/PolicySearchApplication.java
@@ -0,0 +1,15 @@
+package com.arch.policy;
+
+import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.kafka.annotation.EnableKafka;
+
+@EnableKafka
+@EnableDubbo
+@SpringBootApplication
+public class PolicySearchApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(PolicySearchApplication.class, args);
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/PolicySnapshot.java b/architecture/src/main/java/com/arch/policy/PolicySnapshot.java
new file mode 100644
index 0000000..d4e7e53
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/PolicySnapshot.java
@@ -0,0 +1,170 @@
+package com.arch.policy;
+
+import org.roaringbitmap.RoaringBitmap;
+import org.rocksdb.Options;
+import org.rocksdb.RocksDB;
+import org.rocksdb.RocksDBException;
+import org.rocksdb.RocksIterator;
+
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/** A version-isolated RocksDB detail store and its matching bitmap index. */
+public final class PolicySnapshot implements AutoCloseable {
+ static { RocksDB.loadLibrary(); }
+
+ private final String version;
+ private final Path directory;
+ private final Options options;
+ private final RocksDB database;
+ private final Map bitmapIndex = new HashMap();
+ private final RoaringBitmap allPolicyIds = new RoaringBitmap();
+ private final Map> termsByPolicy = new HashMap>();
+ private MessagePosition position = MessagePosition.BEGINNING;
+ private boolean closed;
+
+ public PolicySnapshot(String version, Path directory) throws RocksDBException {
+ this.version = version;
+ this.directory = directory;
+ this.options = new Options().setCreateIfMissing(true);
+ this.database = RocksDB.open(options, directory.toString());
+ }
+
+ public synchronized void upsert(PolicyRecord policy) throws RocksDBException {
+ ensureOpen();
+ removeFromIndex(policy.getId());
+ database.put(key(policy.getId()), policy.getDetail());
+ allPolicyIds.add(policy.getId());
+ termsByPolicy.put(policy.getId(), policy.getIndexTerms());
+ for (String term : policy.getIndexTerms()) {
+ RoaringBitmap bitmap = bitmapIndex.get(term);
+ if (bitmap == null) {
+ bitmap = new RoaringBitmap();
+ bitmapIndex.put(term, bitmap);
+ }
+ bitmap.add(policy.getId());
+ }
+ }
+
+ public synchronized void delete(int policyId) throws RocksDBException {
+ ensureOpen();
+ database.delete(key(policyId));
+ allPolicyIds.remove(policyId);
+ removeFromIndex(policyId);
+ }
+
+ public synchronized byte[] findDetail(int policyId) throws RocksDBException {
+ ensureOpen();
+ return database.get(key(policyId));
+ }
+
+ public synchronized RoaringBitmap findPolicyIds(String term) {
+ ensureOpen();
+ RoaringBitmap bitmap = bitmapIndex.get(term);
+ return bitmap == null ? new RoaringBitmap() : bitmap.clone();
+ }
+
+ public synchronized RoaringBitmap allPolicyIds() {
+ ensureOpen();
+ return allPolicyIds.clone();
+ }
+
+ /** Applies an ordered event and advances its position in the same monitor. */
+ public synchronized boolean apply(PolicyChange change) throws RocksDBException {
+ ensureOpen();
+ if (change.getPosition().compareTo(position) <= 0) return false;
+ if (change.getType() == PolicyChange.Type.DELETE) delete(change.getPolicyId());
+ else upsert(change.getPolicy());
+ advanceTo(change.getPosition());
+ return true;
+ }
+
+ public synchronized long policyCount() throws RocksDBException {
+ ensureOpen();
+ long count = 0;
+ try (RocksIterator iterator = database.newIterator()) {
+ for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) count++;
+ iterator.status();
+ }
+ return count;
+ }
+
+ public synchronized Map bitmapCardinalities() {
+ ensureOpen();
+ Map result = new HashMap();
+ for (Map.Entry entry : bitmapIndex.entrySet()) {
+ result.put(entry.getKey(), entry.getValue().getCardinality());
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ public synchronized int indexedPolicyCount() {
+ ensureOpen();
+ RoaringBitmap all = new RoaringBitmap();
+ for (RoaringBitmap bitmap : bitmapIndex.values()) all.or(bitmap);
+ return all.getCardinality();
+ }
+
+ public String getVersion() { return version; }
+ public Path getDirectory() { return directory; }
+ public synchronized MessagePosition getPosition() { return position; }
+ public synchronized void advanceTo(MessagePosition newPosition) {
+ ensureOpen();
+ if (newPosition.compareTo(position) < 0) throw new IllegalArgumentException("position cannot move backwards");
+ position = newPosition;
+ }
+
+ @Override public synchronized void close() {
+ if (closed) return;
+ closed = true;
+ database.close();
+ options.close();
+ bitmapIndex.clear();
+ allPolicyIds.clear();
+ termsByPolicy.clear();
+ }
+
+ public void closeAndDelete() {
+ close();
+ try {
+ if (!Files.exists(directory)) return;
+ try (Stream paths = Files.walk(directory)) {
+ paths.sorted(java.util.Comparator.reverseOrder()).forEach(path -> {
+ try { Files.deleteIfExists(path); }
+ catch (IOException failure) { throw new DeleteFailure(failure); }
+ });
+ }
+ } catch (IOException failure) {
+ throw new IllegalStateException("cannot delete snapshot " + directory, failure);
+ } catch (DeleteFailure failure) {
+ throw new IllegalStateException("cannot delete snapshot " + directory, failure.getCause());
+ }
+ }
+
+ private static final class DeleteFailure extends RuntimeException {
+ private DeleteFailure(IOException cause) { super(cause); }
+ }
+
+ private void removeFromIndex(int policyId) {
+ Set oldTerms = termsByPolicy.remove(policyId);
+ if (oldTerms == null) return;
+ for (String term : oldTerms) {
+ RoaringBitmap bitmap = bitmapIndex.get(term);
+ bitmap.remove(policyId);
+ if (bitmap.isEmpty()) bitmapIndex.remove(term);
+ }
+ }
+
+ private void ensureOpen() {
+ if (closed) throw new IllegalStateException("snapshot is closed: " + version);
+ }
+
+ private static byte[] key(int id) { return ByteBuffer.allocate(4).putInt(id).array(); }
+}
diff --git a/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java b/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java
new file mode 100644
index 0000000..d90fac8
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java
@@ -0,0 +1,80 @@
+package com.arch.policy;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Coordinates startup retry and side-by-side runtime refresh. */
+public final class PolicySnapshotService implements AutoCloseable {
+ private final SnapshotBuilder builder;
+ private final ActiveSnapshotRegistry registry;
+ private final ScheduledExecutorService executor;
+ private final long retryDelayMillis;
+ private final AtomicBoolean building = new AtomicBoolean();
+ private volatile boolean stopped;
+
+ public PolicySnapshotService(SnapshotBuilder builder, ActiveSnapshotRegistry registry,
+ ScheduledExecutorService executor, long retryDelayMillis) {
+ this.builder = builder;
+ this.registry = registry;
+ this.executor = executor;
+ this.retryDelayMillis = retryDelayMillis;
+ }
+
+ /** Keeps readiness false and retries until the first valid snapshot is activated. */
+ public void start(String version) { submitBuild(version, true); }
+
+ /** Returns false when another candidate is already being built. */
+ public boolean refresh(String version) { return submitBuild(version, false); }
+
+ public boolean isReady() { return registry.isReady(); }
+ public boolean isBuilding() { return building.get(); }
+
+ /** Used by application startup: Spring startup does not finish before a snapshot is ready. */
+ public void initializeBlocking(String version) throws InterruptedException {
+ if (!building.compareAndSet(false, true)) throw new IllegalStateException("snapshot build already running");
+ try {
+ while (!stopped && !registry.isReady()) {
+ try {
+ registry.activate(builder.build(version));
+ } catch (Exception failure) {
+ Thread.sleep(retryDelayMillis);
+ }
+ }
+ } finally {
+ building.set(false);
+ }
+ }
+
+ private boolean submitBuild(final String version, final boolean retryOnFailure) {
+ if (stopped || !building.compareAndSet(false, true)) return false;
+ executor.execute(new Runnable() {
+ @Override public void run() { buildAndActivate(version, retryOnFailure); }
+ });
+ return true;
+ }
+
+ private void buildAndActivate(final String version, final boolean retryOnFailure) {
+ try {
+ PolicySnapshot candidate = builder.build(version);
+ if (stopped) candidate.closeAndDelete();
+ else registry.activate(candidate);
+ } catch (Exception ignored) {
+ if (retryOnFailure && !stopped) {
+ executor.schedule(new Runnable() {
+ @Override public void run() { buildAndActivate(version, true); }
+ }, retryDelayMillis, TimeUnit.MILLISECONDS);
+ return;
+ }
+ } finally {
+ // Startup retry owns the build slot until it succeeds or the service stops.
+ if (!retryOnFailure || registry.isReady() || stopped) building.set(false);
+ }
+ }
+
+ @Override public void close() {
+ stopped = true;
+ registry.close();
+ executor.shutdownNow();
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java b/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java
new file mode 100644
index 0000000..fbd2ccf
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java
@@ -0,0 +1,56 @@
+package com.arch.policy;
+
+import org.rocksdb.RocksDBException;
+
+import java.nio.file.Path;
+
+import static com.arch.policy.SnapshotPorts.*;
+
+/** Builds a candidate without exposing it to queries. */
+public final class SnapshotBuilder {
+ private final FullPolicyLoader fullLoader;
+ private final IncrementalReplayer replayer;
+ private final SnapshotValidator validator;
+ private final SnapshotDirectory directories;
+
+ public SnapshotBuilder(FullPolicyLoader fullLoader, IncrementalReplayer replayer,
+ SnapshotValidator validator, SnapshotDirectory directories) {
+ this.fullLoader = fullLoader;
+ this.replayer = replayer;
+ this.validator = validator;
+ this.directories = directories;
+ }
+
+ public PolicySnapshot build(String version) throws Exception {
+ Path directory = directories.create(version);
+ PolicySnapshot candidate = null;
+ try {
+ candidate = open(version, directory);
+ MessagePosition position = fullLoader.loadInto(candidate);
+ candidate.advanceTo(position);
+ catchUp(candidate);
+ validator.validate(candidate, candidate.getPosition());
+ return candidate;
+ } catch (Exception failure) {
+ if (candidate != null) candidate.close();
+ directories.delete(directory);
+ throw failure;
+ }
+ }
+
+ private void catchUp(PolicySnapshot candidate) throws Exception {
+ while (true) {
+ MessagePosition target = replayer.latestPosition();
+ if (candidate.getPosition().compareTo(target) >= 0) return;
+ MessagePosition replayed = replayer.replayInto(candidate, candidate.getPosition(), target);
+ if (replayed.compareTo(candidate.getPosition()) <= 0 || replayed.compareTo(target) > 0) {
+ throw new IllegalStateException("incremental replay made invalid progress");
+ }
+ candidate.advanceTo(replayed);
+ }
+ }
+
+ private PolicySnapshot open(String version, Path directory) throws RocksDBException {
+ return new PolicySnapshot(version, directory);
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/SnapshotPorts.java b/architecture/src/main/java/com/arch/policy/SnapshotPorts.java
new file mode 100644
index 0000000..9e3c4d4
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/SnapshotPorts.java
@@ -0,0 +1,28 @@
+package com.arch.policy;
+
+import java.nio.file.Path;
+
+public final class SnapshotPorts {
+ private SnapshotPorts() {}
+
+ public interface FullPolicyLoader {
+ /** Returns the message position corresponding to the full-data cut. */
+ MessagePosition loadInto(PolicySnapshot target) throws Exception;
+ }
+
+ public interface IncrementalReplayer {
+ /** Replays (position, latest] and returns the actual replayed position. */
+ MessagePosition replayInto(PolicySnapshot target, MessagePosition position,
+ MessagePosition latest) throws Exception;
+ MessagePosition latestPosition() throws Exception;
+ }
+
+ public interface SnapshotValidator {
+ void validate(PolicySnapshot candidate, MessagePosition expectedPosition) throws Exception;
+ }
+
+ public interface SnapshotDirectory {
+ Path create(String version) throws Exception;
+ void delete(Path directory) throws Exception;
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java b/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java
new file mode 100644
index 0000000..a58d313
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java
@@ -0,0 +1,76 @@
+package com.arch.policy.aggregation;
+
+import com.arch.policy.api.PolicySearchRequest;
+import com.arch.policy.api.PolicySearchResponse;
+import com.arch.policy.api.SupplierCallbackRequest;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+public final class AsyncSearchCoordinator {
+ private static final long MAX_WAIT_SLICE_MILLIS = 200;
+ private final SearchStateStore stateStore;
+ private final SupplierTaskDispatcher dispatcher;
+ private final LocalSearchWaiters localWaiters;
+ private final ObjectMapper objectMapper;
+ private final long redisTtlSeconds;
+
+ public AsyncSearchCoordinator(SearchStateStore stateStore, SupplierTaskDispatcher dispatcher,
+ LocalSearchWaiters localWaiters, ObjectMapper objectMapper,
+ long redisTtlSeconds) {
+ this.stateStore = stateStore;
+ this.dispatcher = dispatcher;
+ this.localWaiters = localWaiters;
+ this.objectMapper = objectMapper;
+ this.redisTtlSeconds = redisTtlSeconds;
+ }
+
+ public PolicySearchResponse search(PolicySearchRequest request) throws Exception {
+ validate(request);
+ String searchKey = UUID.randomUUID().toString();
+ Set suppliers = new LinkedHashSet(request.getSupplierIds());
+ stateStore.initialize(searchKey, suppliers, redisTtlSeconds);
+ LocalSearchWaiters.Waiter waiter = localWaiters.register(searchKey);
+ try {
+ if (stateStore.getState(searchKey) == SearchState.COMPLETED) return aggregate(searchKey);
+ dispatcher.dispatch(searchKey, suppliers, request);
+ return awaitResults(searchKey, request.getTotalTimeoutMillis(), waiter);
+ } finally {
+ localWaiters.remove(searchKey, waiter);
+ }
+ }
+
+ private PolicySearchResponse awaitResults(String searchKey, long timeoutMillis,
+ LocalSearchWaiters.Waiter waiter) throws Exception {
+ long deadline = System.currentTimeMillis() + timeoutMillis;
+ while (true) {
+ SearchState state = stateStore.getState(searchKey);
+ if (state == SearchState.COMPLETED) return aggregate(searchKey);
+ long remaining = deadline - System.currentTimeMillis();
+ if (state == SearchState.TIMED_OUT || remaining <= 0) {
+ stateStore.markTimedOut(searchKey, redisTtlSeconds);
+ return aggregate(searchKey);
+ }
+ waiter.await(Math.min(remaining, MAX_WAIT_SLICE_MILLIS));
+ }
+ }
+
+ private PolicySearchResponse aggregate(String searchKey) throws Exception {
+ List results = new ArrayList();
+ for (String payload : stateStore.getResultPayloads(searchKey)) {
+ results.add(objectMapper.readValue(payload, SupplierCallbackRequest.class));
+ }
+ SearchState state = stateStore.getState(searchKey);
+ return new PolicySearchResponse(searchKey, state.name(), results);
+ }
+
+ private static void validate(PolicySearchRequest request) {
+ if (request == null) throw new IllegalArgumentException("request is required");
+ if (request.getTotalTimeoutMillis() <= 0) throw new IllegalArgumentException("timeout must be positive");
+ if (request.getSupplierIds().isEmpty()) throw new IllegalArgumentException("supplierIds is required");
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java b/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java
new file mode 100644
index 0000000..5ccf471
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java
@@ -0,0 +1,32 @@
+package com.arch.policy.aggregation;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public final class LocalSearchWaiters {
+ private final ConcurrentMap waiters = new ConcurrentHashMap();
+
+ public Waiter register(String searchKey) {
+ Waiter waiter = new Waiter();
+ Waiter existing = waiters.putIfAbsent(searchKey, waiter);
+ if (existing != null) throw new IllegalStateException("duplicate local search: " + searchKey);
+ return waiter;
+ }
+
+ public void remove(String searchKey, Waiter waiter) { waiters.remove(searchKey, waiter); }
+
+ public void signal(String searchKey) {
+ Waiter waiter = waiters.get(searchKey);
+ if (waiter != null) waiter.signal();
+ }
+
+ public static final class Waiter {
+ private final CountDownLatch completed = new CountDownLatch(1);
+ public void await(long millis) throws InterruptedException {
+ completed.await(millis, TimeUnit.MILLISECONDS);
+ }
+ private void signal() { completed.countDown(); }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java b/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java
new file mode 100644
index 0000000..fa6ae2e
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java
@@ -0,0 +1,3 @@
+package com.arch.policy.aggregation;
+
+public enum SearchState { WAITING, COMPLETED, TIMED_OUT }
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java b/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java
new file mode 100644
index 0000000..c0a0a33
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java
@@ -0,0 +1,13 @@
+package com.arch.policy.aggregation;
+
+import java.util.List;
+import java.util.Set;
+
+public interface SearchStateStore {
+ void initialize(String searchKey, Set supplierIds, long ttlSeconds);
+ SearchState getState(String searchKey);
+ List getResultPayloads(String searchKey);
+ void recordCallback(String searchKey, String supplierId, String payload,
+ boolean finished, long ttlSeconds);
+ void markTimedOut(String searchKey, long ttlSeconds);
+}
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java b/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java
new file mode 100644
index 0000000..6113c36
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java
@@ -0,0 +1,38 @@
+package com.arch.policy.aggregation;
+
+import com.arch.policy.api.SupplierCallbackRequest;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public final class SupplierCallbackService {
+ private final SearchStateStore stateStore;
+ private final ObjectMapper objectMapper;
+ private final long redisTtlSeconds;
+
+ public SupplierCallbackService(SearchStateStore stateStore, ObjectMapper objectMapper,
+ long redisTtlSeconds) {
+ this.stateStore = stateStore;
+ this.objectMapper = objectMapper;
+ this.redisTtlSeconds = redisTtlSeconds;
+ }
+
+ public void callback(SupplierCallbackRequest request) throws Exception {
+ validate(request);
+ String payload = request.getResults().isEmpty() ? "" : objectMapper.writeValueAsString(request);
+ stateStore.recordCallback(request.getSearchKey(), request.getSupplierId(), payload,
+ request.isSearchFinished(), redisTtlSeconds);
+ }
+
+ public void markFinishedWithoutResult(String searchKey, String supplierId) throws Exception {
+ SupplierCallbackRequest request = new SupplierCallbackRequest();
+ request.setSearchKey(searchKey);
+ request.setSupplierId(supplierId);
+ request.setSearchFinished(true);
+ callback(request);
+ }
+
+ private static void validate(SupplierCallbackRequest request) {
+ if (request == null || request.getSearchKey() == null || request.getSupplierId() == null) {
+ throw new IllegalArgumentException("searchKey and supplierId are required");
+ }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java b/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java
new file mode 100644
index 0000000..ab43db7
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java
@@ -0,0 +1,9 @@
+package com.arch.policy.aggregation;
+
+import com.arch.policy.api.PolicySearchRequest;
+
+import java.util.Set;
+
+public interface SupplierTaskDispatcher {
+ void dispatch(String searchKey, Set supplierIds, PolicySearchRequest request);
+}
diff --git a/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java b/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java
new file mode 100644
index 0000000..43b5166
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java
@@ -0,0 +1,10 @@
+package com.arch.policy.api;
+
+import java.io.Serializable;
+
+public final class CallbackResponse implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private final boolean success;
+ public CallbackResponse(boolean success) { this.success = success; }
+ public boolean isSuccess() { return success; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java b/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java
new file mode 100644
index 0000000..fe9a308
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java
@@ -0,0 +1,22 @@
+package com.arch.policy.api;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class PolicySearchRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private String criteria;
+ private List supplierIds = new ArrayList();
+ private long totalTimeoutMillis = 3000;
+
+ public String getCriteria() { return criteria; }
+ public void setCriteria(String criteria) { this.criteria = criteria; }
+ public List getSupplierIds() { return Collections.unmodifiableList(supplierIds); }
+ public void setSupplierIds(List supplierIds) {
+ this.supplierIds = supplierIds == null ? new ArrayList() : new ArrayList(supplierIds);
+ }
+ public long getTotalTimeoutMillis() { return totalTimeoutMillis; }
+ public void setTotalTimeoutMillis(long totalTimeoutMillis) { this.totalTimeoutMillis = totalTimeoutMillis; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java b/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java
new file mode 100644
index 0000000..f8b3ecd
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java
@@ -0,0 +1,22 @@
+package com.arch.policy.api;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class PolicySearchResponse implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private final String searchKey;
+ private final String state;
+ private final List results;
+
+ public PolicySearchResponse(String searchKey, String state, List results) {
+ this.searchKey = searchKey;
+ this.state = state;
+ this.results = Collections.unmodifiableList(new ArrayList(results));
+ }
+ public String getSearchKey() { return searchKey; }
+ public String getState() { return state; }
+ public List getResults() { return results; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java b/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java
new file mode 100644
index 0000000..5ab9e75
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java
@@ -0,0 +1,7 @@
+package com.arch.policy.api;
+
+import java.util.concurrent.CompletableFuture;
+
+public interface PolicySearchRpcService {
+ CompletableFuture asyncSearch(PolicySearchRequest request);
+}
diff --git a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java
new file mode 100644
index 0000000..f965f51
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java
@@ -0,0 +1,25 @@
+package com.arch.policy.api;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class SupplierCallbackRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private String searchKey;
+ private String supplierId;
+ private List results = new ArrayList();
+ private boolean searchFinished;
+
+ public String getSearchKey() { return searchKey; }
+ public void setSearchKey(String searchKey) { this.searchKey = searchKey; }
+ public String getSupplierId() { return supplierId; }
+ public void setSupplierId(String supplierId) { this.supplierId = supplierId; }
+ public List getResults() { return Collections.unmodifiableList(results); }
+ public void setResults(List results) {
+ this.results = results == null ? new ArrayList() : new ArrayList(results);
+ }
+ public boolean isSearchFinished() { return searchFinished; }
+ public void setSearchFinished(boolean searchFinished) { this.searchFinished = searchFinished; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java
new file mode 100644
index 0000000..1cdff44
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java
@@ -0,0 +1,5 @@
+package com.arch.policy.api;
+
+public interface SupplierCallbackRpcService {
+ CallbackResponse callback(SupplierCallbackRequest request);
+}
diff --git a/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java b/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java
new file mode 100644
index 0000000..d7d1d35
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java
@@ -0,0 +1,16 @@
+package com.arch.policy.application;
+
+import com.arch.policy.ActiveSnapshotRegistry;
+import com.arch.policy.PolicyChange;
+
+public final class PolicyIncrementalUpdater {
+ private final ActiveSnapshotRegistry registry;
+
+ public PolicyIncrementalUpdater(ActiveSnapshotRegistry registry) { this.registry = registry; }
+
+ public boolean apply(PolicyChange change) throws Exception {
+ try (ActiveSnapshotRegistry.SnapshotLease lease = registry.acquire()) {
+ return lease.snapshot().apply(change);
+ }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java b/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java
new file mode 100644
index 0000000..58bbf5f
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java
@@ -0,0 +1,19 @@
+package com.arch.policy.application;
+
+import com.arch.policy.PolicySnapshotService;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+
+public final class PolicyStartupRunner implements ApplicationRunner {
+ private final PolicySnapshotService snapshotService;
+ private final String initialVersion;
+
+ public PolicyStartupRunner(PolicySnapshotService snapshotService, String initialVersion) {
+ this.snapshotService = snapshotService;
+ this.initialVersion = initialVersion;
+ }
+
+ @Override public void run(ApplicationArguments args) throws Exception {
+ snapshotService.initializeBlocking(initialVersion);
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java b/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java
new file mode 100644
index 0000000..03f770c
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java
@@ -0,0 +1,109 @@
+package com.arch.policy.config;
+
+import com.arch.policy.ActiveSnapshotRegistry;
+import com.arch.policy.DefaultSnapshotValidator;
+import com.arch.policy.FileSystemSnapshotDirectory;
+import com.arch.policy.PolicySnapshotService;
+import com.arch.policy.SnapshotBuilder;
+import com.arch.policy.SnapshotPorts.FullPolicyLoader;
+import com.arch.policy.SnapshotPorts.IncrementalReplayer;
+import com.arch.policy.application.PolicyIncrementalUpdater;
+import com.arch.policy.application.PolicyStartupRunner;
+import com.arch.policy.aggregation.AsyncSearchCoordinator;
+import com.arch.policy.aggregation.LocalSearchWaiters;
+import com.arch.policy.aggregation.SearchStateStore;
+import com.arch.policy.aggregation.SupplierCallbackService;
+import com.arch.policy.aggregation.SupplierTaskDispatcher;
+import com.arch.policy.demo.DemoSupplierTaskDispatcher;
+import com.arch.policy.kafka.KafkaPolicyChangeListener;
+import com.arch.policy.redis.RedisSearchStateStore;
+import com.arch.policy.redis.SearchFinishedSubscriber;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.listener.ChannelTopic;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.nio.file.Paths;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+@Configuration
+public class PolicySearchConfiguration {
+ @Bean public ActiveSnapshotRegistry activeSnapshotRegistry() { return new ActiveSnapshotRegistry(); }
+
+ @Bean public PolicyIncrementalUpdater policyIncrementalUpdater(ActiveSnapshotRegistry registry) {
+ return new PolicyIncrementalUpdater(registry);
+ }
+
+ @Bean(destroyMethod = "shutdown") public Executor policySearchExecutor() {
+ return Executors.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors()));
+ }
+
+ @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); }
+
+ @Bean public SearchStateStore searchStateStore(StringRedisTemplate redis) {
+ return new RedisSearchStateStore(redis);
+ }
+
+ @Bean public LocalSearchWaiters localSearchWaiters() { return new LocalSearchWaiters(); }
+
+ @Bean public SupplierCallbackService supplierCallbackService(
+ SearchStateStore store, ObjectMapper mapper,
+ @Value("${policy.search.redis-ttl-seconds:60}") long ttlSeconds) {
+ return new SupplierCallbackService(store, mapper, ttlSeconds);
+ }
+
+ @Bean(destroyMethod = "shutdown") public ScheduledExecutorService demoSupplierExecutor() {
+ return Executors.newScheduledThreadPool(4);
+ }
+
+ @Bean public SupplierTaskDispatcher supplierTaskDispatcher(
+ ScheduledExecutorService demoSupplierExecutor, SupplierCallbackService callbackService) {
+ return new DemoSupplierTaskDispatcher(demoSupplierExecutor, callbackService);
+ }
+
+ @Bean public AsyncSearchCoordinator asyncSearchCoordinator(
+ SearchStateStore store, SupplierTaskDispatcher dispatcher, LocalSearchWaiters waiters,
+ ObjectMapper mapper, @Value("${policy.search.redis-ttl-seconds:60}") long ttlSeconds) {
+ return new AsyncSearchCoordinator(store, dispatcher, waiters, mapper, ttlSeconds);
+ }
+
+ @Bean public RedisMessageListenerContainer searchFinishedListener(
+ RedisConnectionFactory connectionFactory, LocalSearchWaiters waiters) {
+ RedisMessageListenerContainer container = new RedisMessageListenerContainer();
+ container.setConnectionFactory(connectionFactory);
+ container.addMessageListener(new SearchFinishedSubscriber(waiters),
+ new ChannelTopic(RedisSearchStateStore.FINISHED_CHANNEL));
+ return container;
+ }
+
+ @Bean public KafkaPolicyChangeListener kafkaPolicyChangeListener(ObjectMapper mapper,
+ PolicyIncrementalUpdater updater) {
+ return new KafkaPolicyChangeListener(mapper, updater);
+ }
+
+ @Bean public SnapshotBuilder snapshotBuilder(FullPolicyLoader fullLoader,
+ IncrementalReplayer replayer,
+ @Value("${policy.snapshot.directory:./data/policy-snapshots}") String directory) {
+ return new SnapshotBuilder(fullLoader, replayer, new DefaultSnapshotValidator(),
+ new FileSystemSnapshotDirectory(Paths.get(directory)));
+ }
+
+ @Bean(destroyMethod = "close") public PolicySnapshotService policySnapshotService(
+ SnapshotBuilder builder, ActiveSnapshotRegistry registry,
+ @Value("${policy.snapshot.retry-delay-ms:5000}") long retryDelayMillis) {
+ return new PolicySnapshotService(builder, registry,
+ Executors.newSingleThreadScheduledExecutor(), retryDelayMillis);
+ }
+
+ @Bean public PolicyStartupRunner policyStartupRunner(
+ PolicySnapshotService service,
+ @Value("${policy.snapshot.initial-version:startup}") String initialVersion) {
+ return new PolicyStartupRunner(service, initialVersion);
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java b/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java
new file mode 100644
index 0000000..ecb3714
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java
@@ -0,0 +1,47 @@
+package com.arch.policy.demo;
+
+import com.arch.policy.aggregation.SupplierCallbackService;
+import com.arch.policy.aggregation.SupplierTaskDispatcher;
+import com.arch.policy.api.PolicySearchRequest;
+import com.arch.policy.api.SupplierCallbackRequest;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/** Simulates downstream suppliers asynchronously calling this module back. */
+public final class DemoSupplierTaskDispatcher implements SupplierTaskDispatcher {
+ private final ScheduledExecutorService executor;
+ private final SupplierCallbackService callbackService;
+
+ public DemoSupplierTaskDispatcher(ScheduledExecutorService executor,
+ SupplierCallbackService callbackService) {
+ this.executor = executor;
+ this.callbackService = callbackService;
+ }
+
+ @Override public void dispatch(final String searchKey, Set supplierIds,
+ final PolicySearchRequest request) {
+ int delay = 20;
+ for (final String supplierId : supplierIds) {
+ executor.schedule(new Runnable() {
+ @Override public void run() {
+ try {
+ SupplierCallbackRequest callback = new SupplierCallbackRequest();
+ callback.setSearchKey(searchKey);
+ callback.setSupplierId(supplierId);
+ callback.setResults(Collections.singletonList(
+ supplierId + " quote for " + request.getCriteria()));
+ callback.setSearchFinished(true);
+ callbackService.callback(callback);
+ } catch (Exception failure) {
+ try { callbackService.markFinishedWithoutResult(searchKey, supplierId); }
+ catch (Exception ignored) { /* broker retry/alert belongs in a real adapter */ }
+ }
+ }
+ }, delay, TimeUnit.MILLISECONDS);
+ delay += 20;
+ }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java b/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java
new file mode 100644
index 0000000..595318c
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java
@@ -0,0 +1,45 @@
+package com.arch.policy.kafka;
+
+import com.arch.policy.MessagePosition;
+import com.arch.policy.PolicyChange;
+import com.arch.policy.PolicyRecord;
+import com.arch.policy.application.PolicyIncrementalUpdater;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.kafka.annotation.KafkaListener;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+
+public final class KafkaPolicyChangeListener {
+ private final ObjectMapper objectMapper;
+ private final PolicyIncrementalUpdater updater;
+
+ public KafkaPolicyChangeListener(ObjectMapper objectMapper, PolicyIncrementalUpdater updater) {
+ this.objectMapper = objectMapper;
+ this.updater = updater;
+ }
+
+ @KafkaListener(topics = "${policy.kafka.topic:policy-change}",
+ groupId = "${policy.kafka.group-id:policy-search}")
+ public void onMessage(String json) throws Exception {
+ PolicyChangeMessage message = objectMapper.readValue(json, PolicyChangeMessage.class);
+ updater.apply(toChange(message));
+ }
+
+ private PolicyChange toChange(PolicyChangeMessage message) {
+ MessagePosition position = new MessagePosition(message.getPosition());
+ if ("DELETE".equalsIgnoreCase(message.getOperation())) {
+ return PolicyChange.delete(message.getPolicyId(), position);
+ }
+ if (!"UPSERT".equalsIgnoreCase(message.getOperation())) {
+ throw new IllegalArgumentException("unsupported policy operation: " + message.getOperation());
+ }
+ if (message.getDetail() == null || message.getIndexTerms() == null) {
+ throw new IllegalArgumentException("UPSERT requires detail and indexTerms");
+ }
+ PolicyRecord policy = new PolicyRecord(message.getPolicyId(),
+ message.getDetail().getBytes(StandardCharsets.UTF_8),
+ new HashSet(message.getIndexTerms()));
+ return PolicyChange.upsert(policy, position);
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java b/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java
new file mode 100644
index 0000000..3f539ec
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java
@@ -0,0 +1,23 @@
+package com.arch.policy.kafka;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class PolicyChangeMessage {
+ private String operation;
+ private int policyId;
+ private String detail;
+ private List indexTerms = new ArrayList();
+ private long position;
+
+ public String getOperation() { return operation; }
+ public void setOperation(String operation) { this.operation = operation; }
+ public int getPolicyId() { return policyId; }
+ public void setPolicyId(int policyId) { this.policyId = policyId; }
+ public String getDetail() { return detail; }
+ public void setDetail(String detail) { this.detail = detail; }
+ public List getIndexTerms() { return indexTerms; }
+ public void setIndexTerms(List indexTerms) { this.indexTerms = indexTerms; }
+ public long getPosition() { return position; }
+ public void setPosition(long position) { this.position = position; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java b/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java
new file mode 100644
index 0000000..49547f0
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java
@@ -0,0 +1,84 @@
+package com.arch.policy.redis;
+
+import com.arch.policy.aggregation.SearchState;
+import com.arch.policy.aggregation.SearchStateStore;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public final class RedisSearchStateStore implements SearchStateStore {
+ public static final String FINISHED_CHANNEL = "search-finished";
+ private static final String EMPTY_RESULT_MARKER = "__SEARCH_INITIALIZED__";
+ private static final DefaultRedisScript INIT = script(
+ "redis.call('DEL', KEYS[1], KEYS[2], KEYS[3]); redis.call('RPUSH', KEYS[1], ARGV[2]); " +
+ "redis.call('EXPIRE', KEYS[1], ARGV[1]); " +
+ "for i=3,#ARGV do redis.call('SADD', KEYS[2], ARGV[i]); end; " +
+ "if redis.call('SCARD', KEYS[2]) == 0 then " +
+ " redis.call('SET', KEYS[3], 'COMPLETED', 'EX', ARGV[1]); return 1; " +
+ "end; redis.call('SET', KEYS[3], 'WAITING', 'EX', ARGV[1]); " +
+ "redis.call('EXPIRE', KEYS[1], ARGV[1]); redis.call('EXPIRE', KEYS[2], ARGV[1]); return 0;");
+ private static final DefaultRedisScript CALLBACK = script(
+ "if redis.call('GET', KEYS[3]) ~= 'WAITING' then return 0; end; " +
+ "if redis.call('SISMEMBER', KEYS[2], ARGV[2]) == 0 then return 0; end; " +
+ "if ARGV[1] ~= '' then redis.call('RPUSH', KEYS[1], ARGV[1]); end; " +
+ "local removed=0; if ARGV[3] == 'true' then removed=redis.call('SREM', KEYS[2], ARGV[2]); end; " +
+ "redis.call('EXPIRE', KEYS[1], ARGV[5]); redis.call('EXPIRE', KEYS[2], ARGV[5]); " +
+ "redis.call('EXPIRE', KEYS[3], ARGV[5]); " +
+ "if removed > 0 and redis.call('SCARD', KEYS[2]) == 0 then " +
+ " redis.call('SET', KEYS[3], 'COMPLETED', 'EX', ARGV[5]); " +
+ " redis.call('PUBLISH', ARGV[6], ARGV[4]); return 2; end; return 1;");
+ private static final DefaultRedisScript TIMEOUT = script(
+ "if redis.call('GET', KEYS[3]) == 'WAITING' then " +
+ " redis.call('SET', KEYS[3], 'TIMED_OUT', 'EX', ARGV[1]); " +
+ " redis.call('EXPIRE', KEYS[1], ARGV[1]); redis.call('EXPIRE', KEYS[2], ARGV[1]); return 1; end; return 0;");
+
+ private final StringRedisTemplate redis;
+
+ public RedisSearchStateStore(StringRedisTemplate redis) { this.redis = redis; }
+
+ @Override public void initialize(String searchKey, Set supplierIds, long ttlSeconds) {
+ List args = new ArrayList();
+ args.add(Long.toString(ttlSeconds));
+ args.add(EMPTY_RESULT_MARKER);
+ args.addAll(supplierIds);
+ redis.execute(INIT, keys(searchKey), args.toArray());
+ }
+
+ @Override public SearchState getState(String searchKey) {
+ String state = redis.opsForValue().get(stateKey(searchKey));
+ return state == null ? SearchState.TIMED_OUT : SearchState.valueOf(state);
+ }
+
+ @Override public List getResultPayloads(String searchKey) {
+ List values = redis.opsForList().range(resultKey(searchKey), 0, -1);
+ if (values == null) return Collections.emptyList();
+ List results = new ArrayList(values);
+ results.remove(EMPTY_RESULT_MARKER);
+ return results;
+ }
+
+ @Override public void recordCallback(String searchKey, String supplierId, String payload,
+ boolean finished, long ttlSeconds) {
+ redis.execute(CALLBACK, keys(searchKey), payload, supplierId, Boolean.toString(finished),
+ searchKey, Long.toString(ttlSeconds), FINISHED_CHANNEL);
+ }
+
+ @Override public void markTimedOut(String searchKey, long ttlSeconds) {
+ redis.execute(TIMEOUT, keys(searchKey), Long.toString(ttlSeconds));
+ }
+
+ private static List keys(String searchKey) {
+ return Arrays.asList(resultKey(searchKey), pendingKey(searchKey), stateKey(searchKey));
+ }
+ private static String resultKey(String key) { return "search:" + key + ":results"; }
+ private static String pendingKey(String key) { return "search:" + key + ":pending"; }
+ private static String stateKey(String key) { return "search:" + key + ":state"; }
+ private static DefaultRedisScript script(String source) {
+ return new DefaultRedisScript(source, Long.class);
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java b/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java
new file mode 100644
index 0000000..f8ea514
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java
@@ -0,0 +1,15 @@
+package com.arch.policy.redis;
+
+import com.arch.policy.aggregation.LocalSearchWaiters;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+
+import java.nio.charset.StandardCharsets;
+
+public final class SearchFinishedSubscriber implements MessageListener {
+ private final LocalSearchWaiters waiters;
+ public SearchFinishedSubscriber(LocalSearchWaiters waiters) { this.waiters = waiters; }
+ @Override public void onMessage(Message message, byte[] pattern) {
+ waiters.signal(new String(message.getBody(), StandardCharsets.UTF_8));
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java b/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java
new file mode 100644
index 0000000..ef51aa6
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java
@@ -0,0 +1,34 @@
+package com.arch.policy.rpc;
+
+import com.arch.policy.api.PolicySearchRequest;
+import com.arch.policy.api.PolicySearchResponse;
+import com.arch.policy.api.PolicySearchRpcService;
+import com.arch.policy.aggregation.AsyncSearchCoordinator;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.springframework.beans.factory.annotation.Qualifier;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+@DubboService(version = "1.0.0", timeout = 3000)
+public final class DubboPolicySearchService implements PolicySearchRpcService {
+ private final AsyncSearchCoordinator searchService;
+ private final Executor searchExecutor;
+
+ public DubboPolicySearchService(AsyncSearchCoordinator searchService,
+ @Qualifier("policySearchExecutor") Executor searchExecutor) {
+ this.searchService = searchService;
+ this.searchExecutor = searchExecutor;
+ }
+
+ @Override public CompletableFuture asyncSearch(final PolicySearchRequest request) {
+ final CompletableFuture future = new CompletableFuture();
+ searchExecutor.execute(new Runnable() {
+ @Override public void run() {
+ try { future.complete(searchService.search(request)); }
+ catch (Exception failure) { future.completeExceptionally(failure); }
+ }
+ });
+ return future;
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java b/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java
new file mode 100644
index 0000000..f685dc1
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java
@@ -0,0 +1,23 @@
+package com.arch.policy.rpc;
+
+import com.arch.policy.aggregation.SupplierCallbackService;
+import com.arch.policy.api.CallbackResponse;
+import com.arch.policy.api.SupplierCallbackRequest;
+import com.arch.policy.api.SupplierCallbackRpcService;
+import org.apache.dubbo.config.annotation.DubboService;
+
+@DubboService(version = "1.0.0", timeout = 3000)
+public final class DubboSupplierCallbackService implements SupplierCallbackRpcService {
+ private final SupplierCallbackService callbackService;
+ public DubboSupplierCallbackService(SupplierCallbackService callbackService) {
+ this.callbackService = callbackService;
+ }
+ @Override public CallbackResponse callback(SupplierCallbackRequest request) {
+ try {
+ callbackService.callback(request);
+ return new CallbackResponse(true);
+ } catch (Exception failure) {
+ throw new IllegalStateException("supplier callback failed", failure);
+ }
+ }
+}
diff --git a/architecture/src/main/resources/application.yml b/architecture/src/main/resources/application.yml
new file mode 100644
index 0000000..c30aadb
--- /dev/null
+++ b/architecture/src/main/resources/application.yml
@@ -0,0 +1,32 @@
+spring:
+ application:
+ name: policy-search
+ kafka:
+ consumer:
+ enable-auto-commit: false
+ auto-offset-reset: earliest
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ listener:
+ ack-mode: record
+ redis:
+ host: localhost
+ port: 6379
+
+dubbo:
+ application:
+ name: policy-search
+ protocol:
+ name: dubbo
+ port: 20880
+
+policy:
+ search:
+ redis-ttl-seconds: 60
+ snapshot:
+ directory: ./data/policy-snapshots
+ initial-version: startup
+ retry-delay-ms: 5000
+ kafka:
+ topic: policy-change
+ group-id: policy-search
diff --git a/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java b/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java
new file mode 100644
index 0000000..fe28969
--- /dev/null
+++ b/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java
@@ -0,0 +1,99 @@
+package com.arch.policy;
+
+import com.arch.policy.aggregation.*;
+import com.arch.policy.api.PolicySearchRequest;
+import com.arch.policy.api.PolicySearchResponse;
+import com.arch.policy.api.SupplierCallbackRequest;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class AsyncSearchCoordinatorTest {
+ @Test
+ void waitsForAllSuppliersAndAggregatesAsyncCallbacks() throws Exception {
+ final MemoryStore store = new MemoryStore();
+ final LocalSearchWaiters waiters = new LocalSearchWaiters();
+ final ObjectMapper mapper = new ObjectMapper();
+ SupplierTaskDispatcher dispatcher = (searchKey, suppliers, request) -> {
+ new Thread(() -> {
+ try {
+ for (String supplier : suppliers) {
+ SupplierCallbackRequest callback = callback(searchKey, supplier, supplier + "-quote", true);
+ store.recordCallback(searchKey, supplier, mapper.writeValueAsString(callback), true, 60);
+ }
+ waiters.signal(searchKey);
+ } catch (Exception failure) { throw new RuntimeException(failure); }
+ }).start();
+ };
+ AsyncSearchCoordinator coordinator = new AsyncSearchCoordinator(store, dispatcher, waiters, mapper, 60);
+ PolicySearchRequest request = request(1000, "A", "B");
+
+ PolicySearchResponse response = coordinator.search(request);
+
+ assertEquals(SearchState.COMPLETED.name(), response.getState());
+ assertEquals(2, response.getResults().size());
+ }
+
+ @Test
+ void timeoutReturnsPartialResultsAndRejectsLateCallbacks() throws Exception {
+ final MemoryStore store = new MemoryStore();
+ final ObjectMapper mapper = new ObjectMapper();
+ SupplierTaskDispatcher dispatcher = (searchKey, suppliers, request) -> {
+ try {
+ SupplierCallbackRequest partial = callback(searchKey, "A", "A-quote", true);
+ store.recordCallback(searchKey, "A", mapper.writeValueAsString(partial), true, 60);
+ } catch (Exception failure) { throw new RuntimeException(failure); }
+ };
+ AsyncSearchCoordinator coordinator = new AsyncSearchCoordinator(
+ store, dispatcher, new LocalSearchWaiters(), mapper, 60);
+
+ PolicySearchResponse response = coordinator.search(request(30, "A", "B"));
+
+ assertEquals(SearchState.TIMED_OUT.name(), response.getState());
+ assertEquals(1, response.getResults().size());
+ }
+
+ private static PolicySearchRequest request(long timeout, String... suppliers) {
+ PolicySearchRequest request = new PolicySearchRequest();
+ request.setCriteria("demo");
+ request.setSupplierIds(Arrays.asList(suppliers));
+ request.setTotalTimeoutMillis(timeout);
+ return request;
+ }
+
+ private static SupplierCallbackRequest callback(String key, String supplier, String result, boolean done) {
+ SupplierCallbackRequest request = new SupplierCallbackRequest();
+ request.setSearchKey(key);
+ request.setSupplierId(supplier);
+ request.setResults(Collections.singletonList(result));
+ request.setSearchFinished(done);
+ return request;
+ }
+
+ private static final class MemoryStore implements SearchStateStore {
+ private SearchState state;
+ private final Set pending = new HashSet();
+ private final List results = new ArrayList();
+ @Override public synchronized void initialize(String key, Set suppliers, long ttl) {
+ pending.addAll(suppliers);
+ state = suppliers.isEmpty() ? SearchState.COMPLETED : SearchState.WAITING;
+ }
+ @Override public synchronized SearchState getState(String key) { return state; }
+ @Override public synchronized List getResultPayloads(String key) {
+ return new ArrayList(results);
+ }
+ @Override public synchronized void recordCallback(String key, String supplier, String payload,
+ boolean finished, long ttl) {
+ if (state != SearchState.WAITING) return;
+ if (!payload.isEmpty()) results.add(payload);
+ if (finished) pending.remove(supplier);
+ if (pending.isEmpty()) state = SearchState.COMPLETED;
+ }
+ @Override public synchronized void markTimedOut(String key, long ttl) {
+ if (state == SearchState.WAITING) state = SearchState.TIMED_OUT;
+ }
+ }
+}
diff --git a/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java b/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java
new file mode 100644
index 0000000..297a7c0
--- /dev/null
+++ b/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java
@@ -0,0 +1,119 @@
+package com.arch.policy;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+import static com.arch.policy.SnapshotPorts.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+class SnapshotLifecycleTest {
+ @TempDir Path root;
+
+ @Test
+ void buildsFullDataReplaysChangesAndValidatesPosition() throws Exception {
+ SnapshotBuilder builder = builder(false);
+
+ PolicySnapshot snapshot = builder.build("V101");
+
+ assertEquals(new MessagePosition(12), snapshot.getPosition());
+ assertArrayEquals(bytes("new-detail"), snapshot.findDetail(1));
+ assertEquals(1, snapshot.findPolicyIds("active").getCardinality());
+ assertEquals(0, snapshot.findPolicyIds("draft").getCardinality());
+ snapshot.closeAndDelete();
+ }
+
+ @Test
+ void switchWaitsForOldQueriesBeforeClosingAndDeletingOldVersion() throws Exception {
+ ActiveSnapshotRegistry registry = new ActiveSnapshotRegistry();
+ PolicySnapshot old = builder(false).build("V100");
+ PolicySnapshot replacement = builder(false).build("V101");
+ registry.activate(old);
+ ActiveSnapshotRegistry.SnapshotLease oldQuery = registry.acquire();
+
+ registry.activate(replacement);
+
+ assertTrue(Files.exists(old.getDirectory()));
+ assertArrayEquals(bytes("new-detail"), oldQuery.snapshot().findDetail(1));
+ try (ActiveSnapshotRegistry.SnapshotLease newQuery = registry.acquire()) {
+ assertEquals("V101", newQuery.snapshot().getVersion());
+ }
+ oldQuery.close();
+ assertFalse(Files.exists(old.getDirectory()));
+ assertThrows(IllegalStateException.class, () -> old.findPolicyIds("active"));
+ registry.close();
+ }
+
+ @Test
+ void failedRuntimeCandidateDoesNotReplaceServingSnapshot() throws Exception {
+ ActiveSnapshotRegistry registry = new ActiveSnapshotRegistry();
+ registry.activate(builder(false).build("V100"));
+ ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+ PolicySnapshotService service = new PolicySnapshotService(builder(true), registry, executor, 1);
+
+ assertTrue(service.refresh("V101"));
+ waitUntilBuildFinishes(service);
+
+ assertTrue(service.isReady());
+ try (ActiveSnapshotRegistry.SnapshotLease query = registry.acquire()) {
+ assertEquals("V100", query.snapshot().getVersion());
+ }
+ assertFalse(Files.exists(root.resolve("V101")));
+ service.close();
+ }
+
+ private SnapshotBuilder builder(final boolean failValidation) {
+ FullPolicyLoader full = target -> {
+ target.upsert(new PolicyRecord(1, bytes("old-detail"), Collections.singleton("draft")));
+ return new MessagePosition(10);
+ };
+ IncrementalReplayer replay = new IncrementalReplayer() {
+ @Override public MessagePosition latestPosition() { return new MessagePosition(12); }
+ @Override public MessagePosition replayInto(PolicySnapshot target, MessagePosition from,
+ MessagePosition latest) throws Exception {
+ target.upsert(new PolicyRecord(1, bytes("new-detail"),
+ Collections.singleton("active")));
+ return latest;
+ }
+ };
+ SnapshotValidator validator = (candidate, position) -> {
+ new DefaultSnapshotValidator().validate(candidate, position);
+ if (failValidation) throw new IllegalStateException("invalid candidate");
+ };
+ SnapshotDirectory directories = new SnapshotDirectory() {
+ @Override public Path create(String version) throws Exception {
+ Path path = root.resolve(version);
+ Files.createDirectories(path);
+ return path;
+ }
+ @Override public void delete(Path directory) throws Exception {
+ if (!Files.exists(directory)) return;
+ Files.walk(directory).sorted(Comparator.reverseOrder()).forEach(path -> {
+ try { Files.deleteIfExists(path); }
+ catch (Exception failure) { throw new RuntimeException(failure); }
+ });
+ }
+ };
+ return new SnapshotBuilder(full, replay, validator, directories);
+ }
+
+ private static byte[] bytes(String value) {
+ return value.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+ }
+
+ private static void waitUntilBuildFinishes(PolicySnapshotService service) throws Exception {
+ long deadline = System.currentTimeMillis() + 3000;
+ while (System.currentTimeMillis() < deadline) {
+ if (!service.isBuilding()) return;
+ Thread.sleep(10);
+ }
+ fail("candidate build did not finish");
+ }
+}
From 5deeb99325653b946b380e7d854a735b1a809965 Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Sun, 19 Jul 2026 21:12:39 +0800
Subject: [PATCH 33/37] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BC=AA=E4=BB=A3?=
=?UTF-8?q?=E7=A0=81=E6=9E=B6=E6=9E=84=E5=BA=93=E5=88=86=E5=B1=82=E7=BB=93?=
=?UTF-8?q?=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
architecture/README.md | 214 ++++++++++++++----
.../api/SupplierCallbackRpcService.java | 5 -
.../{ => search}/PolicySearchRpcService.java | 5 +-
.../search/SupplierCallbackRpcService.java | 8 +
.../config/PolicySearchConfiguration.java | 40 ++--
.../{ => common/model}/MessagePosition.java | 2 +-
.../{ => common/model}/PolicyChange.java | 2 +-
.../{ => common/model}/PolicyRecord.java | 2 +-
.../search}/CallbackResponse.java | 2 +-
.../search}/PolicySearchRequest.java | 2 +-
.../search}/PolicySearchResponse.java | 2 +-
.../search}/SupplierCallbackRequest.java | 2 +-
.../application}/AsyncSearchCoordinator.java | 8 +-
.../application}/LocalSearchWaiters.java | 2 +-
.../application/PolicyIncrementalUpdater.java | 6 +-
.../application/PolicyStartupRunner.java | 4 +-
.../application}/SearchState.java | 2 +-
.../application}/SearchStateStore.java | 2 +-
.../application}/SupplierCallbackService.java | 4 +-
.../application}/SupplierTaskDispatcher.java | 4 +-
.../snapshot}/ActiveSnapshotRegistry.java | 2 +-
.../snapshot}/DefaultSnapshotValidator.java | 6 +-
.../FileSystemSnapshotDirectory.java | 4 +-
.../domain/snapshot}/PolicySnapshot.java | 6 +-
.../snapshot}/PolicySnapshotService.java | 2 +-
.../domain/snapshot}/SnapshotBuilder.java | 6 +-
.../domain/snapshot}/SnapshotPorts.java | 4 +-
.../demo/DemoSupplierTaskDispatcher.java | 10 +-
.../kafka/KafkaPolicyChangeListener.java | 10 +-
.../kafka/PolicyChangeMessage.java | 2 +-
.../redis/RedisSearchStateStore.java | 6 +-
.../redis/SearchFinishedSubscriber.java | 4 +-
.../rpc/DubboPolicySearchService.java | 10 +-
.../rpc/DubboSupplierCallbackService.java | 10 +-
.../AsyncSearchCoordinatorTest.java | 9 +-
.../snapshot}/SnapshotLifecycleTest.java | 6 +-
36 files changed, 277 insertions(+), 138 deletions(-)
delete mode 100644 architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java
rename architecture/src/main/java/com/arch/policy/api/{ => search}/PolicySearchRpcService.java (53%)
create mode 100644 architecture/src/main/java/com/arch/policy/api/search/SupplierCallbackRpcService.java
rename architecture/src/main/java/com/arch/policy/{ => common}/config/PolicySearchConfiguration.java (77%)
rename architecture/src/main/java/com/arch/policy/{ => common/model}/MessagePosition.java (95%)
rename architecture/src/main/java/com/arch/policy/{ => common/model}/PolicyChange.java (96%)
rename architecture/src/main/java/com/arch/policy/{ => common/model}/PolicyRecord.java (95%)
rename architecture/src/main/java/com/arch/policy/{api => common/search}/CallbackResponse.java (88%)
rename architecture/src/main/java/com/arch/policy/{api => common/search}/PolicySearchRequest.java (96%)
rename architecture/src/main/java/com/arch/policy/{api => common/search}/PolicySearchResponse.java (95%)
rename architecture/src/main/java/com/arch/policy/{api => common/search}/SupplierCallbackRequest.java (96%)
rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/AsyncSearchCoordinator.java (93%)
rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/LocalSearchWaiters.java (96%)
rename architecture/src/main/java/com/arch/policy/{ => search}/application/PolicyIncrementalUpdater.java (71%)
rename architecture/src/main/java/com/arch/policy/{ => search}/application/PolicyStartupRunner.java (84%)
rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/SearchState.java (57%)
rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/SearchStateStore.java (91%)
rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/SupplierCallbackService.java (93%)
rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/SupplierTaskDispatcher.java (61%)
rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/ActiveSnapshotRegistry.java (97%)
rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/DefaultSnapshotValidator.java (76%)
rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/FileSystemSnapshotDirectory.java (91%)
rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/PolicySnapshot.java (96%)
rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/PolicySnapshotService.java (98%)
rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/SnapshotBuilder.java (92%)
rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/SnapshotPorts.java (90%)
rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/demo/DemoSupplierTaskDispatcher.java (86%)
rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/kafka/KafkaPolicyChangeListener.java (86%)
rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/kafka/PolicyChangeMessage.java (94%)
rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/redis/RedisSearchStateStore.java (96%)
rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/redis/SearchFinishedSubscriber.java (82%)
rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/rpc/DubboPolicySearchService.java (80%)
rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/rpc/DubboSupplierCallbackService.java (70%)
rename architecture/src/test/java/com/arch/policy/{ => search/application}/AsyncSearchCoordinatorTest.java (95%)
rename architecture/src/test/java/com/arch/policy/{ => search/domain/snapshot}/SnapshotLifecycleTest.java (95%)
diff --git a/architecture/README.md b/architecture/README.md
index ed7e14c..58cde70 100644
--- a/architecture/README.md
+++ b/architecture/README.md
@@ -1,46 +1,168 @@
-# Policy snapshot lifecycle
-
-This module implements version-isolated policy snapshots. Each snapshot owns a RocksDB detail
-store, a bitmap index, and the incremental-message position at which both were built.
-
-## Integration flow
-
-1. Implement `FullPolicyLoader` to stream the full-data cut into `PolicySnapshot.upsert` and
- return the cut's message position.
-2. Implement `IncrementalReplayer` to apply upserts/deletes after that position. The builder
- repeats replay until it reaches a stable latest position.
-3. Supply `DefaultSnapshotValidator` (or a stricter domain validator), a
- `FileSystemSnapshotDirectory`, and construct `SnapshotBuilder`.
-4. Call `PolicySnapshotService.start(version)` during startup. Keep the application's readiness
- probe bound to `service.isReady()`; failures remain unready and retry.
-5. Call `refresh(newVersion)` at runtime. A failed candidate is discarded. A valid candidate is
- atomically activated while the old snapshot continues serving existing leases.
-6. Every search must use `try (SnapshotLease lease = registry.acquire())`. Releasing the last old
- lease closes RocksDB and deletes that retired version's directory.
-
-The module intentionally leaves message-broker and full-data-source clients behind interfaces so
-the snapshot consistency rules are independent of Kafka, HTTP, database, or framework choices.
-
-## Package layout
-
-- `api`: serializable Dubbo contract and request/response DTOs.
-- `rpc`: Dubbo search provider and supplier callback provider.
-- `aggregation`: Redis-backed fan-out/fan-in search coordination and local waiters.
-- `redis`: atomic Lua state transitions and Pub/Sub early wake-up.
-- `demo`: asynchronous downstream supplier simulation.
-- `kafka`: JSON policy-change consumer and message DTO.
-- `application`: startup and incremental-update use cases.
-- root `policy` package: versioned RocksDB/Bitmap snapshot domain and lifecycle.
-
-Kafka messages use a globally monotonic `position` so duplicate/out-of-order delivery is ignored.
-If the topic has multiple partitions, the producer must supply this global sequence; otherwise the
-position model should be replaced with a per-partition offset map.
-
-## Async supplier search
-
-`asyncSearch` initializes pending suppliers, state and result TTL atomically in Redis, registers a
-local waiter, double-checks Redis, then dispatches all supplier tasks. Supplier callbacks append
-result chunks and remove a supplier from the pending set only on its final callback. The Lua script
-sets `COMPLETED` and publishes `search-finished` when the last supplier finishes. Pub/Sub only wakes
-the local waiter early; Redis remains the source of truth and is checked every 200 ms. Timeout is
-also a Lua state transition and returns all partial results already recorded.
+# 架构方案伪代码库
+
+`architecture` module 用于沉淀不同业务场景下的架构思路、设计方案和关键伪代码。它关注的是
+复杂问题如何拆分、核心流程如何协作、数据一致性如何保证,以及不同技术组件之间的边界,而不是提供
+可以直接上线的完整业务系统。
+
+当前实现以机票业务为背景,模拟以下两个相互关联的架构场景:
+
+1. **多供应商并发搜索**:一次机票查询并发分发给多个供应商,通过异步回调聚合结果,并支持超时返回部分结果。
+2. **海量政策高性能匹配**:使用 RocksDB 保存政策明细、RoaringBitmap 构建匹配索引,并通过版本化快照实现运行时无损切换。
+
+后续会继续增加交易、下单等其他场景的架构伪代码,使该 module 逐步形成可复用的架构与设计方案集合。
+
+## 模块目标
+
+- 用最少的代码表达架构中的核心职责、协作关系和一致性约束。
+- 为类似业务问题提供可讨论、可验证、可演进的设计参考。
+- 通过接口隔离外部系统,使方案不被 Dubbo、Kafka、Redis 等具体技术绑定。
+- 通过测试验证关键架构行为,而不是覆盖完整的生产业务流程。
+- 持续积累搜索、政策匹配、交易、下单等不同场景的方案。
+
+本模块属于架构伪代码,生产落地时仍需根据实际情况补充鉴权、限流、熔断、监控、链路追踪、异常分级、
+数据迁移、容量评估、容灾和部署方案。
+
+## 场景目录
+
+| 场景 | 核心问题 | 当前状态 |
+| --- | --- | --- |
+| 多供应商并发搜索 | 任务分发、异步回调、结果聚合、超时与迟到回调 | 已实现伪代码 |
+| 海量政策匹配 | 全量加载、增量追平、位图索引、快照校验与热切换 | 已实现伪代码 |
+| 交易/下单 | 幂等、状态流转、库存确认、分布式一致性与补偿 | 待补充 |
+
+## 分层结构
+
+代码先区分项目级公开契约、公共能力和具体业务场景,再在场景内部按照应用、领域和基础设施分层。
+
+```text
+com.arch.policy
+├── api
+│ └── search # 项目级搜索契约、请求和响应 DTO
+├── common
+│ ├── config # Spring Bean 装配
+│ └── model # 可跨场景复用的模型
+├── search
+│ ├── application # 查询编排、回调处理、启动和增量更新用例
+│ ├── domain
+│ │ └── snapshot # RocksDB/位图快照领域逻辑
+│ └── infrastructure
+│ ├── demo # 模拟供应商
+│ ├── kafka # 政策变更消息适配器
+│ ├── redis # 聚合状态和完成通知适配器
+│ └── rpc # Dubbo 接口实现
+└── PolicySearchApplication # 当前搜索场景的启动入口
+```
+
+依赖方向如下:
+
+```mermaid
+flowchart LR
+ Caller["外部调用方"] --> API["api.search
项目级公开契约"]
+ Infrastructure["search.infrastructure
RPC / Redis / Kafka"] --> API
+ Infrastructure --> Application["search.application
流程编排"]
+ Application --> Domain["search.domain
领域规则"]
+ Application --> Common["common
公共配置与模型"]
+ Domain --> Common
+```
+
+公开 API 不依赖场景内部实现,领域层不依赖 Dubbo、Redis、Kafka 等外部技术。后续增加交易场景时,
+可以平行新增 `api.order` 和 `order.application/domain/infrastructure`,避免交易代码与搜索代码混杂。
+只有真正跨场景稳定复用的模型或能力才应放入 `common`。
+
+## 场景一:多供应商并发搜索
+
+查询服务将一次请求分发给多个供应商。供应商可以分批回调结果,最后一次回调负责声明该供应商完成。
+Redis 保存聚合结果、待完成供应商集合和查询状态,是整个流程的唯一事实来源。
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor Caller as 调用方
+ participant SearchAPI as PolicySearchRpcService
+ participant Coordinator as AsyncSearchCoordinator
+ participant Redis as RedisSearchStateStore
+ participant Dispatcher as SupplierTaskDispatcher
+ participant Supplier as 多个供应商
+ participant CallbackAPI as SupplierCallbackRpcService
+ participant Callback as SupplierCallbackService
+ participant Waiter as LocalSearchWaiters
+
+ Caller->>SearchAPI: asyncSearch(request)
+ SearchAPI->>Coordinator: 执行查询
+ Coordinator->>Redis: 初始化状态、待完成集合和 TTL
+ Coordinator->>Waiter: 注册本地等待器
+ Coordinator->>Dispatcher: 并发分发供应商任务
+ Dispatcher-->>Supplier: 异步查询
+
+ loop 供应商分批返回结果
+ Supplier->>CallbackAPI: callback(result, searchFinished)
+ CallbackAPI->>Callback: 处理回调
+ Callback->>Redis: 追加结果并更新待完成集合
+ end
+
+ Redis-->>Waiter: 全部完成后发布通知
+ Waiter-->>Coordinator: 提前唤醒
+ Coordinator->>Redis: 读取最终状态和结果
+ Coordinator-->>SearchAPI: PolicySearchResponse
+ SearchAPI-->>Caller: 完成异步调用
+```
+
+本地等待器和 Redis Pub/Sub 只用于提前唤醒。即使通知丢失,查询线程仍会定期检查 Redis。超过总超时时间后,
+Lua 脚本会原子地将查询状态更新为 `TIMED_OUT`,返回已经收到的部分结果,并拒绝迟到回调继续修改结果。
+
+## 场景二:海量政策匹配与快照切换
+
+每个政策快照使用独立目录保存 RocksDB 明细库,并在内存中维护 RoaringBitmap 匹配索引。新版本先在旁路
+完成全量加载、增量追平和一致性校验,只有通过校验后才会替换当前服务版本。
+
+```mermaid
+flowchart TD
+ Start["应用启动或运行时刷新"] --> Create["创建版本隔离目录"]
+ Create --> Full["加载全量政策"]
+ Full --> Position["记录全量数据切点"]
+ Position --> Replay["回放切点后的增量消息"]
+ Replay --> CaughtUp{"已追平最新位置?"}
+ CaughtUp -- 否 --> Replay
+ CaughtUp -- 是 --> Validate["校验明细、索引和消息位置"]
+ Validate --> Valid{"校验成功?"}
+ Valid -- 否 --> Discard["关闭并删除候选快照"]
+ Valid -- 是 --> Activate["原子激活新快照"]
+ Activate --> NewQuery["新查询使用新版本"]
+ Activate --> Drain["旧版本等待已有查询释放租约"]
+ Drain --> Delete["关闭并删除旧快照"]
+```
+
+查询通过 `ActiveSnapshotRegistry.acquire()` 获取快照租约,并使用 `try-with-resources` 释放。切换完成后,
+新查询立即使用新快照,旧快照则保留到最后一个旧查询结束,从而避免正在执行的查询访问已关闭的 RocksDB。
+
+快照方案的主要扩展点:
+
+1. `FullPolicyLoader`:加载全量政策,并返回全量数据对应的消息位置。
+2. `IncrementalReplayer`:回放全量切点之后的新增、修改和删除事件。
+3. `SnapshotValidator`:校验政策明细、位图索引和消息位置的一致性。
+4. `SnapshotDirectory`:隔离不同快照版本的存储目录。
+5. `PolicySnapshotService`:负责首次初始化、失败重试和运行时刷新。
+
+Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序消息会被忽略。如果 Topic 使用多个分区,
+生产端必须提供全局序列;否则应将当前位置模型调整为按分区保存 offset。
+
+## 新增架构场景的约定
+
+新增交易、下单或其他架构伪代码时,应遵循以下约定:
+
+1. 在 README 的场景目录中说明要解决的问题、关键约束和方案状态。
+2. 对外契约放在 `api.`,内部实现放在对应的 `` 业务包。
+3. 使用 `application/domain/infrastructure` 表达职责边界,不让领域规则依赖具体中间件。
+4. 优先提供展示关键协作关系的最小实现,避免把伪代码扩展成不完整的生产框架。
+5. 为幂等、一致性、并发、超时、切换和失败隔离等关键架构行为编写测试。
+6. 在场景文档中记录设计取舍、适用边界,以及生产落地仍需补充的能力。
+
+## 运行验证
+
+在仓库根目录执行:
+
+```bash
+mvn -f architecture/pom.xml clean test
+```
+
+当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换以及失败候选版本隔离。
diff --git a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java
deleted file mode 100644
index 1cdff44..0000000
--- a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.arch.policy.api;
-
-public interface SupplierCallbackRpcService {
- CallbackResponse callback(SupplierCallbackRequest request);
-}
diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java b/architecture/src/main/java/com/arch/policy/api/search/PolicySearchRpcService.java
similarity index 53%
rename from architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java
rename to architecture/src/main/java/com/arch/policy/api/search/PolicySearchRpcService.java
index 5ab9e75..98533cf 100644
--- a/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java
+++ b/architecture/src/main/java/com/arch/policy/api/search/PolicySearchRpcService.java
@@ -1,4 +1,7 @@
-package com.arch.policy.api;
+package com.arch.policy.api.search;
+
+import com.arch.policy.common.search.PolicySearchRequest;
+import com.arch.policy.common.search.PolicySearchResponse;
import java.util.concurrent.CompletableFuture;
diff --git a/architecture/src/main/java/com/arch/policy/api/search/SupplierCallbackRpcService.java b/architecture/src/main/java/com/arch/policy/api/search/SupplierCallbackRpcService.java
new file mode 100644
index 0000000..8995d25
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/api/search/SupplierCallbackRpcService.java
@@ -0,0 +1,8 @@
+package com.arch.policy.api.search;
+
+import com.arch.policy.common.search.CallbackResponse;
+import com.arch.policy.common.search.SupplierCallbackRequest;
+
+public interface SupplierCallbackRpcService {
+ CallbackResponse callback(SupplierCallbackRequest request);
+}
diff --git a/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java b/architecture/src/main/java/com/arch/policy/common/config/PolicySearchConfiguration.java
similarity index 77%
rename from architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java
rename to architecture/src/main/java/com/arch/policy/common/config/PolicySearchConfiguration.java
index 03f770c..21191fb 100644
--- a/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java
+++ b/architecture/src/main/java/com/arch/policy/common/config/PolicySearchConfiguration.java
@@ -1,23 +1,23 @@
-package com.arch.policy.config;
-
-import com.arch.policy.ActiveSnapshotRegistry;
-import com.arch.policy.DefaultSnapshotValidator;
-import com.arch.policy.FileSystemSnapshotDirectory;
-import com.arch.policy.PolicySnapshotService;
-import com.arch.policy.SnapshotBuilder;
-import com.arch.policy.SnapshotPorts.FullPolicyLoader;
-import com.arch.policy.SnapshotPorts.IncrementalReplayer;
-import com.arch.policy.application.PolicyIncrementalUpdater;
-import com.arch.policy.application.PolicyStartupRunner;
-import com.arch.policy.aggregation.AsyncSearchCoordinator;
-import com.arch.policy.aggregation.LocalSearchWaiters;
-import com.arch.policy.aggregation.SearchStateStore;
-import com.arch.policy.aggregation.SupplierCallbackService;
-import com.arch.policy.aggregation.SupplierTaskDispatcher;
-import com.arch.policy.demo.DemoSupplierTaskDispatcher;
-import com.arch.policy.kafka.KafkaPolicyChangeListener;
-import com.arch.policy.redis.RedisSearchStateStore;
-import com.arch.policy.redis.SearchFinishedSubscriber;
+package com.arch.policy.common.config;
+
+import com.arch.policy.search.application.AsyncSearchCoordinator;
+import com.arch.policy.search.application.LocalSearchWaiters;
+import com.arch.policy.search.application.PolicyIncrementalUpdater;
+import com.arch.policy.search.application.PolicyStartupRunner;
+import com.arch.policy.search.application.SearchStateStore;
+import com.arch.policy.search.application.SupplierCallbackService;
+import com.arch.policy.search.application.SupplierTaskDispatcher;
+import com.arch.policy.search.domain.snapshot.ActiveSnapshotRegistry;
+import com.arch.policy.search.domain.snapshot.DefaultSnapshotValidator;
+import com.arch.policy.search.domain.snapshot.FileSystemSnapshotDirectory;
+import com.arch.policy.search.domain.snapshot.PolicySnapshotService;
+import com.arch.policy.search.domain.snapshot.SnapshotBuilder;
+import com.arch.policy.search.domain.snapshot.SnapshotPorts.FullPolicyLoader;
+import com.arch.policy.search.domain.snapshot.SnapshotPorts.IncrementalReplayer;
+import com.arch.policy.search.infrastructure.demo.DemoSupplierTaskDispatcher;
+import com.arch.policy.search.infrastructure.kafka.KafkaPolicyChangeListener;
+import com.arch.policy.search.infrastructure.redis.RedisSearchStateStore;
+import com.arch.policy.search.infrastructure.redis.SearchFinishedSubscriber;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/architecture/src/main/java/com/arch/policy/MessagePosition.java b/architecture/src/main/java/com/arch/policy/common/model/MessagePosition.java
similarity index 95%
rename from architecture/src/main/java/com/arch/policy/MessagePosition.java
rename to architecture/src/main/java/com/arch/policy/common/model/MessagePosition.java
index 50794e1..edca20b 100644
--- a/architecture/src/main/java/com/arch/policy/MessagePosition.java
+++ b/architecture/src/main/java/com/arch/policy/common/model/MessagePosition.java
@@ -1,4 +1,4 @@
-package com.arch.policy;
+package com.arch.policy.common.model;
public final class MessagePosition implements Comparable {
public static final MessagePosition BEGINNING = new MessagePosition(0);
diff --git a/architecture/src/main/java/com/arch/policy/PolicyChange.java b/architecture/src/main/java/com/arch/policy/common/model/PolicyChange.java
similarity index 96%
rename from architecture/src/main/java/com/arch/policy/PolicyChange.java
rename to architecture/src/main/java/com/arch/policy/common/model/PolicyChange.java
index 5dfcecc..b014367 100644
--- a/architecture/src/main/java/com/arch/policy/PolicyChange.java
+++ b/architecture/src/main/java/com/arch/policy/common/model/PolicyChange.java
@@ -1,4 +1,4 @@
-package com.arch.policy;
+package com.arch.policy.common.model;
public final class PolicyChange {
public enum Type { UPSERT, DELETE }
diff --git a/architecture/src/main/java/com/arch/policy/PolicyRecord.java b/architecture/src/main/java/com/arch/policy/common/model/PolicyRecord.java
similarity index 95%
rename from architecture/src/main/java/com/arch/policy/PolicyRecord.java
rename to architecture/src/main/java/com/arch/policy/common/model/PolicyRecord.java
index 42371e3..6fbab70 100644
--- a/architecture/src/main/java/com/arch/policy/PolicyRecord.java
+++ b/architecture/src/main/java/com/arch/policy/common/model/PolicyRecord.java
@@ -1,4 +1,4 @@
-package com.arch.policy;
+package com.arch.policy.common.model;
import java.util.Arrays;
import java.util.Collections;
diff --git a/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java b/architecture/src/main/java/com/arch/policy/common/search/CallbackResponse.java
similarity index 88%
rename from architecture/src/main/java/com/arch/policy/api/CallbackResponse.java
rename to architecture/src/main/java/com/arch/policy/common/search/CallbackResponse.java
index 43b5166..f5fccf7 100644
--- a/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java
+++ b/architecture/src/main/java/com/arch/policy/common/search/CallbackResponse.java
@@ -1,4 +1,4 @@
-package com.arch.policy.api;
+package com.arch.policy.common.search;
import java.io.Serializable;
diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java b/architecture/src/main/java/com/arch/policy/common/search/PolicySearchRequest.java
similarity index 96%
rename from architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java
rename to architecture/src/main/java/com/arch/policy/common/search/PolicySearchRequest.java
index fe9a308..6553673 100644
--- a/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java
+++ b/architecture/src/main/java/com/arch/policy/common/search/PolicySearchRequest.java
@@ -1,4 +1,4 @@
-package com.arch.policy.api;
+package com.arch.policy.common.search;
import java.io.Serializable;
import java.util.ArrayList;
diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java b/architecture/src/main/java/com/arch/policy/common/search/PolicySearchResponse.java
similarity index 95%
rename from architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java
rename to architecture/src/main/java/com/arch/policy/common/search/PolicySearchResponse.java
index f8b3ecd..fa0d708 100644
--- a/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java
+++ b/architecture/src/main/java/com/arch/policy/common/search/PolicySearchResponse.java
@@ -1,4 +1,4 @@
-package com.arch.policy.api;
+package com.arch.policy.common.search;
import java.io.Serializable;
import java.util.ArrayList;
diff --git a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java b/architecture/src/main/java/com/arch/policy/common/search/SupplierCallbackRequest.java
similarity index 96%
rename from architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java
rename to architecture/src/main/java/com/arch/policy/common/search/SupplierCallbackRequest.java
index f965f51..a4b5ed2 100644
--- a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java
+++ b/architecture/src/main/java/com/arch/policy/common/search/SupplierCallbackRequest.java
@@ -1,4 +1,4 @@
-package com.arch.policy.api;
+package com.arch.policy.common.search;
import java.io.Serializable;
import java.util.ArrayList;
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java b/architecture/src/main/java/com/arch/policy/search/application/AsyncSearchCoordinator.java
similarity index 93%
rename from architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java
rename to architecture/src/main/java/com/arch/policy/search/application/AsyncSearchCoordinator.java
index a58d313..6491af2 100644
--- a/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java
+++ b/architecture/src/main/java/com/arch/policy/search/application/AsyncSearchCoordinator.java
@@ -1,8 +1,8 @@
-package com.arch.policy.aggregation;
+package com.arch.policy.search.application;
-import com.arch.policy.api.PolicySearchRequest;
-import com.arch.policy.api.PolicySearchResponse;
-import com.arch.policy.api.SupplierCallbackRequest;
+import com.arch.policy.common.search.PolicySearchRequest;
+import com.arch.policy.common.search.PolicySearchResponse;
+import com.arch.policy.common.search.SupplierCallbackRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java b/architecture/src/main/java/com/arch/policy/search/application/LocalSearchWaiters.java
similarity index 96%
rename from architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java
rename to architecture/src/main/java/com/arch/policy/search/application/LocalSearchWaiters.java
index 5ccf471..66c27c7 100644
--- a/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java
+++ b/architecture/src/main/java/com/arch/policy/search/application/LocalSearchWaiters.java
@@ -1,4 +1,4 @@
-package com.arch.policy.aggregation;
+package com.arch.policy.search.application;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
diff --git a/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java b/architecture/src/main/java/com/arch/policy/search/application/PolicyIncrementalUpdater.java
similarity index 71%
rename from architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java
rename to architecture/src/main/java/com/arch/policy/search/application/PolicyIncrementalUpdater.java
index d7d1d35..b8deab9 100644
--- a/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java
+++ b/architecture/src/main/java/com/arch/policy/search/application/PolicyIncrementalUpdater.java
@@ -1,7 +1,7 @@
-package com.arch.policy.application;
+package com.arch.policy.search.application;
-import com.arch.policy.ActiveSnapshotRegistry;
-import com.arch.policy.PolicyChange;
+import com.arch.policy.common.model.PolicyChange;
+import com.arch.policy.search.domain.snapshot.ActiveSnapshotRegistry;
public final class PolicyIncrementalUpdater {
private final ActiveSnapshotRegistry registry;
diff --git a/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java b/architecture/src/main/java/com/arch/policy/search/application/PolicyStartupRunner.java
similarity index 84%
rename from architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java
rename to architecture/src/main/java/com/arch/policy/search/application/PolicyStartupRunner.java
index 58bbf5f..40ec539 100644
--- a/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java
+++ b/architecture/src/main/java/com/arch/policy/search/application/PolicyStartupRunner.java
@@ -1,6 +1,6 @@
-package com.arch.policy.application;
+package com.arch.policy.search.application;
-import com.arch.policy.PolicySnapshotService;
+import com.arch.policy.search.domain.snapshot.PolicySnapshotService;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java b/architecture/src/main/java/com/arch/policy/search/application/SearchState.java
similarity index 57%
rename from architecture/src/main/java/com/arch/policy/aggregation/SearchState.java
rename to architecture/src/main/java/com/arch/policy/search/application/SearchState.java
index fa6ae2e..140010a 100644
--- a/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java
+++ b/architecture/src/main/java/com/arch/policy/search/application/SearchState.java
@@ -1,3 +1,3 @@
-package com.arch.policy.aggregation;
+package com.arch.policy.search.application;
public enum SearchState { WAITING, COMPLETED, TIMED_OUT }
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java b/architecture/src/main/java/com/arch/policy/search/application/SearchStateStore.java
similarity index 91%
rename from architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java
rename to architecture/src/main/java/com/arch/policy/search/application/SearchStateStore.java
index c0a0a33..d4bff02 100644
--- a/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java
+++ b/architecture/src/main/java/com/arch/policy/search/application/SearchStateStore.java
@@ -1,4 +1,4 @@
-package com.arch.policy.aggregation;
+package com.arch.policy.search.application;
import java.util.List;
import java.util.Set;
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java b/architecture/src/main/java/com/arch/policy/search/application/SupplierCallbackService.java
similarity index 93%
rename from architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java
rename to architecture/src/main/java/com/arch/policy/search/application/SupplierCallbackService.java
index 6113c36..c3d8c59 100644
--- a/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java
+++ b/architecture/src/main/java/com/arch/policy/search/application/SupplierCallbackService.java
@@ -1,6 +1,6 @@
-package com.arch.policy.aggregation;
+package com.arch.policy.search.application;
-import com.arch.policy.api.SupplierCallbackRequest;
+import com.arch.policy.common.search.SupplierCallbackRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
public final class SupplierCallbackService {
diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java b/architecture/src/main/java/com/arch/policy/search/application/SupplierTaskDispatcher.java
similarity index 61%
rename from architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java
rename to architecture/src/main/java/com/arch/policy/search/application/SupplierTaskDispatcher.java
index ab43db7..213e734 100644
--- a/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java
+++ b/architecture/src/main/java/com/arch/policy/search/application/SupplierTaskDispatcher.java
@@ -1,6 +1,6 @@
-package com.arch.policy.aggregation;
+package com.arch.policy.search.application;
-import com.arch.policy.api.PolicySearchRequest;
+import com.arch.policy.common.search.PolicySearchRequest;
import java.util.Set;
diff --git a/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/ActiveSnapshotRegistry.java
similarity index 97%
rename from architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java
rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/ActiveSnapshotRegistry.java
index f086e2c..44cb02b 100644
--- a/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java
+++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/ActiveSnapshotRegistry.java
@@ -1,4 +1,4 @@
-package com.arch.policy;
+package com.arch.policy.search.domain.snapshot;
import java.util.concurrent.atomic.AtomicBoolean;
diff --git a/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/DefaultSnapshotValidator.java
similarity index 76%
rename from architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java
rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/DefaultSnapshotValidator.java
index 2c8c848..2c51ddf 100644
--- a/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java
+++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/DefaultSnapshotValidator.java
@@ -1,6 +1,8 @@
-package com.arch.policy;
+package com.arch.policy.search.domain.snapshot;
-import static com.arch.policy.SnapshotPorts.SnapshotValidator;
+import com.arch.policy.common.model.MessagePosition;
+
+import static com.arch.policy.search.domain.snapshot.SnapshotPorts.SnapshotValidator;
/** Baseline invariants; domain-specific checks can be supplied through SnapshotValidator. */
public final class DefaultSnapshotValidator implements SnapshotValidator {
diff --git a/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/FileSystemSnapshotDirectory.java
similarity index 91%
rename from architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java
rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/FileSystemSnapshotDirectory.java
index 64c0bb1..663fed2 100644
--- a/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java
+++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/FileSystemSnapshotDirectory.java
@@ -1,4 +1,4 @@
-package com.arch.policy;
+package com.arch.policy.search.domain.snapshot;
import java.io.IOException;
import java.nio.file.Files;
@@ -6,7 +6,7 @@
import java.util.Comparator;
import java.util.stream.Stream;
-import static com.arch.policy.SnapshotPorts.SnapshotDirectory;
+import static com.arch.policy.search.domain.snapshot.SnapshotPorts.SnapshotDirectory;
/** Keeps every version in an isolated directory and removes abandoned candidates. */
public final class FileSystemSnapshotDirectory implements SnapshotDirectory {
diff --git a/architecture/src/main/java/com/arch/policy/PolicySnapshot.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshot.java
similarity index 96%
rename from architecture/src/main/java/com/arch/policy/PolicySnapshot.java
rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshot.java
index d4e7e53..eb13c1d 100644
--- a/architecture/src/main/java/com/arch/policy/PolicySnapshot.java
+++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshot.java
@@ -1,4 +1,8 @@
-package com.arch.policy;
+package com.arch.policy.search.domain.snapshot;
+
+import com.arch.policy.common.model.MessagePosition;
+import com.arch.policy.common.model.PolicyChange;
+import com.arch.policy.common.model.PolicyRecord;
import org.roaringbitmap.RoaringBitmap;
import org.rocksdb.Options;
diff --git a/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshotService.java
similarity index 98%
rename from architecture/src/main/java/com/arch/policy/PolicySnapshotService.java
rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshotService.java
index d90fac8..f2f35f9 100644
--- a/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java
+++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshotService.java
@@ -1,4 +1,4 @@
-package com.arch.policy;
+package com.arch.policy.search.domain.snapshot;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
diff --git a/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotBuilder.java
similarity index 92%
rename from architecture/src/main/java/com/arch/policy/SnapshotBuilder.java
rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotBuilder.java
index fbd2ccf..49b8797 100644
--- a/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java
+++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotBuilder.java
@@ -1,10 +1,12 @@
-package com.arch.policy;
+package com.arch.policy.search.domain.snapshot;
+
+import com.arch.policy.common.model.MessagePosition;
import org.rocksdb.RocksDBException;
import java.nio.file.Path;
-import static com.arch.policy.SnapshotPorts.*;
+import static com.arch.policy.search.domain.snapshot.SnapshotPorts.*;
/** Builds a candidate without exposing it to queries. */
public final class SnapshotBuilder {
diff --git a/architecture/src/main/java/com/arch/policy/SnapshotPorts.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotPorts.java
similarity index 90%
rename from architecture/src/main/java/com/arch/policy/SnapshotPorts.java
rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotPorts.java
index 9e3c4d4..f41f262 100644
--- a/architecture/src/main/java/com/arch/policy/SnapshotPorts.java
+++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotPorts.java
@@ -1,4 +1,6 @@
-package com.arch.policy;
+package com.arch.policy.search.domain.snapshot;
+
+import com.arch.policy.common.model.MessagePosition;
import java.nio.file.Path;
diff --git a/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/demo/DemoSupplierTaskDispatcher.java
similarity index 86%
rename from architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java
rename to architecture/src/main/java/com/arch/policy/search/infrastructure/demo/DemoSupplierTaskDispatcher.java
index ecb3714..7826bf0 100644
--- a/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java
+++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/demo/DemoSupplierTaskDispatcher.java
@@ -1,9 +1,9 @@
-package com.arch.policy.demo;
+package com.arch.policy.search.infrastructure.demo;
-import com.arch.policy.aggregation.SupplierCallbackService;
-import com.arch.policy.aggregation.SupplierTaskDispatcher;
-import com.arch.policy.api.PolicySearchRequest;
-import com.arch.policy.api.SupplierCallbackRequest;
+import com.arch.policy.search.application.SupplierCallbackService;
+import com.arch.policy.search.application.SupplierTaskDispatcher;
+import com.arch.policy.common.search.PolicySearchRequest;
+import com.arch.policy.common.search.SupplierCallbackRequest;
import java.util.Collections;
import java.util.Set;
diff --git a/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/KafkaPolicyChangeListener.java
similarity index 86%
rename from architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java
rename to architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/KafkaPolicyChangeListener.java
index 595318c..e935aa6 100644
--- a/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java
+++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/KafkaPolicyChangeListener.java
@@ -1,9 +1,9 @@
-package com.arch.policy.kafka;
+package com.arch.policy.search.infrastructure.kafka;
-import com.arch.policy.MessagePosition;
-import com.arch.policy.PolicyChange;
-import com.arch.policy.PolicyRecord;
-import com.arch.policy.application.PolicyIncrementalUpdater;
+import com.arch.policy.common.model.MessagePosition;
+import com.arch.policy.common.model.PolicyChange;
+import com.arch.policy.common.model.PolicyRecord;
+import com.arch.policy.search.application.PolicyIncrementalUpdater;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.kafka.annotation.KafkaListener;
diff --git a/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/PolicyChangeMessage.java
similarity index 94%
rename from architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java
rename to architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/PolicyChangeMessage.java
index 3f539ec..9718d5f 100644
--- a/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java
+++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/PolicyChangeMessage.java
@@ -1,4 +1,4 @@
-package com.arch.policy.kafka;
+package com.arch.policy.search.infrastructure.kafka;
import java.util.ArrayList;
import java.util.List;
diff --git a/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/redis/RedisSearchStateStore.java
similarity index 96%
rename from architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java
rename to architecture/src/main/java/com/arch/policy/search/infrastructure/redis/RedisSearchStateStore.java
index 49547f0..6347e52 100644
--- a/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java
+++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/redis/RedisSearchStateStore.java
@@ -1,7 +1,7 @@
-package com.arch.policy.redis;
+package com.arch.policy.search.infrastructure.redis;
-import com.arch.policy.aggregation.SearchState;
-import com.arch.policy.aggregation.SearchStateStore;
+import com.arch.policy.search.application.SearchState;
+import com.arch.policy.search.application.SearchStateStore;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
diff --git a/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/redis/SearchFinishedSubscriber.java
similarity index 82%
rename from architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java
rename to architecture/src/main/java/com/arch/policy/search/infrastructure/redis/SearchFinishedSubscriber.java
index f8ea514..208c8f0 100644
--- a/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java
+++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/redis/SearchFinishedSubscriber.java
@@ -1,6 +1,6 @@
-package com.arch.policy.redis;
+package com.arch.policy.search.infrastructure.redis;
-import com.arch.policy.aggregation.LocalSearchWaiters;
+import com.arch.policy.search.application.LocalSearchWaiters;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
diff --git a/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboPolicySearchService.java
similarity index 80%
rename from architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java
rename to architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboPolicySearchService.java
index ef51aa6..7fc5cca 100644
--- a/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java
+++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboPolicySearchService.java
@@ -1,9 +1,9 @@
-package com.arch.policy.rpc;
+package com.arch.policy.search.infrastructure.rpc;
-import com.arch.policy.api.PolicySearchRequest;
-import com.arch.policy.api.PolicySearchResponse;
-import com.arch.policy.api.PolicySearchRpcService;
-import com.arch.policy.aggregation.AsyncSearchCoordinator;
+import com.arch.policy.common.search.PolicySearchRequest;
+import com.arch.policy.common.search.PolicySearchResponse;
+import com.arch.policy.api.search.PolicySearchRpcService;
+import com.arch.policy.search.application.AsyncSearchCoordinator;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Qualifier;
diff --git a/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboSupplierCallbackService.java
similarity index 70%
rename from architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java
rename to architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboSupplierCallbackService.java
index f685dc1..ae58dcb 100644
--- a/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java
+++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboSupplierCallbackService.java
@@ -1,9 +1,9 @@
-package com.arch.policy.rpc;
+package com.arch.policy.search.infrastructure.rpc;
-import com.arch.policy.aggregation.SupplierCallbackService;
-import com.arch.policy.api.CallbackResponse;
-import com.arch.policy.api.SupplierCallbackRequest;
-import com.arch.policy.api.SupplierCallbackRpcService;
+import com.arch.policy.search.application.SupplierCallbackService;
+import com.arch.policy.common.search.CallbackResponse;
+import com.arch.policy.common.search.SupplierCallbackRequest;
+import com.arch.policy.api.search.SupplierCallbackRpcService;
import org.apache.dubbo.config.annotation.DubboService;
@DubboService(version = "1.0.0", timeout = 3000)
diff --git a/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java b/architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java
similarity index 95%
rename from architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java
rename to architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java
index fe28969..8117b2a 100644
--- a/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java
+++ b/architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java
@@ -1,9 +1,8 @@
-package com.arch.policy;
+package com.arch.policy.search.application;
-import com.arch.policy.aggregation.*;
-import com.arch.policy.api.PolicySearchRequest;
-import com.arch.policy.api.PolicySearchResponse;
-import com.arch.policy.api.SupplierCallbackRequest;
+import com.arch.policy.common.search.PolicySearchRequest;
+import com.arch.policy.common.search.PolicySearchResponse;
+import com.arch.policy.common.search.SupplierCallbackRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
diff --git a/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java b/architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java
similarity index 95%
rename from architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java
rename to architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java
index 297a7c0..9a1ceaf 100644
--- a/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java
+++ b/architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java
@@ -1,5 +1,7 @@
-package com.arch.policy;
+package com.arch.policy.search.domain.snapshot;
+import com.arch.policy.common.model.MessagePosition;
+import com.arch.policy.common.model.PolicyRecord;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -11,7 +13,7 @@
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
-import static com.arch.policy.SnapshotPorts.*;
+import static com.arch.policy.search.domain.snapshot.SnapshotPorts.*;
import static org.junit.jupiter.api.Assertions.*;
class SnapshotLifecycleTest {
From 4326ef25e694d97b7b9f976a267b360eb071aed7 Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Mon, 20 Jul 2026 21:15:38 +0800
Subject: [PATCH 34/37] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=B8=8B=E5=8D=95?=
=?UTF-8?q?=E6=B5=81=E7=A8=8B=E4=BC=AA=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
architecture/README.md | 59 ++++++++-
.../policy/api/book/BookOrderRpcService.java | 12 ++
.../BookOrderApplicationService.java | 112 ++++++++++++++++++
.../book/application/BookOrderStore.java | 17 +++
.../book/application/OrderOutboxMessage.java | 30 +++++
.../book/application/OrderStateHistory.java | 33 ++++++
.../application/PostTransitionExecutor.java | 8 ++
.../RetryablePostTransitionExecutor.java | 24 ++++
.../application/TransitionCommitResult.java | 24 ++++
.../arch/policy/book/domain/BookOrder.java | 50 ++++++++
.../book/domain/DefaultOrderTransitions.java | 48 ++++++++
.../arch/policy/book/domain/OrderEvent.java | 15 +++
.../arch/policy/book/domain/OrderState.java | 26 ++++
.../policy/book/domain/OrderStateMachine.java | 72 +++++++++++
.../policy/book/domain/OrderTransition.java | 61 ++++++++++
.../book/domain/RequiredAttributeGuard.java | 16 +++
.../book/domain/StateTransitionContext.java | 30 +++++
.../policy/book/domain/TransitionAction.java | 5 +
.../book/domain/TransitionExecution.java | 18 +++
.../policy/book/domain/TransitionGuard.java | 5 +
.../repository/InMemoryBookOrderStore.java | 86 ++++++++++++++
.../InMemoryFailedPostActionStore.java | 37 ++++++
.../rpc/DubboBookOrderService.java | 25 ++++
.../policy/common/book/BookOrderRequest.java | 24 ++++
.../policy/common/book/BookOrderResponse.java | 20 ++++
.../policy/common/book/OrderEventRequest.java | 32 +++++
.../common/config/BookOrderConfiguration.java | 36 ++++++
.../BookOrderApplicationServiceTest.java | 105 ++++++++++++++++
.../RetryablePostTransitionExecutorTest.java | 40 +++++++
.../book/domain/OrderStateMachineTest.java | 89 ++++++++++++++
30 files changed, 1157 insertions(+), 2 deletions(-)
create mode 100644 architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderState.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java
create mode 100644 architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java
create mode 100644 architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java
create mode 100644 architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java
diff --git a/architecture/README.md b/architecture/README.md
index 58cde70..04f7058 100644
--- a/architecture/README.md
+++ b/architecture/README.md
@@ -28,7 +28,7 @@
| --- | --- | --- |
| 多供应商并发搜索 | 任务分发、异步回调、结果聚合、超时与迟到回调 | 已实现伪代码 |
| 海量政策匹配 | 全量加载、增量追平、位图索引、快照校验与热切换 | 已实现伪代码 |
-| 交易/下单 | 幂等、状态流转、库存确认、分布式一致性与补偿 | 待补充 |
+| 交易/下单 | 幂等下单、受控状态流转、乐观并发与失败隔离 | 已实现核心伪代码 |
## 分层结构
@@ -37,6 +37,7 @@
```text
com.arch.policy
├── api
+│ ├── book # 下单及订单状态变更契约
│ └── search # 项目级搜索契约、请求和响应 DTO
├── common
│ ├── config # Spring Bean 装配
@@ -50,6 +51,10 @@ com.arch.policy
│ ├── kafka # 政策变更消息适配器
│ ├── redis # 聚合状态和完成通知适配器
│ └── rpc # Dubbo 接口实现
+├── book
+│ ├── application # 幂等下单、状态变更用例和持久化端口
+│ ├── domain # 订单聚合、状态枚举和合法迁移规则
+│ └── infrastructure # 内存仓储示例和 Dubbo 适配器
└── PolicySearchApplication # 当前搜索场景的启动入口
```
@@ -146,6 +151,55 @@ flowchart TD
Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序消息会被忽略。如果 Topic 使用多个分区,
生产端必须提供全局序列;否则应将当前位置模型调整为按分区保存 offset。
+## 场景三:幂等下单与订单状态机
+
+`BookOrderRpcService.createOrder` 以调用方生成的 `requestId` 作为幂等键。订单通过 `CREATE_SUCCEEDED` 事件
+从 `CREATE` 进入 `WAIT_PAY`。重复请求返回第一次创建的订单,不重复生成业务单。生产实现应在数据库中为
+`request_id` 建唯一索引,并把创单与初始状态流转放入本地事务。
+
+外部系统通过 `BookOrderRpcService.fireEvent` 提交支付、出票、验真、取消等业务事件,不能直接指定目标状态。
+`OrderStateMachine` 使用 `(当前状态, 业务事件)` 定位唯一迁移,状态参考 `ipolicytradecore`:
+
+```mermaid
+stateDiagram-v2
+ CREATE --> WAIT_PAY: CREATE_SUCCEEDED
+ CREATE --> CREATE_FAIL: CREATE_FAILED
+ CREATE --> CANCEL: CANCEL
+ WAIT_PAY --> BOOKING: PAY_SUCCEEDED
+ WAIT_PAY --> CANCEL: CANCEL
+ BOOKING --> BOOKED: BOOK_SUCCEEDED
+ BOOKING --> BOOK_FAIL: BOOK_FAILED
+ BOOKING --> VALIDATING: START_VALIDATE
+ BOOKED --> VALIDATING: START_VALIDATE
+ BOOKED --> CANCEL: CANCEL
+ BOOKED --> REFUNDED: REFUND_SUCCEEDED
+ BOOK_FAIL --> VALIDATING: START_VALIDATE
+ BOOK_FAIL --> CANCEL: CANCEL
+ VALIDATING --> BOOKED: VALIDATE_SUCCEEDED
+ VALIDATING --> VALIDATE_FAIL: VALIDATE_FAILED
+ VALIDATE_FAIL --> BOOKED: VALIDATE_SUCCEEDED
+ VALIDATE_FAIL --> CANCEL: CANCEL
+ CREATE_FAIL --> DELETED: DELETE
+ BOOK_FAIL --> DELETED: DELETE
+ CANCEL --> DELETED: DELETE
+ REFUNDED --> DELETED: DELETE
+```
+
+一次迁移依次执行 Guard、前置 Action、领域状态变更、原子持久化和后置 Action。`PAY_SUCCEEDED`、
+`BOOK_SUCCEEDED` 等关键事件通过 Guard 校验支付单号、PNR 等业务凭据;业务可以通过迁移 Builder 注册更多
+风控校验、库存检查和任务创建处理器,而不修改状态机引擎。
+
+状态事件携带全局唯一 `eventId` 和 `expectedVersion`。仓储在一个事务边界内完成以下写入:
+
+1. 使用 `order_no + version` compare-and-set 更新订单,阻止并发覆盖。
+2. 保存 `eventId` 处理结果,重复消息直接返回第一次处理的快照。
+3. 追加包含 `from/event/to/operator` 的完整状态历史。
+4. 写入 Outbox 消息,由独立发布任务可靠投递给库存、支付、出票等下游。
+
+后置 Action 仅在事务提交后执行,失败会进入 `FailedPostActionStore` 等待重试,不会把已提交订单回滚成旧状态。
+示例使用内存实现展示原子语义;生产落地应使用数据库唯一索引、条件更新、状态历史表、Outbox 表和重试任务,
+并由消息消费方继续按照 `eventId` 幂等。
+
## 新增架构场景的约定
新增交易、下单或其他架构伪代码时,应遵循以下约定:
@@ -165,4 +219,5 @@ Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序
mvn -f architecture/pom.xml clean test
```
-当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换以及失败候选版本隔离。
+当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换、幂等下单、事件驱动
+状态迁移、Guard/Action 执行顺序、事件幂等、乐观并发、状态历史和 Outbox 原子记录。
diff --git a/architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java b/architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java
new file mode 100644
index 0000000..ff75a2c
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java
@@ -0,0 +1,12 @@
+package com.arch.policy.api.book;
+
+import com.arch.policy.common.book.BookOrderRequest;
+import com.arch.policy.common.book.BookOrderResponse;
+import com.arch.policy.common.book.OrderEventRequest;
+
+public interface BookOrderRpcService {
+
+ BookOrderResponse createOrder(BookOrderRequest request);
+
+ BookOrderResponse fireEvent(OrderEventRequest request);
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java b/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java
new file mode 100644
index 0000000..9531a45
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java
@@ -0,0 +1,112 @@
+package com.arch.policy.book.application;
+
+import com.arch.policy.book.domain.BookOrder;
+import com.arch.policy.book.domain.OrderEvent;
+import com.arch.policy.book.domain.OrderState;
+import com.arch.policy.book.domain.OrderStateMachine;
+import com.arch.policy.book.domain.StateTransitionContext;
+import com.arch.policy.book.domain.TransitionExecution;
+import com.arch.policy.common.book.BookOrderRequest;
+import com.arch.policy.common.book.BookOrderResponse;
+import com.arch.policy.common.book.OrderEventRequest;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.UUID;
+
+public final class BookOrderApplicationService {
+ private final BookOrderStore store;
+ private final OrderStateMachine stateMachine;
+ private final PostTransitionExecutor postTransitionExecutor;
+
+ public BookOrderApplicationService(BookOrderStore store, OrderStateMachine stateMachine,
+ PostTransitionExecutor postTransitionExecutor) {
+ this.store = store;
+ this.stateMachine = stateMachine;
+ this.postTransitionExecutor = postTransitionExecutor;
+ }
+
+ public BookOrderResponse createOrder(BookOrderRequest request) {
+ validate(request);
+ BookOrder existing = store.findByRequestId(request.getRequestId());
+ if (existing != null) return response(existing);
+
+ BookOrder order = BookOrder.create(newOrderNo(), request.getRequestId(), request.getCustomerId(),
+ request.getProductId(), request.getQuantity(), request.getAmount());
+ TransitionExecution execution = stateMachine.fire(new StateTransitionContext(
+ "CREATE:" + request.getRequestId(), OrderEvent.CREATE_SUCCEEDED, order,
+ request.getCustomerId(), Collections.emptyMap()));
+ TransitionCommitResult result = store.create(order, history(execution), outbox(execution));
+ runPostActionsIfCommitted(result, execution);
+ return response(result.getOrder());
+ }
+
+ public BookOrderResponse fireEvent(OrderEventRequest request) {
+ validate(request);
+ BookOrder processed = store.findByEventId(request.getEventId());
+ if (processed != null) return response(processed);
+ BookOrder order = store.findByOrderNo(request.getOrderNo());
+ if (order == null) throw new IllegalArgumentException("order not found: " + request.getOrderNo());
+ if (order.getVersion() != request.getExpectedVersion()) {
+ throw new IllegalStateException("stale order version: " + request.getExpectedVersion());
+ }
+ OrderEvent event = parseEvent(request.getEvent());
+ TransitionExecution execution = stateMachine.fire(new StateTransitionContext(
+ request.getEventId(), event, order, request.getOperator(), request.getAttributes()));
+ TransitionCommitResult result = store.transit(order, execution.getPreviousVersion(),
+ history(execution), outbox(execution));
+ runPostActionsIfCommitted(result, execution);
+ return response(result.getOrder());
+ }
+
+ private void runPostActionsIfCommitted(TransitionCommitResult result, TransitionExecution execution) {
+ if (!result.isDuplicateEvent()) postTransitionExecutor.execute(stateMachine, execution);
+ }
+
+ private static OrderStateHistory history(TransitionExecution execution) {
+ return new OrderStateHistory(execution.getContext().getEventId(),
+ execution.getContext().getOrder().getOrderNo(), execution.getTransition().getFrom(),
+ execution.getTransition().getEvent(), execution.getTransition().getTo(),
+ execution.getContext().getOperator(), System.currentTimeMillis());
+ }
+
+ private static OrderOutboxMessage outbox(TransitionExecution execution) {
+ BookOrder order = execution.getContext().getOrder();
+ return new OrderOutboxMessage(execution.getContext().getEventId(), order.getOrderNo(),
+ execution.getContext().getEvent(), order.getState(), order.getVersion());
+ }
+
+ private static void validate(BookOrderRequest request) {
+ if (request == null) throw new IllegalArgumentException("request is required");
+ if (isBlank(request.getRequestId())) throw new IllegalArgumentException("requestId is required");
+ if (isBlank(request.getCustomerId())) throw new IllegalArgumentException("customerId is required");
+ if (isBlank(request.getProductId())) throw new IllegalArgumentException("productId is required");
+ if (request.getQuantity() <= 0) throw new IllegalArgumentException("quantity must be positive");
+ if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("amount must not be negative");
+ }
+ }
+
+ private static void validate(OrderEventRequest request) {
+ if (request == null || isBlank(request.getEventId()) || isBlank(request.getOrderNo())
+ || isBlank(request.getEvent()) || isBlank(request.getOperator())) {
+ throw new IllegalArgumentException("eventId, orderNo, event and operator are required");
+ }
+ }
+
+ private static OrderEvent parseEvent(String event) {
+ try { return OrderEvent.valueOf(event.trim().toUpperCase()); }
+ catch (IllegalArgumentException failure) {
+ throw new IllegalArgumentException("unknown order event: " + event, failure);
+ }
+ }
+
+ private static String newOrderNo() {
+ return "BO" + UUID.randomUUID().toString().replace("-", "").substring(0, 20).toUpperCase();
+ }
+
+ private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); }
+ private static BookOrderResponse response(BookOrder order) {
+ return new BookOrderResponse(order.getOrderNo(), order.getState().name(), order.getVersion());
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java b/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java
new file mode 100644
index 0000000..5e69210
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java
@@ -0,0 +1,17 @@
+package com.arch.policy.book.application;
+
+import com.arch.policy.book.domain.BookOrder;
+
+public interface BookOrderStore {
+ BookOrder findByOrderNo(String orderNo);
+
+ BookOrder findByRequestId(String requestId);
+
+ BookOrder findByEventId(String eventId);
+
+ TransitionCommitResult create(BookOrder order, OrderStateHistory history,
+ OrderOutboxMessage outboxMessage);
+
+ TransitionCommitResult transit(BookOrder order, long expectedVersion,
+ OrderStateHistory history, OrderOutboxMessage outboxMessage);
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java b/architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java
new file mode 100644
index 0000000..9e0b4c3
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java
@@ -0,0 +1,30 @@
+package com.arch.policy.book.application;
+
+import com.arch.policy.book.domain.OrderEvent;
+import com.arch.policy.book.domain.OrderState;
+
+public final class OrderOutboxMessage {
+ private final String eventId;
+ private final String orderNo;
+ private final OrderEvent event;
+ private final OrderState state;
+ private final long orderVersion;
+ private boolean published;
+
+ public OrderOutboxMessage(String eventId, String orderNo, OrderEvent event,
+ OrderState state, long orderVersion) {
+ this.eventId = eventId;
+ this.orderNo = orderNo;
+ this.event = event;
+ this.state = state;
+ this.orderVersion = orderVersion;
+ }
+
+ public String getEventId() { return eventId; }
+ public String getOrderNo() { return orderNo; }
+ public OrderEvent getEvent() { return event; }
+ public OrderState getState() { return state; }
+ public long getOrderVersion() { return orderVersion; }
+ public boolean isPublished() { return published; }
+ public void markPublished() { published = true; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java b/architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java
new file mode 100644
index 0000000..38e8fab
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java
@@ -0,0 +1,33 @@
+package com.arch.policy.book.application;
+
+import com.arch.policy.book.domain.OrderEvent;
+import com.arch.policy.book.domain.OrderState;
+
+public final class OrderStateHistory {
+ private final String eventId;
+ private final String orderNo;
+ private final OrderState from;
+ private final OrderEvent event;
+ private final OrderState to;
+ private final String operator;
+ private final long occurredAtMillis;
+
+ public OrderStateHistory(String eventId, String orderNo, OrderState from, OrderEvent event,
+ OrderState to, String operator, long occurredAtMillis) {
+ this.eventId = eventId;
+ this.orderNo = orderNo;
+ this.from = from;
+ this.event = event;
+ this.to = to;
+ this.operator = operator;
+ this.occurredAtMillis = occurredAtMillis;
+ }
+
+ public String getEventId() { return eventId; }
+ public String getOrderNo() { return orderNo; }
+ public OrderState getFrom() { return from; }
+ public OrderEvent getEvent() { return event; }
+ public OrderState getTo() { return to; }
+ public String getOperator() { return operator; }
+ public long getOccurredAtMillis() { return occurredAtMillis; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java b/architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java
new file mode 100644
index 0000000..74e5d54
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java
@@ -0,0 +1,8 @@
+package com.arch.policy.book.application;
+
+import com.arch.policy.book.domain.OrderStateMachine;
+import com.arch.policy.book.domain.TransitionExecution;
+
+public interface PostTransitionExecutor {
+ void execute(OrderStateMachine stateMachine, TransitionExecution execution);
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java b/architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java
new file mode 100644
index 0000000..3043b16
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java
@@ -0,0 +1,24 @@
+package com.arch.policy.book.application;
+
+import com.arch.policy.book.domain.OrderStateMachine;
+import com.arch.policy.book.domain.TransitionExecution;
+
+public final class RetryablePostTransitionExecutor implements PostTransitionExecutor {
+ private final FailedPostActionStore failedActionStore;
+
+ public RetryablePostTransitionExecutor(FailedPostActionStore failedActionStore) {
+ this.failedActionStore = failedActionStore;
+ }
+
+ @Override public void execute(OrderStateMachine stateMachine, TransitionExecution execution) {
+ try {
+ stateMachine.afterCommit(execution);
+ } catch (RuntimeException failure) {
+ failedActionStore.record(execution, failure);
+ }
+ }
+
+ public interface FailedPostActionStore {
+ void record(TransitionExecution execution, RuntimeException failure);
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java b/architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java
new file mode 100644
index 0000000..6564e33
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java
@@ -0,0 +1,24 @@
+package com.arch.policy.book.application;
+
+import com.arch.policy.book.domain.BookOrder;
+
+public final class TransitionCommitResult {
+ private final BookOrder order;
+ private final boolean duplicateEvent;
+
+ private TransitionCommitResult(BookOrder order, boolean duplicateEvent) {
+ this.order = order;
+ this.duplicateEvent = duplicateEvent;
+ }
+
+ public static TransitionCommitResult committed(BookOrder order) {
+ return new TransitionCommitResult(order, false);
+ }
+
+ public static TransitionCommitResult duplicate(BookOrder order) {
+ return new TransitionCommitResult(order, true);
+ }
+
+ public BookOrder getOrder() { return order; }
+ public boolean isDuplicateEvent() { return duplicateEvent; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java b/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java
new file mode 100644
index 0000000..3daa8f3
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java
@@ -0,0 +1,50 @@
+package com.arch.policy.book.domain;
+
+import java.math.BigDecimal;
+
+public final class BookOrder {
+ private final String orderNo;
+ private final String requestId;
+ private final String customerId;
+ private final String productId;
+ private final int quantity;
+ private final BigDecimal amount;
+ private OrderState state;
+ private long version;
+
+ private BookOrder(String orderNo, String requestId, String customerId, String productId,
+ int quantity, BigDecimal amount, OrderState state, long version) {
+ this.orderNo = orderNo;
+ this.requestId = requestId;
+ this.customerId = customerId;
+ this.productId = productId;
+ this.quantity = quantity;
+ this.amount = amount;
+ this.state = state;
+ this.version = version;
+ }
+
+ public static BookOrder create(String orderNo, String requestId, String customerId,
+ String productId, int quantity, BigDecimal amount) {
+ return new BookOrder(orderNo, requestId, customerId, productId, quantity, amount,
+ OrderState.CREATE, 0L);
+ }
+
+ public BookOrder copy() {
+ return new BookOrder(orderNo, requestId, customerId, productId, quantity, amount, state, version);
+ }
+
+ void applyState(OrderState target) {
+ state = target;
+ version++;
+ }
+
+ public String getOrderNo() { return orderNo; }
+ public String getRequestId() { return requestId; }
+ public String getCustomerId() { return customerId; }
+ public String getProductId() { return productId; }
+ public int getQuantity() { return quantity; }
+ public BigDecimal getAmount() { return amount; }
+ public OrderState getState() { return state; }
+ public long getVersion() { return version; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java b/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java
new file mode 100644
index 0000000..bab4e01
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java
@@ -0,0 +1,48 @@
+package com.arch.policy.book.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class DefaultOrderTransitions {
+ private DefaultOrderTransitions() {}
+
+ public static List definitions() {
+ List transitions = new ArrayList();
+ add(transitions, OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY);
+ add(transitions, OrderState.CREATE, OrderEvent.CREATE_FAILED, OrderState.CREATE_FAIL);
+ add(transitions, OrderState.CREATE, OrderEvent.CANCEL, OrderState.CANCEL);
+ transitions.add(OrderTransition
+ .from(OrderState.WAIT_PAY, OrderEvent.PAY_SUCCEEDED, OrderState.BOOKING)
+ .guard(new RequiredAttributeGuard("paymentNo")).build());
+ add(transitions, OrderState.WAIT_PAY, OrderEvent.CANCEL, OrderState.CANCEL);
+ transitions.add(OrderTransition
+ .from(OrderState.BOOKING, OrderEvent.BOOK_SUCCEEDED, OrderState.BOOKED)
+ .guard(new RequiredAttributeGuard("bookingReference")).build());
+ transitions.add(OrderTransition
+ .from(OrderState.BOOKING, OrderEvent.BOOK_FAILED, OrderState.BOOK_FAIL)
+ .guard(new RequiredAttributeGuard("failureCode")).build());
+ add(transitions, OrderState.BOOKING, OrderEvent.START_VALIDATE, OrderState.VALIDATING);
+ add(transitions, OrderState.BOOK_FAIL, OrderEvent.START_VALIDATE, OrderState.VALIDATING);
+ add(transitions, OrderState.BOOK_FAIL, OrderEvent.CANCEL, OrderState.CANCEL);
+ add(transitions, OrderState.BOOK_FAIL, OrderEvent.DELETE, OrderState.DELETED);
+ add(transitions, OrderState.BOOKED, OrderEvent.START_VALIDATE, OrderState.VALIDATING);
+ add(transitions, OrderState.BOOKED, OrderEvent.CANCEL, OrderState.CANCEL);
+ transitions.add(OrderTransition
+ .from(OrderState.BOOKED, OrderEvent.REFUND_SUCCEEDED, OrderState.REFUNDED)
+ .guard(new RequiredAttributeGuard("refundNo")).build());
+ add(transitions, OrderState.VALIDATING, OrderEvent.VALIDATE_SUCCEEDED, OrderState.BOOKED);
+ add(transitions, OrderState.VALIDATING, OrderEvent.VALIDATE_FAILED, OrderState.VALIDATE_FAIL);
+ add(transitions, OrderState.VALIDATE_FAIL, OrderEvent.VALIDATE_SUCCEEDED, OrderState.BOOKED);
+ add(transitions, OrderState.VALIDATE_FAIL, OrderEvent.CANCEL, OrderState.CANCEL);
+ add(transitions, OrderState.CREATE_FAIL, OrderEvent.CANCEL, OrderState.CANCEL);
+ add(transitions, OrderState.CREATE_FAIL, OrderEvent.DELETE, OrderState.DELETED);
+ add(transitions, OrderState.CANCEL, OrderEvent.DELETE, OrderState.DELETED);
+ add(transitions, OrderState.REFUNDED, OrderEvent.DELETE, OrderState.DELETED);
+ return transitions;
+ }
+
+ private static void add(List transitions, OrderState from,
+ OrderEvent event, OrderState to) {
+ transitions.add(OrderTransition.from(from, event, to).build());
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java
new file mode 100644
index 0000000..a32a008
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java
@@ -0,0 +1,15 @@
+package com.arch.policy.book.domain;
+
+public enum OrderEvent {
+ CREATE_SUCCEEDED,
+ CREATE_FAILED,
+ PAY_SUCCEEDED,
+ BOOK_SUCCEEDED,
+ BOOK_FAILED,
+ START_VALIDATE,
+ VALIDATE_SUCCEEDED,
+ VALIDATE_FAILED,
+ CANCEL,
+ REFUND_SUCCEEDED,
+ DELETE
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderState.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderState.java
new file mode 100644
index 0000000..a747609
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/OrderState.java
@@ -0,0 +1,26 @@
+package com.arch.policy.book.domain;
+
+public enum OrderState {
+ CREATE(0, "创单"),
+ WAIT_PAY(1, "待支付"),
+ CREATE_FAIL(2, "创单失败"),
+ CANCEL(3, "已取消"),
+ BOOKING(4, "预定中"),
+ BOOKED(5, "已预定"),
+ BOOK_FAIL(6, "预定失败"),
+ DELETED(7, "已删除"),
+ REFUNDED(8, "已退订"),
+ VALIDATING(10, "验真中"),
+ VALIDATE_FAIL(11, "验真失败");
+
+ private final int code;
+ private final String description;
+
+ OrderState(int code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ public int getCode() { return code; }
+ public String getDescription() { return description; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java
new file mode 100644
index 0000000..7d5b19d
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java
@@ -0,0 +1,72 @@
+package com.arch.policy.book.domain;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class OrderStateMachine {
+ private final Map transitions;
+
+ public OrderStateMachine(List definitions) {
+ Map registry = new HashMap();
+ for (OrderTransition transition : definitions) {
+ TransitionKey key = new TransitionKey(transition.getFrom(), transition.getEvent());
+ if (registry.put(key, transition) != null) {
+ throw new IllegalStateException("duplicate order transition: " + key);
+ }
+ }
+ transitions = Collections.unmodifiableMap(registry);
+ }
+
+ public TransitionExecution fire(StateTransitionContext context) {
+ OrderTransition transition = transitions.get(
+ new TransitionKey(context.getOrder().getState(), context.getEvent()));
+ if (transition == null) {
+ throw new IllegalStateException("unsupported order event " + context.getEvent()
+ + " in state " + context.getOrder().getState());
+ }
+ executeGuards(transition, context);
+ executeActions(transition.getPreActions(), context);
+ long previousVersion = context.getOrder().getVersion();
+ context.getOrder().applyState(transition.getTo());
+ return new TransitionExecution(transition, context, previousVersion);
+ }
+
+ public void afterCommit(TransitionExecution execution) {
+ executeActions(execution.getTransition().getPostActions(), execution.getContext());
+ }
+
+ public OrderState targetOf(OrderState state, OrderEvent event) {
+ OrderTransition transition = transitions.get(new TransitionKey(state, event));
+ return transition == null ? null : transition.getTo();
+ }
+
+ private static void executeGuards(OrderTransition transition, StateTransitionContext context) {
+ for (TransitionGuard guard : transition.getGuards()) guard.check(context);
+ }
+
+ private static void executeActions(List actions, StateTransitionContext context) {
+ for (TransitionAction action : actions) action.execute(context);
+ }
+
+ private static final class TransitionKey {
+ private final OrderState state;
+ private final OrderEvent event;
+
+ private TransitionKey(OrderState state, OrderEvent event) {
+ this.state = state;
+ this.event = event;
+ }
+
+ @Override public boolean equals(Object other) {
+ if (this == other) return true;
+ if (!(other instanceof TransitionKey)) return false;
+ TransitionKey that = (TransitionKey) other;
+ return state == that.state && event == that.event;
+ }
+
+ @Override public int hashCode() { return 31 * state.hashCode() + event.hashCode(); }
+ @Override public String toString() { return state + " + " + event; }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java
new file mode 100644
index 0000000..4ae61df
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java
@@ -0,0 +1,61 @@
+package com.arch.policy.book.domain;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class OrderTransition {
+ private final OrderState from;
+ private final OrderEvent event;
+ private final OrderState to;
+ private final List guards;
+ private final List preActions;
+ private final List postActions;
+
+ private OrderTransition(Builder builder) {
+ from = builder.from;
+ event = builder.event;
+ to = builder.to;
+ guards = immutableCopy(builder.guards);
+ preActions = immutableCopy(builder.preActions);
+ postActions = immutableCopy(builder.postActions);
+ }
+
+ public static Builder from(OrderState from, OrderEvent event, OrderState to) {
+ return new Builder(from, event, to);
+ }
+
+ public OrderState getFrom() { return from; }
+ public OrderEvent getEvent() { return event; }
+ public OrderState getTo() { return to; }
+ public List getGuards() { return guards; }
+ public List getPreActions() { return preActions; }
+ public List getPostActions() { return postActions; }
+
+ private static List immutableCopy(List values) {
+ return Collections.unmodifiableList(new ArrayList(values));
+ }
+
+ public static final class Builder {
+ private final OrderState from;
+ private final OrderEvent event;
+ private final OrderState to;
+ private final List guards = new ArrayList();
+ private final List preActions = new ArrayList();
+ private final List postActions = new ArrayList();
+
+ private Builder(OrderState from, OrderEvent event, OrderState to) {
+ if (from == null || event == null || to == null) {
+ throw new IllegalArgumentException("from, event and to are required");
+ }
+ this.from = from;
+ this.event = event;
+ this.to = to;
+ }
+
+ public Builder guard(TransitionGuard guard) { guards.add(guard); return this; }
+ public Builder before(TransitionAction action) { preActions.add(action); return this; }
+ public Builder after(TransitionAction action) { postActions.add(action); return this; }
+ public OrderTransition build() { return new OrderTransition(this); }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java b/architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java
new file mode 100644
index 0000000..b636a00
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java
@@ -0,0 +1,16 @@
+package com.arch.policy.book.domain;
+
+public final class RequiredAttributeGuard implements TransitionGuard {
+ private final String attributeName;
+
+ public RequiredAttributeGuard(String attributeName) {
+ this.attributeName = attributeName;
+ }
+
+ @Override public void check(StateTransitionContext context) {
+ String value = context.attribute(attributeName);
+ if (value == null || value.trim().isEmpty()) {
+ throw new IllegalStateException(attributeName + " is required for event " + context.getEvent());
+ }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java b/architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java
new file mode 100644
index 0000000..3269260
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java
@@ -0,0 +1,30 @@
+package com.arch.policy.book.domain;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class StateTransitionContext {
+ private final String eventId;
+ private final OrderEvent event;
+ private final BookOrder order;
+ private final String operator;
+ private final Map attributes;
+
+ public StateTransitionContext(String eventId, OrderEvent event, BookOrder order,
+ String operator, Map attributes) {
+ this.eventId = eventId;
+ this.event = event;
+ this.order = order;
+ this.operator = operator;
+ this.attributes = attributes == null ? Collections.emptyMap()
+ : Collections.unmodifiableMap(new HashMap(attributes));
+ }
+
+ public String getEventId() { return eventId; }
+ public OrderEvent getEvent() { return event; }
+ public BookOrder getOrder() { return order; }
+ public String getOperator() { return operator; }
+ public Map getAttributes() { return attributes; }
+ public String attribute(String name) { return attributes.get(name); }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java b/architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java
new file mode 100644
index 0000000..a035155
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java
@@ -0,0 +1,5 @@
+package com.arch.policy.book.domain;
+
+public interface TransitionAction {
+ void execute(StateTransitionContext context);
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java b/architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java
new file mode 100644
index 0000000..1e4a0c8
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java
@@ -0,0 +1,18 @@
+package com.arch.policy.book.domain;
+
+public final class TransitionExecution {
+ private final OrderTransition transition;
+ private final StateTransitionContext context;
+ private final long previousVersion;
+
+ public TransitionExecution(OrderTransition transition, StateTransitionContext context,
+ long previousVersion) {
+ this.transition = transition;
+ this.context = context;
+ this.previousVersion = previousVersion;
+ }
+
+ public OrderTransition getTransition() { return transition; }
+ public StateTransitionContext getContext() { return context; }
+ public long getPreviousVersion() { return previousVersion; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java b/architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java
new file mode 100644
index 0000000..5c88ebe
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java
@@ -0,0 +1,5 @@
+package com.arch.policy.book.domain;
+
+public interface TransitionGuard {
+ void check(StateTransitionContext context);
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java
new file mode 100644
index 0000000..a76b4af
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java
@@ -0,0 +1,86 @@
+package com.arch.policy.book.infrastructure.repository;
+
+import com.arch.policy.book.application.BookOrderStore;
+import com.arch.policy.book.application.OrderOutboxMessage;
+import com.arch.policy.book.application.OrderStateHistory;
+import com.arch.policy.book.application.TransitionCommitResult;
+import com.arch.policy.book.domain.BookOrder;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class InMemoryBookOrderStore implements BookOrderStore {
+ private final Map orders = new HashMap();
+ private final Map requestIndexes = new HashMap();
+ private final Map eventResults = new HashMap();
+ private final List histories = new ArrayList();
+ private final List outboxMessages = new ArrayList();
+
+ @Override public synchronized BookOrder findByOrderNo(String orderNo) {
+ return copy(orders.get(orderNo));
+ }
+
+ @Override public synchronized BookOrder findByRequestId(String requestId) {
+ String orderNo = requestIndexes.get(requestId);
+ return orderNo == null ? null : copy(orders.get(orderNo));
+ }
+
+ @Override public synchronized BookOrder findByEventId(String eventId) {
+ return copy(eventResults.get(eventId));
+ }
+
+ @Override public synchronized TransitionCommitResult create(
+ BookOrder order, OrderStateHistory history, OrderOutboxMessage outboxMessage) {
+ String existingOrderNo = requestIndexes.get(order.getRequestId());
+ if (existingOrderNo != null) {
+ return TransitionCommitResult.duplicate(copy(orders.get(existingOrderNo)));
+ }
+ persist(order, history, outboxMessage);
+ requestIndexes.put(order.getRequestId(), order.getOrderNo());
+ return TransitionCommitResult.committed(order.copy());
+ }
+
+ @Override public synchronized TransitionCommitResult transit(
+ BookOrder order, long expectedVersion, OrderStateHistory history,
+ OrderOutboxMessage outboxMessage) {
+ BookOrder processed = eventResults.get(history.getEventId());
+ if (processed != null) return TransitionCommitResult.duplicate(processed.copy());
+
+ BookOrder current = orders.get(order.getOrderNo());
+ if (current == null || current.getVersion() != expectedVersion
+ || order.getVersion() != expectedVersion + 1) {
+ throw new IllegalStateException("concurrent order update: " + order.getOrderNo());
+ }
+ persist(order, history, outboxMessage);
+ return TransitionCommitResult.committed(order.copy());
+ }
+
+ public synchronized List historiesOf(String orderNo) {
+ List result = new ArrayList();
+ for (OrderStateHistory history : histories) {
+ if (orderNo.equals(history.getOrderNo())) result.add(history);
+ }
+ return Collections.unmodifiableList(result);
+ }
+
+ public synchronized List unpublishedOutboxMessages() {
+ List result = new ArrayList();
+ for (OrderOutboxMessage message : outboxMessages) {
+ if (!message.isPublished()) result.add(message);
+ }
+ return Collections.unmodifiableList(result);
+ }
+
+ private void persist(BookOrder order, OrderStateHistory history, OrderOutboxMessage outboxMessage) {
+ BookOrder snapshot = order.copy();
+ orders.put(order.getOrderNo(), snapshot);
+ eventResults.put(history.getEventId(), snapshot.copy());
+ histories.add(history);
+ outboxMessages.add(outboxMessage);
+ }
+
+ private static BookOrder copy(BookOrder order) { return order == null ? null : order.copy(); }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java
new file mode 100644
index 0000000..00c7db5
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java
@@ -0,0 +1,37 @@
+package com.arch.policy.book.infrastructure.repository;
+
+import com.arch.policy.book.application.RetryablePostTransitionExecutor.FailedPostActionStore;
+import com.arch.policy.book.domain.TransitionExecution;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class InMemoryFailedPostActionStore implements FailedPostActionStore {
+ private final List failures = new ArrayList();
+
+ @Override public synchronized void record(TransitionExecution execution, RuntimeException failure) {
+ failures.add(new FailedPostAction(execution.getContext().getEventId(),
+ execution.getContext().getOrder().getOrderNo(), failure.getMessage()));
+ }
+
+ public synchronized List all() {
+ return Collections.unmodifiableList(new ArrayList(failures));
+ }
+
+ public static final class FailedPostAction {
+ private final String eventId;
+ private final String orderNo;
+ private final String reason;
+
+ public FailedPostAction(String eventId, String orderNo, String reason) {
+ this.eventId = eventId;
+ this.orderNo = orderNo;
+ this.reason = reason;
+ }
+
+ public String getEventId() { return eventId; }
+ public String getOrderNo() { return orderNo; }
+ public String getReason() { return reason; }
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java
new file mode 100644
index 0000000..12625eb
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java
@@ -0,0 +1,25 @@
+package com.arch.policy.book.infrastructure.rpc;
+
+import com.arch.policy.api.book.BookOrderRpcService;
+import com.arch.policy.book.application.BookOrderApplicationService;
+import com.arch.policy.common.book.BookOrderRequest;
+import com.arch.policy.common.book.BookOrderResponse;
+import com.arch.policy.common.book.OrderEventRequest;
+import org.apache.dubbo.config.annotation.DubboService;
+
+@DubboService(version = "1.0.0", timeout = 3000)
+public final class DubboBookOrderService implements BookOrderRpcService {
+ private final BookOrderApplicationService applicationService;
+
+ public DubboBookOrderService(BookOrderApplicationService applicationService) {
+ this.applicationService = applicationService;
+ }
+
+ @Override public BookOrderResponse createOrder(BookOrderRequest request) {
+ return applicationService.createOrder(request);
+ }
+
+ @Override public BookOrderResponse fireEvent(OrderEventRequest request) {
+ return applicationService.fireEvent(request);
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java b/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java
new file mode 100644
index 0000000..d890783
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java
@@ -0,0 +1,24 @@
+package com.arch.policy.common.book;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+public final class BookOrderRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private String requestId;
+ private String customerId;
+ private String productId;
+ private int quantity;
+ private BigDecimal amount;
+
+ public String getRequestId() { return requestId; }
+ public void setRequestId(String requestId) { this.requestId = requestId; }
+ public String getCustomerId() { return customerId; }
+ public void setCustomerId(String customerId) { this.customerId = customerId; }
+ public String getProductId() { return productId; }
+ public void setProductId(String productId) { this.productId = productId; }
+ public int getQuantity() { return quantity; }
+ public void setQuantity(int quantity) { this.quantity = quantity; }
+ public BigDecimal getAmount() { return amount; }
+ public void setAmount(BigDecimal amount) { this.amount = amount; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java b/architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java
new file mode 100644
index 0000000..4845559
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java
@@ -0,0 +1,20 @@
+package com.arch.policy.common.book;
+
+import java.io.Serializable;
+
+public final class BookOrderResponse implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private final String orderNo;
+ private final String state;
+ private final long version;
+
+ public BookOrderResponse(String orderNo, String state, long version) {
+ this.orderNo = orderNo;
+ this.state = state;
+ this.version = version;
+ }
+
+ public String getOrderNo() { return orderNo; }
+ public String getState() { return state; }
+ public long getVersion() { return version; }
+}
diff --git a/architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java b/architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java
new file mode 100644
index 0000000..0e39d3a
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java
@@ -0,0 +1,32 @@
+package com.arch.policy.common.book;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class OrderEventRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private String eventId;
+ private String orderNo;
+ private String event;
+ private String operator;
+ private long expectedVersion;
+ private Map attributes = new HashMap();
+
+ public String getEventId() { return eventId; }
+ public void setEventId(String eventId) { this.eventId = eventId; }
+ public String getOrderNo() { return orderNo; }
+ public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
+ public String getEvent() { return event; }
+ public void setEvent(String event) { this.event = event; }
+ public String getOperator() { return operator; }
+ public void setOperator(String operator) { this.operator = operator; }
+ public long getExpectedVersion() { return expectedVersion; }
+ public void setExpectedVersion(long expectedVersion) { this.expectedVersion = expectedVersion; }
+ public Map getAttributes() { return Collections.unmodifiableMap(attributes); }
+ public void setAttributes(Map attributes) {
+ this.attributes = attributes == null
+ ? new HashMap() : new HashMap(attributes);
+ }
+}
diff --git a/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java b/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java
new file mode 100644
index 0000000..71716c5
--- /dev/null
+++ b/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java
@@ -0,0 +1,36 @@
+package com.arch.policy.common.config;
+
+import com.arch.policy.book.application.BookOrderApplicationService;
+import com.arch.policy.book.application.BookOrderStore;
+import com.arch.policy.book.application.PostTransitionExecutor;
+import com.arch.policy.book.application.RetryablePostTransitionExecutor;
+import com.arch.policy.book.domain.DefaultOrderTransitions;
+import com.arch.policy.book.domain.OrderStateMachine;
+import com.arch.policy.book.infrastructure.repository.InMemoryBookOrderStore;
+import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class BookOrderConfiguration {
+ @Bean public OrderStateMachine orderStateMachine() {
+ return new OrderStateMachine(DefaultOrderTransitions.definitions());
+ }
+
+ @Bean public BookOrderStore bookOrderStore() { return new InMemoryBookOrderStore(); }
+
+ @Bean public InMemoryFailedPostActionStore failedPostActionStore() {
+ return new InMemoryFailedPostActionStore();
+ }
+
+ @Bean public PostTransitionExecutor postTransitionExecutor(
+ InMemoryFailedPostActionStore failedPostActionStore) {
+ return new RetryablePostTransitionExecutor(failedPostActionStore);
+ }
+
+ @Bean public BookOrderApplicationService bookOrderApplicationService(
+ BookOrderStore store, OrderStateMachine stateMachine,
+ PostTransitionExecutor postTransitionExecutor) {
+ return new BookOrderApplicationService(store, stateMachine, postTransitionExecutor);
+ }
+}
diff --git a/architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java b/architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java
new file mode 100644
index 0000000..6dffeaf
--- /dev/null
+++ b/architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java
@@ -0,0 +1,105 @@
+package com.arch.policy.book.application;
+
+import com.arch.policy.book.domain.DefaultOrderTransitions;
+import com.arch.policy.book.domain.OrderStateMachine;
+import com.arch.policy.book.infrastructure.repository.InMemoryBookOrderStore;
+import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore;
+import com.arch.policy.common.book.BookOrderRequest;
+import com.arch.policy.common.book.BookOrderResponse;
+import com.arch.policy.common.book.OrderEventRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class BookOrderApplicationServiceTest {
+ private InMemoryBookOrderStore store;
+ private BookOrderApplicationService service;
+
+ @BeforeEach
+ void setUp() {
+ store = new InMemoryBookOrderStore();
+ OrderStateMachine stateMachine = new OrderStateMachine(DefaultOrderTransitions.definitions());
+ service = new BookOrderApplicationService(store, stateMachine,
+ new RetryablePostTransitionExecutor(new InMemoryFailedPostActionStore()));
+ }
+
+ @Test
+ void createsOrderIdempotentlyAndCommitsHistoryWithOutbox() {
+ BookOrderRequest request = request("request-1");
+
+ BookOrderResponse first = service.createOrder(request);
+ BookOrderResponse repeated = service.createOrder(request);
+
+ assertEquals("WAIT_PAY", first.getState());
+ assertEquals(1L, first.getVersion());
+ assertEquals(first.getOrderNo(), repeated.getOrderNo());
+ assertEquals(1, store.historiesOf(first.getOrderNo()).size());
+ assertEquals(1, store.unpublishedOutboxMessages().size());
+ }
+
+ @Test
+ void drivesOrderByBusinessEventsInsteadOfTargetStates() {
+ BookOrderResponse created = service.createOrder(request("request-2"));
+
+ BookOrderResponse booking = fire(created, "pay-1", "PAY_SUCCEEDED");
+ BookOrderResponse booked = fire(booking, "book-1", "BOOK_SUCCEEDED");
+
+ assertEquals("BOOKED", booked.getState());
+ assertEquals(3L, booked.getVersion());
+ assertEquals(3, store.historiesOf(booked.getOrderNo()).size());
+ assertEquals(3, store.unpublishedOutboxMessages().size());
+ }
+
+ @Test
+ void returnsOriginalResultForDuplicateEventEvenWithStaleVersion() {
+ BookOrderResponse created = service.createOrder(request("request-3"));
+ BookOrderResponse booking = fire(created, "pay-2", "PAY_SUCCEEDED");
+
+ BookOrderResponse duplicate = fire(created, "pay-2", "PAY_SUCCEEDED");
+
+ assertEquals(booking.getState(), duplicate.getState());
+ assertEquals(booking.getVersion(), duplicate.getVersion());
+ assertEquals(2, store.historiesOf(created.getOrderNo()).size());
+ }
+
+ @Test
+ void rejectsUnsupportedEventAndConcurrentUpdate() {
+ BookOrderResponse created = service.createOrder(request("request-4"));
+ assertThrows(IllegalStateException.class,
+ () -> fire(created, "book-too-early", "BOOK_SUCCEEDED"));
+
+ fire(created, "pay-3", "PAY_SUCCEEDED");
+ assertThrows(IllegalStateException.class,
+ () -> fire(created, "cancel-stale", "CANCEL"));
+ }
+
+ private BookOrderResponse fire(BookOrderResponse current, String eventId, String event) {
+ OrderEventRequest request = new OrderEventRequest();
+ request.setEventId(eventId);
+ request.setOrderNo(current.getOrderNo());
+ request.setExpectedVersion(current.getVersion());
+ request.setEvent(event);
+ request.setOperator("test-operator");
+ if ("PAY_SUCCEEDED".equals(event)) {
+ request.setAttributes(Collections.singletonMap("paymentNo", "payment-1"));
+ } else if ("BOOK_SUCCEEDED".equals(event)) {
+ request.setAttributes(Collections.singletonMap("bookingReference", "PNR001"));
+ }
+ return service.fireEvent(request);
+ }
+
+ private static BookOrderRequest request(String requestId) {
+ BookOrderRequest request = new BookOrderRequest();
+ request.setRequestId(requestId);
+ request.setCustomerId("customer-1");
+ request.setProductId("policy-1");
+ request.setQuantity(1);
+ request.setAmount(new BigDecimal("100.00"));
+ return request;
+ }
+}
diff --git a/architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java b/architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java
new file mode 100644
index 0000000..892f71f
--- /dev/null
+++ b/architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java
@@ -0,0 +1,40 @@
+package com.arch.policy.book.application;
+
+import com.arch.policy.book.domain.BookOrder;
+import com.arch.policy.book.domain.OrderEvent;
+import com.arch.policy.book.domain.OrderState;
+import com.arch.policy.book.domain.OrderStateMachine;
+import com.arch.policy.book.domain.OrderTransition;
+import com.arch.policy.book.domain.StateTransitionContext;
+import com.arch.policy.book.domain.TransitionExecution;
+import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class RetryablePostTransitionExecutorTest {
+ @Test
+ void recordsPostActionFailureForRetryWithoutRevertingCommittedState() {
+ OrderTransition transition = OrderTransition
+ .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY)
+ .after(context -> { throw new IllegalStateException("notification unavailable"); })
+ .build();
+ OrderStateMachine stateMachine = new OrderStateMachine(Arrays.asList(transition));
+ BookOrder order = BookOrder.create("order-1", "request-1", "customer-1", "product-1",
+ 1, BigDecimal.ONE);
+ TransitionExecution execution = stateMachine.fire(new StateTransitionContext(
+ "event-1", OrderEvent.CREATE_SUCCEEDED, order, "tester",
+ Collections.emptyMap()));
+ InMemoryFailedPostActionStore failureStore = new InMemoryFailedPostActionStore();
+
+ new RetryablePostTransitionExecutor(failureStore).execute(stateMachine, execution);
+
+ assertEquals(OrderState.WAIT_PAY, order.getState());
+ assertEquals(1, failureStore.all().size());
+ assertEquals("event-1", failureStore.all().get(0).getEventId());
+ }
+}
diff --git a/architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java b/architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java
new file mode 100644
index 0000000..9b17b6f
--- /dev/null
+++ b/architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java
@@ -0,0 +1,89 @@
+package com.arch.policy.book.domain;
+
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class OrderStateMachineTest {
+ @Test
+ void resolvesTargetByCurrentStateAndBusinessEvent() {
+ OrderStateMachine stateMachine = new OrderStateMachine(DefaultOrderTransitions.definitions());
+
+ assertEquals(OrderState.WAIT_PAY,
+ stateMachine.targetOf(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED));
+ assertEquals(OrderState.BOOKING,
+ stateMachine.targetOf(OrderState.WAIT_PAY, OrderEvent.PAY_SUCCEEDED));
+ assertEquals(OrderState.BOOKED,
+ stateMachine.targetOf(OrderState.VALIDATE_FAIL, OrderEvent.VALIDATE_SUCCEEDED));
+ assertNull(stateMachine.targetOf(OrderState.WAIT_PAY, OrderEvent.BOOK_SUCCEEDED));
+ }
+
+ @Test
+ void executesGuardAndPreActionBeforeMutationAndPostActionAfterCommit() {
+ final AtomicInteger calls = new AtomicInteger();
+ TransitionGuard guard = context -> calls.compareAndSet(0, 1);
+ TransitionAction preAction = context -> {
+ assertEquals(OrderState.CREATE, context.getOrder().getState());
+ calls.compareAndSet(1, 2);
+ };
+ TransitionAction postAction = context -> {
+ assertEquals(OrderState.WAIT_PAY, context.getOrder().getState());
+ calls.compareAndSet(2, 3);
+ };
+ OrderTransition transition = OrderTransition
+ .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY)
+ .guard(guard).before(preAction).after(postAction).build();
+ OrderStateMachine stateMachine = new OrderStateMachine(Arrays.asList(transition));
+ BookOrder order = order();
+
+ TransitionExecution execution = stateMachine.fire(context(order, OrderEvent.CREATE_SUCCEEDED));
+ assertEquals(2, calls.get());
+ assertEquals(OrderState.WAIT_PAY, order.getState());
+
+ stateMachine.afterCommit(execution);
+ assertEquals(3, calls.get());
+ }
+
+ @Test
+ void guardFailurePreventsStateMutation() {
+ OrderTransition transition = OrderTransition
+ .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY)
+ .guard(context -> { throw new IllegalStateException("risk rejected"); })
+ .build();
+ OrderStateMachine stateMachine = new OrderStateMachine(Arrays.asList(transition));
+ BookOrder order = order();
+
+ assertThrows(IllegalStateException.class,
+ () -> stateMachine.fire(context(order, OrderEvent.CREATE_SUCCEEDED)));
+ assertEquals(OrderState.CREATE, order.getState());
+ assertEquals(0L, order.getVersion());
+ }
+
+ @Test
+ void rejectsDuplicateTransitionRegistration() {
+ OrderTransition first = OrderTransition
+ .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY).build();
+ OrderTransition duplicate = OrderTransition
+ .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.CREATE_FAIL).build();
+
+ assertThrows(IllegalStateException.class,
+ () -> new OrderStateMachine(Arrays.asList(first, duplicate)));
+ }
+
+ private static BookOrder order() {
+ return BookOrder.create("order-1", "request-1", "customer-1", "product-1",
+ 1, BigDecimal.ONE);
+ }
+
+ private static StateTransitionContext context(BookOrder order, OrderEvent event) {
+ return new StateTransitionContext("event-1", event, order, "tester",
+ Collections.emptyMap());
+ }
+}
From 1bc1bae44018c3b39cfb708b8e093c9d5eee67a3 Mon Sep 17 00:00:00 2001
From: coderbruis
Date: Tue, 21 Jul 2026 21:03:02 +0800
Subject: [PATCH 35/37] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=8B=E5=8D=95Seata?=
=?UTF-8?q?=20saga=E6=B5=81=E7=A8=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
architecture/README.md | 90 +++++++--
architecture/pom.xml | 30 +--
.../BookOrderApplicationService.java | 173 +++++++++++-------
.../book/application/BookOrderStore.java | 9 +-
.../application/CompensationTaskService.java | 51 ++++++
.../application/CreateOrderResultCache.java | 11 ++
.../application/DirectOrderCreationSaga.java | 24 +++
.../application/ExternalResultStatus.java | 7 +
.../book/application/GdsBookingGateway.java | 9 +
.../book/application/GdsBookingResult.java | 29 +++
.../application/GdsReconciliationService.java | 25 +++
.../book/application/LocalCreateResult.java | 24 +++
.../OrderCreationOutboxMessage.java | 21 +++
.../OrderCreationOutboxPublisher.java | 25 +++
.../book/application/OrderCreationSaga.java | 20 ++
.../OrderCreationSagaStateServices.java | 86 +++++++++
.../OrderEventApplicationService.java | 97 ++++++++++
.../book/application/OrderReconciler.java | 5 +
.../book/application/OrderWorkflowTask.java | 19 ++
.../application/OrderWorkflowTaskStore.java | 5 +
.../book/application/PaymentGateway.java | 5 +
.../application/PromotionStockGateway.java | 5 +
.../policy/book/application/RecoveryTask.java | 66 +++++++
.../application/RecoveryTaskProcessor.java | 67 +++++++
.../book/application/RecoveryTaskStore.java | 11 ++
.../arch/policy/book/domain/BookOrder.java | 12 +-
.../book/domain/DefaultOrderTransitions.java | 7 +
.../arch/policy/book/domain/OrderEvent.java | 2 +
.../cache/DefaultCreateOrderResultCache.java | 54 ++++++
.../demo/DemoGdsBookingGateway.java | 18 ++
.../job/OrderCreationOutboxScheduler.java | 15 ++
.../job/OrderRecoveryScheduler.java | 13 ++
.../repository/InMemoryBookOrderStore.java | 35 +++-
.../InMemoryOrderWorkflowTaskStore.java | 17 ++
.../repository/InMemoryRecoveryTaskStore.java | 45 +++++
.../rpc/DubboBookOrderService.java | 8 +-
.../seata/SeataOrderCreationSaga.java | 37 ++++
.../policy/common/book/BookOrderRequest.java | 3 +
.../common/config/BookOrderConfiguration.java | 109 ++++++++++-
.../common/config/RedissonConfiguration.java | 28 +++
.../common/config/SeataSagaConfiguration.java | 54 ++++++
.../redis/RedissonDistributedLock.java | 47 +++++
.../redis/RedissonRedisClient.java | 30 +++
.../policy/common/lock/DistributedLock.java | 14 ++
.../DistributedLockUnavailableException.java | 7 +
.../arch/policy/common/redis/RedisClient.java | 7 +
.../common/redis/RedisClientException.java | 7 +
.../src/main/resources/application.yml | 9 +
.../main/resources/db/book_order_schema.sql | 37 ++++
.../statelang/book_order_creation_saga.json | 54 ++++++
.../BookOrderApplicationServiceTest.java | 105 -----------
.../RetryablePostTransitionExecutorTest.java | 40 ----
.../book/domain/OrderStateMachineTest.java | 89 ---------
.../AsyncSearchCoordinatorTest.java | 98 ----------
.../snapshot/SnapshotLifecycleTest.java | 121 ------------
55 files changed, 1469 insertions(+), 567 deletions(-)
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/CompensationTaskService.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/CreateOrderResultCache.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/DirectOrderCreationSaga.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/ExternalResultStatus.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/GdsBookingGateway.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/GdsBookingResult.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/GdsReconciliationService.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/LocalCreateResult.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxMessage.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxPublisher.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationSaga.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationSagaStateServices.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderEventApplicationService.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderReconciler.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTask.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTaskStore.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/PaymentGateway.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/PromotionStockGateway.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/RecoveryTask.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskProcessor.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskStore.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/cache/DefaultCreateOrderResultCache.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/demo/DemoGdsBookingGateway.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderCreationOutboxScheduler.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderRecoveryScheduler.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryOrderWorkflowTaskStore.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryRecoveryTaskStore.java
create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/seata/SeataOrderCreationSaga.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/config/RedissonConfiguration.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/config/SeataSagaConfiguration.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonDistributedLock.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonRedisClient.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/lock/DistributedLock.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/lock/DistributedLockUnavailableException.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/redis/RedisClient.java
create mode 100644 architecture/src/main/java/com/arch/policy/common/redis/RedisClientException.java
create mode 100644 architecture/src/main/resources/db/book_order_schema.sql
create mode 100644 architecture/src/main/resources/statelang/book_order_creation_saga.json
delete mode 100644 architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java
delete mode 100644 architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java
delete mode 100644 architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java
delete mode 100644 architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java
delete mode 100644 architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java
diff --git a/architecture/README.md b/architecture/README.md
index 04f7058..bc3298a 100644
--- a/architecture/README.md
+++ b/architecture/README.md
@@ -14,9 +14,9 @@
## 模块目标
- 用最少的代码表达架构中的核心职责、协作关系和一致性约束。
-- 为类似业务问题提供可讨论、可验证、可演进的设计参考。
+- 为类似业务问题提供可讨论、可演进的设计参考。
- 通过接口隔离外部系统,使方案不被 Dubbo、Kafka、Redis 等具体技术绑定。
-- 通过测试验证关键架构行为,而不是覆盖完整的生产业务流程。
+- 通过场景说明和关键注释表达核心架构行为及约束。
- 持续积累搜索、政策匹配、交易、下单等不同场景的方案。
本模块属于架构伪代码,生产落地时仍需根据实际情况补充鉴权、限流、熔断、监控、链路追踪、异常分级、
@@ -153,9 +153,54 @@ Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序
## 场景三:幂等下单与订单状态机
-`BookOrderRpcService.createOrder` 以调用方生成的 `requestId` 作为幂等键。订单通过 `CREATE_SUCCEEDED` 事件
-从 `CREATE` 进入 `WAIT_PAY`。重复请求返回第一次创建的订单,不重复生成业务单。生产实现应在数据库中为
-`request_id` 建唯一索引,并把创单与初始状态流转放入本地事务。
+`BookOrderRpcService.createOrder` 以调用方生成的 `requestId` 作为幂等键。下单首先保存 `CREATE` 状态订单,
+提交后才由 Seata Saga 调用 GDS,因此不会在本地数据库事务中持有远程调用。
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant Caller as 调用方
+ participant Order as 下单应用服务
+ participant DB as 本地数据库
+ participant Saga as Seata Saga
+ participant GDS as GDS
+ participant Task as 任务/补偿表
+
+ Caller->>Order: createOrder(requestId)
+ Order->>Order: 查询 Redis 幂等结果缓存
+ Order->>Order: Redisson RLock 获取 requestId 短锁
+ Order->>Order: 锁内二次查缓存,仅持锁者查数据库
+ Order->>DB: 本地事务写订单 CREATE
+ DB-->>Order: 提交成功
+ Order->>Order: 回填结果缓存并释放 RLock
+ Order->>DB: 抢占订单创建调度租约
+ Order->>Saga: 启动 BookOrderCreationSaga
+ Saga->>GDS: 发起 PNR 占编
+ GDS-->>Saga: SUCCESS / FAIL / UNKNOWN
+ alt SUCCESS
+ Saga->>DB: CREATE_SUCCEEDED → WAIT_PAY
+ Saga->>Task: 幂等创建待支付任务
+ else FAIL
+ Saga->>DB: CREATE_FAILED → CREATE_FAIL
+ Saga->>Task: 幂等创建促销库存返还任务
+ else UNKNOWN
+ Saga->>DB: START_VALIDATE → VALIDATING
+ Saga->>Task: 创建 GDS 核对任务 + 人工任务
+ end
+```
+
+订单与 `START_ORDER_CREATION` Outbox 在同一本地事务中提交。事务后立即尝试发布;如果进程在订单提交后、
+启动 Saga 前崩溃,`OrderCreationOutboxScheduler` 会重新投递未发布消息。Saga 使用 `orderNo` 作为业务幂等键,
+已经启动的流程由 Seata 根据持久化的状态机日志继续恢复。
+Saga 使用 `orderNo` 作为业务幂等键;GDS 侧也必须使用订单号或稳定请求号保证占编幂等。
+
+高并发重复请求首先读取 `book:create:result:{requestId}` 幂等结果缓存,命中后不访问数据库。缓存未命中时,
+使用 Redisson `RLock` 锁定 `book:create:lock:{requestId}`,锁内再次检查缓存,只有锁持有者才允许查询数据库和
+执行本地创建事务;未获得锁的请求等待首个请求回填结果缓存,同样不会查询数据库。RLock 最多等待 300ms,
+持锁期间由 Redisson watchdog 自动续期;锁只覆盖本地事务,不覆盖耗时不可控的 Saga/GDS 调用。
+
+订单状态迁移后会同步刷新结果缓存,缓存 TTL 为 10 分钟。Redis 不可用时锁和缓存主动降级,最终仍由数据库
+`request_id` 唯一索引保证只能创建一张订单;Redisson 锁和结果缓存是防穿透、削峰层,不是最终一致性依据。
外部系统通过 `BookOrderRpcService.fireEvent` 提交支付、出票、验真、取消等业务事件,不能直接指定目标状态。
`OrderStateMachine` 使用 `(当前状态, 业务事件)` 定位唯一迁移,状态参考 `ipolicytradecore`:
@@ -164,6 +209,7 @@ Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序
stateDiagram-v2
CREATE --> WAIT_PAY: CREATE_SUCCEEDED
CREATE --> CREATE_FAIL: CREATE_FAILED
+ CREATE --> VALIDATING: START_VALIDATE
CREATE --> CANCEL: CANCEL
WAIT_PAY --> BOOKING: PAY_SUCCEEDED
WAIT_PAY --> CANCEL: CANCEL
@@ -177,6 +223,8 @@ stateDiagram-v2
BOOK_FAIL --> CANCEL: CANCEL
VALIDATING --> BOOKED: VALIDATE_SUCCEEDED
VALIDATING --> VALIDATE_FAIL: VALIDATE_FAILED
+ VALIDATING --> WAIT_PAY: GDS_BOOKING_CONFIRMED
+ VALIDATING --> CREATE_FAIL: GDS_BOOKING_REJECTED
VALIDATE_FAIL --> BOOKED: VALIDATE_SUCCEEDED
VALIDATE_FAIL --> CANCEL: CANCEL
CREATE_FAIL --> DELETED: DELETE
@@ -200,6 +248,25 @@ stateDiagram-v2
示例使用内存实现展示原子语义;生产落地应使用数据库唯一索引、条件更新、状态历史表、Outbox 表和重试任务,
并由消息消费方继续按照 `eventId` 幂等。
+### Seata Saga 与补偿
+
+`statelang/book_order_creation_saga.json` 是 Seata 状态语言定义,包含 GDS 服务节点、结果 Choice、异常捕获和
+`CancelReservedPnr` 补偿节点。`SeataOrderCreationSaga` 使用 `orderNo` 作为 business key 启动状态机;
+`SeataSagaConfiguration` 在存在 `DataSource` 时创建数据库持久化的 `DbStateMachineConfig` 和
+`StateMachineEngine`。没有引擎 Bean 的本地演示环境使用相同 Saga State Services 直接编排,业务行为一致。
+
+Saga 提供的是可恢复的最终一致性,而不是把 GDS 变成支持 ACID 回滚的数据库资源。每个外部正向动作都必须有
+幂等补偿语义,并允许空补偿:
+
+- PNR 占编失败且已经扣减促销库存:创建 `RETURN_PROMOTION_STOCK` 任务。
+- PNR 已占编但采购商取消:创建 `CANCEL_PNR` 任务。
+- 支付成功但最终出票失败:创建 `REFUND_PAYMENT` 任务。
+- GDS 返回 UNKNOWN:创建 `VALIDATE_GDS_BOOKING` 与人工核对任务,定时比较本地状态和 GDS 实际状态。
+
+补偿表使用业务唯一键防重;取消 PNR 额外使用 `(order_no, child_order_no, pnr, task_type)` 唯一索引。任务采用
+指数退避重试,超过五次进入 `MANUAL_REQUIRED`。示例 DDL 位于 `db/book_order_schema.sql`;Seata Server 和
+Saga 引擎日志表应使用部署版本对应的官方脚本创建,避免跨版本复制表结构。
+
## 新增架构场景的约定
新增交易、下单或其他架构伪代码时,应遵循以下约定:
@@ -208,16 +275,5 @@ stateDiagram-v2
2. 对外契约放在 `api.`,内部实现放在对应的 `` 业务包。
3. 使用 `application/domain/infrastructure` 表达职责边界,不让领域规则依赖具体中间件。
4. 优先提供展示关键协作关系的最小实现,避免把伪代码扩展成不完整的生产框架。
-5. 为幂等、一致性、并发、超时、切换和失败隔离等关键架构行为编写测试。
+5. 用精简的流程代码和注释表达幂等、一致性、并发、超时、切换和失败隔离等关键行为。
6. 在场景文档中记录设计取舍、适用边界,以及生产落地仍需补充的能力。
-
-## 运行验证
-
-在仓库根目录执行:
-
-```bash
-mvn -f architecture/pom.xml clean test
-```
-
-当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换、幂等下单、事件驱动
-状态迁移、Guard/Action 执行顺序、事件幂等、乐观并发、状态历史和 Outbox 原子记录。
diff --git a/architecture/pom.xml b/architecture/pom.xml
index 4f7f396..e677418 100644
--- a/architecture/pom.xml
+++ b/architecture/pom.xml
@@ -12,9 +12,11 @@
8
8
UTF-8
- 5.10.2
2.7.18
3.2.15
+ 1.8.0
+
+ 3.24.3
@@ -28,17 +30,16 @@
RoaringBitmap
0.9.47