Skip to content

Commit 097db7f

Browse files
committed
docs(adr): extract rollback drill into ADR-005a sub-protocol
- rename ADR-005-rollback-drill-protocol -> ADR-005a (sub-protocol of ADR-005; 005 now reserved for rolling-deploy-playbook) - add README 编号规则: ADR-NNNx 子协议后缀, 不占主编号 - fix milestone count 11->10 (M1a-M4b); clarify ADR-005 stays Proposed pending first rollback drill (§2.6 M3a/M3c/M4a all _TBD_) - add REVIEW-2026-06-14-adversarial.md + remediation-plan.md - sync references in RUNBOOK §10 and ADR-001/002/003/004 - update JudgeWorkerProcessor javadoc to reference VerdictResolver#reduceWire (ADR-001) instead of legacy stringly-typed verdict priority
1 parent ce5eb7a commit 097db7f

11 files changed

Lines changed: 458 additions & 47 deletions

backend-spring/src/main/java/com/ulticode/modules/queue/processor/JudgeWorkerProcessor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@
6565
* <li>Poll job from Redis queue
6666
* <li>Set submission status to "Judging"
6767
* <li>Load test cases, build RunSubmissionDTO, execute via Docker sandbox
68-
* <li>Determine verdict with priority ordering (RE > MLE > TLE > WA > PE > Accepted)
68+
* <li>Determine verdict via {@link VerdictResolver#reduceWire} aggregating each case's wire value
69+
* into a single {@code SubmissionStatus} (ADR-001; severity priority encoded in
70+
* {@code SubmissionStatus#getSeverity()}, replacing the old stringly-typed priority comparison)
6971
* <li>Write result to Submission entity
7072
* <li>Push WebSocket notification to user
7173
* </ol>

docs/RUNBOOK.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ pm2 reload ulticode-9001 --update-env
452452
### 10.4 演练 (rollback drill)
453453

454454
每次部署到 dev 拓扑后, 至少跑 1 次对应 milestone 的 rollback drill, 记录实际耗时.
455-
详见 ADR-005 §2.6 + `docs/adr/ADR-005-rollback-drill-protocol.md` (新).
455+
详见 ADR-005 §2.6 + `docs/adr/ADR-005a-rollback-drill-protocol.md` (ADR-005 的子协议, 新).
456456

457457
### 10.5 启动日志确认 (临时, 等 ADR-008)
458458

@@ -501,6 +501,6 @@ pm2 logs ulticode-9001 --nostream --lines 200 | grep -E "app\.features|FeatureFl
501501
端到端 ≥ 15min, 不在本 ADR "5min 热回滚" 演练范围
502502
- 详见 ADR-005 §2.6 表脚注 ¹
503503

504-
**M2a 演练替代项**: 因 M2a 不可热回滚, 演练协议 (ADR-005-rollback-drill-protocol.md)
504+
**M2a 演练替代项**: 因 M2a 不可热回滚, 演练协议 (ADR-005a-rollback-drill-protocol.md)
505505
矩阵中 M2a 行 strike through, 替换为 "演练 git revert + 重建 sandbox image" 的
506506
"重建演练" 协议, 时间窗口 30min.

docs/adr/ADR-001-verdict-status-codec.md

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -65,43 +65,46 @@ private static final Map<String, Integer> VERDICT_PRIORITY = Map.of(
6565

6666
```java
6767
public enum SubmissionStatus {
68-
PENDING ("Pending", 0, Kind.IN_FLIGHT),
69-
JUDGING ("Judging", 0, Kind.IN_FLIGHT),
70-
ACCEPTED ("Accepted", 0, Kind.TERMINAL_GOOD),
71-
PRESENTATION_ERROR ("Presentation Error", 1, Kind.TERMINAL_BAD),
72-
WRONG_ANSWER ("Wrong Answer", 2, Kind.TERMINAL_BAD),
73-
TIME_LIMIT_EXCEEDED("Time Limit Exceeded", 3, Kind.TERMINAL_BAD),
74-
MEMORY_LIMIT_EXCEEDED("Memory Limit Exceeded",4, Kind.TERMINAL_BAD),
75-
OUTPUT_LIMIT_EXCEEDED("Output Limit Exceeded",4, Kind.TERMINAL_BAD),
76-
RUNTIME_ERROR ("Runtime Error", 5, Kind.TERMINAL_BAD),
77-
COMPILE_ERROR ("Compile Error", 6, Kind.TERMINAL_BAD), // 不参与 case-level reduce
78-
SANDBOX_ERROR ("Sandbox Error", 7, Kind.TERMINAL_INFRA),
79-
SYSTEM_ERROR ("System Error", 8, Kind.TERMINAL_INFRA);
68+
PENDING ("Pending", "pending", false, 0, Kind.IN_FLIGHT),
69+
JUDGING ("Judging", "pending", false, 0, Kind.IN_FLIGHT),
70+
ACCEPTED ("Accepted", "accepted", true, 0, Kind.TERMINAL_GOOD),
71+
PRESENTATION_ERROR ("Presentation Error", "error", true, 1, Kind.TERMINAL_BAD),
72+
WRONG_ANSWER ("Wrong Answer", "error", true, 2, Kind.TERMINAL_BAD),
73+
TIME_LIMIT_EXCEEDED("Time Limit Exceeded", "error", true, 3, Kind.TERMINAL_BAD),
74+
MEMORY_LIMIT_EXCEEDED("Memory Limit Exceeded","error", true, 4, Kind.TERMINAL_BAD),
75+
OUTPUT_LIMIT_EXCEEDED("Output Limit Exceeded","error", true, 4, Kind.TERMINAL_BAD),
76+
RUNTIME_ERROR ("Runtime Error", "error", true, 5, Kind.TERMINAL_BAD),
77+
COMPILE_ERROR ("Compile Error", "error", true, 6, Kind.TERMINAL_BAD), // 不参与 case-level reduce
78+
SANDBOX_ERROR ("Sandbox Error", "system", true, 7, Kind.TERMINAL_INFRA),
79+
SYSTEM_ERROR ("System Error", "system", true, 8, Kind.TERMINAL_INFRA);
8080

8181
public enum Kind { IN_FLIGHT, TERMINAL_GOOD, TERMINAL_BAD, TERMINAL_INFRA }
8282

83-
private final String wireValue; // ← 持久化/JSON 字符串, 永远是真相
84-
private final int severity; // ← 越大越严重, ACCEPTED=0
83+
private final String displayName; // ← 持久化/JSON 字符串, 永远是真相 (wire value)
84+
private final String category; // ← 粗粒度过滤分类 (pending/accepted/error/system), 供 admin UI
85+
private final boolean terminal; // ← 是否终态 (不再自动重判)
86+
private final int severity; // ← 越大越严重, ACCEPTED=0
8587
private final Kind kind;
8688

87-
SubmissionStatus(String wireValue, int severity, Kind kind) { ... }
89+
SubmissionStatus(String displayName, String category, boolean terminal,
90+
int severity, Kind kind) { ... }
8891

89-
@JsonValue // Jackson 序列化用 wireValue
90-
public String wireValue() { return wireValue; }
92+
@JsonValue // Jackson 序列化用 displayName 作为 wire value
93+
public String wireValue() { return displayName; }
9194

92-
@JsonCreator // Jackson 反序列化按 wireValue 反查
93-
public static SubmissionStatus fromWire(String s) {
94-
return Codec.fromWire(s);
95-
}
95+
@JsonCreator // Jackson 反序列化按 displayName 反查
96+
public static SubmissionStatus fromWire(String wire) { ... }
9697

9798
public int severity() { return severity; }
9899
public Kind kind() { return kind; }
99100
}
100101
```
101102

103+
> **注 (字段名对齐)**: 实际 enum (`SubmissionStatus.java`) 无独立 `wireValue` 字段 —— 持久化/JSON 字符串直接存于 `displayName` 字段, `wireValue()` 是返回该字段的 `@JsonValue` 方法 (line 122-125), `fromWire(String)``@JsonCreator` 静态工厂 (line 136-147)。此外实际 enum 还携带 ADR 决策伪代码未展开的 `category` / `terminal` 两字段 (admin UI 过滤与终态判定)。对外契约 (wire value 序列化 / fromWire 反序列化 / severity 归约) 一致, 本节伪代码为可读性略作精简, 真值以 `SubmissionStatus.java` 为准。
104+
102105
**关键不变量** (这些是契约, 改动需要新 ADR):
103106

104-
- `wireValue` 字符串**永不改写**;新增状态必须在 ADR 中显式声明并配套迁移
107+
- `displayName` (即 wire value) 字符串**永不改写**;新增状态必须在 ADR 中显式声明并配套迁移
105108
- `name()` (即 `ACCEPTED`)、`ordinal()`**不是契约** , 禁止跨进程依赖
106109
- `severity()` 仅在 JVM 内使用 (verdict 归约) , 不持久化, 不发 API
107110

@@ -179,8 +182,8 @@ public class VerdictResolver {
179182

180183
### 3.2 Negative
181184

182-
- enum 文件膨胀 (8 → 12 状态, 每个带 3 字段)
183-
- `wireValue ≠ name()` 对新人有学习曲线, **README + javadoc 必须明确**
185+
- enum 文件膨胀 (8 → 12 状态, 每个带 5 字段 `displayName`/`category`/`terminal`/`severity`/`kind`)
186+
- `wireValue() ≠ name()` 对新人有学习曲线 (例 `"Accepted"` vs `"ACCEPTED"`), **README + javadoc 必须明确**
184187
- I18n coverage test 需要前端 build script 配合输出 JSON, 增加 CI 步骤
185188

186189
### 3.3 Risks

docs/adr/ADR-002-sandbox-hexagonal.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,18 @@ public class SandboxExecutorImpl implements SandboxExecutor {
100100

101101
### 2.3 Adapter 矩阵 (本次只做 2 个)
102102

103-
| Adapter | 用途 | 本次落地 |
103+
> 命名说明: Port 是 `SandboxExecutor`(§2.1)。docker 侧实现沿用既有命名 `SandboxExecutorImpl`(与 §2.2 伪代码一致), 不是独立的 `DockerSandboxAdapter``DockerSandboxConfig` 是 Spring config(注册 bean), 不是 Adapter, 不计入下表。下表的"加新沙箱"行说明为何 docker 实现复用 Executor 命名而非新起 Adapter。
104+
105+
| Adapter / Executor | 用途 | 本次落地 |
104106
|---|---|---|
105-
| `DockerSandboxAdapter` | 生产 — 现 `SandboxServiceImpl` 重构迁入, 保留全部安全策略 ||
107+
| `SandboxExecutorImpl` | 生产 (docker 默认实现) — 现 `SandboxServiceImpl` 重构迁入, 保留全部安全策略; 由 `@ConditionalOnProperty(name="sandbox.executor", havingValue="docker")` 激活; `ProcessBuilder` 拉容器在此类内 ||
106108
| `InMemorySandboxAdapter` | 测试 — 接收 `SandboxJob` 返回预设 `RunCaseResult` (可按 `job.code` 包含关键字路由到不同 verdict) ||
107109
| `RemoteJudgeSandboxAdapter` | 调外部判机 (HTTP/gRPC) | ❌ 不实现, 不预留接口 (YAGNI) |
108110
| `FirecrackerSandboxAdapter` / `gVisorSandboxAdapter` | 更轻隔离 | ❌ 不实现 |
109111

110112
加新语言: 只新增 `RustLanguageProfile implements LanguageProfile` , 不改 `SandboxExecutor` / `JudgeWorker` (开闭原则 #11) 。
111113

112-
加新沙箱: 只新增 `class XxxSandboxAdapter implements SandboxExecutor` , 通过 `@ConditionalOnProperty(name="sandbox.executor",havingValue="xxx")` 切换 (依赖倒置 #10) 。
114+
加新沙箱: 新增 `class XxxSandboxAdapter implements SandboxExecutor` , 通过 `@ConditionalOnProperty(name="sandbox.executor", havingValue="xxx")` 切换 (依赖倒置 #10) 。docker 默认实现复用 `SandboxExecutorImpl` 命名, 没有为对称性新起 `DockerSandboxAdapter`(实现已工作, 改名只增成本不增价值)
113115

114116
### 2.4 Verdict 解析下沉到 LanguageProfile
115117

@@ -126,7 +128,7 @@ public class SandboxExecutorImpl implements SandboxExecutor {
126128
- **单测无需 Docker daemon** (InMemoryAdapter), CI 提速 + 离线开发可写单测
127129
- 加新语言只动 1 个文件 (新 LanguageProfile bean), 编译期保证 (重复 languageId 启动崩)
128130
- Verdict 字符串契约由 ADR-001 单点管理, sandbox 不再持有
129-
- 现有 `docs/CODEMAPS/sandbox.md` 的安全矩阵零损失 (`DockerSandboxAdapter` 全量继承)
131+
- 现有 `docs/CODEMAPS/sandbox.md` 的安全矩阵零损失 (`SandboxExecutorImpl` docker 实现全量继承)
130132

131133
### 3.2 Negative
132134

@@ -148,7 +150,7 @@ public class SandboxExecutorImpl implements SandboxExecutor {
148150
- [ ] 5 个语言 profile (JS/Python/Java/C/C++) 各自单测 (compile failure / runtime error / accepted)
149151
- [ ] Testcontainers IT 跑核心矩阵 (3 用例 × 5 语言 = 15 case), 与 InMemoryAdapter 行为对照
150152
- [ ] grep 确认 `switch (language)``submission/` 子树为零
151-
- [ ] grep 确认 `ProcessBuilder``submission/` 子树只出现在 `DockerSandboxAdapter`
153+
- [ ] grep 确认 `ProcessBuilder``submission/` 子树只出现在 `SandboxExecutorImpl`
152154
- [ ] `docs/CODEMAPS/sandbox.md` 同步更新, 新增 "Port / Adapter / LanguageProfile" 章节
153155

154156
## 5. References
@@ -163,7 +165,7 @@ public class SandboxExecutorImpl implements SandboxExecutor {
163165
|---|-----|-------------|------|------|
164166
| 1 | `SANDBOX_IMAGE=ulticode-sandbox:latest` 指向 base-17 镜像(只有 JDK,****装 harness) | `.env` | `javac: cannot find symbol ListNode` × 9 → docker exit 1 → empty stdout → `sanitizeSandboxOutput(null)` 返回 `"Runtime error"``RUNTIME_ERROR` | `.env`: `ulticode-sandbox-dform:phase2-pinned` |
165167
| 2 | `SANDBOX_ENABLED=false` 整体禁用 docker 沙箱 | `.env` | 配置对但不调 docker, 所有提交 verdict = `Runtime Error` | `.env`: `SANDBOX_ENABLED=true` |
166-
| 3 | `SANDBOX_SECCOMP_PROFILE=docker/sandbox/seccomp-profile.json` **相对路径**, Spring Boot cwd 是 `backend-spring/`,docker daemon 拒绝 `--volume``/` 的 host path → 立即 exit 非零 | `.env` |#1 症状, 但有 `WARNING: includes invalid characters for a local volume name` | `.env`: 改为**绝对路径** `/home/davidhlp/project/UltiCode/docker/sandbox/seccomp-profile.json` |
168+
| 3 | `SANDBOX_SECCOMP_PROFILE=docker/sandbox/seccomp-profile.json` **相对路径**, Spring Boot cwd 是 `backend-spring/`,docker daemon 拒绝 `--volume``/` 的 host path → 立即 exit 非零 | `.env` |#1 症状, 但有 `WARNING: includes invalid characters for a local volume name` | `.env`: 改为**绝对路径** `/home/davidhlp/project/UltiCode/docker/sandbox/seccomp-profile.json`**治本 (2026-06-14 post-review)**: 该绝对路径已固化到 `application.yml` 默认值, 新机器 / CI / 不读 `.env` 的环境也直接走绝对路径, 不再依赖 `.env` 兜底即可避免此 bug |
167169
| 4 | `JavaLanguageProfile.dockerCommand` dispatch shell 用相对路径 `Solution.java`, 但镜像 `WORKDIR=/home/sandbox`,`/job` 是 mount 卷 — javac 找不到源文件 | `backend-spring/.../JavaLanguageProfile.java:63` | `error: file not found: Solution.java\nUsage: javac <options> <source files>` | 改成 `/job/Solution.java` 绝对路径 |
168170
| 5 | Java 17 `SecurityManager` 弃用 WARNING 行污染 stdout,**** JSON envelope **之前**输出, Jackson 严格解析失败 | `backend-spring/.../CodeExecutionHelperImpl.java#parseDEnvelope` | `D-form envelope unparseable: WARNING: ... {valid JSON here}` → 整体判 `Runtime Error` | `parseDEnvelope` 找第一个 `{`, 取 `substring(jsonStart)` 再 parse |
169171

docs/adr/ADR-003-queue-outbox-fencing.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,10 @@ ADR-003 M3a → M3c-3b 全部 7 commit (`09c97d1b8` → `82d5f022e`) 经 `codex
369369
- [ ] Admin rejudge 并发场景: 同时 rejudge × 2 + 旧 worker 苏醒, 最终只有最新 generation 结果写入
370370
- [ ] 模拟 Redis 网络分区: outbox-dispatcher 抛错 → next_retry_at 退避 → 网络恢复后自动续投
371371
- [ ] Flyway 迁移 `V20260613xxxx__Add_Outbox_And_Fence.sql` 在 CI 通过 (含 historical data backfill)
372-
- [ ] grep 确认旧 `TransactionSynchronizationManager.registerSynchronization` 用于入队的代码为零
372+
- [ ] **deferred to M3d** — grep 确认入队代码已全部收敛到 outbox / JudgeQueue port。**三类入队代码点需区分,不可笼统断言"registerSynchronization 入队为零"**:
373+
- **旧 afterCommit reaper(已废弃)**:原 5min PENDING-only reaper 在 `@Transactional` 内用 `registerSynchronization` 入队的模式(ADR-000 §5 永久拒绝清单),已删除 —— cleanup 目标,M3d 完成时此路径应为零。
374+
- **新 lease-reaper afterCommit(§2.6 F7 设计内,registerSynchronization 本身不算违规)**:`JudgingLeaseReaper:143`(`submission/reaper/JudgingLeaseReaper.java`)的 `registerSynchronization` 是 Round-2 H1 fix 后的单事务 lease 恢复 + afterCommit 入队,属于本 ADR §2.6 F7 显式新设计,**机制本身不在 cleanup 范围**(不是被永久拒绝的旧 reaper)。但其 afterCommit 入队目标(走旧 `enqueueJudgeJob` 还是新 JudgeQueue port)是 §2.8 P1 #1 次路径 follow-up:M3d 需把入队目标切到 port,与 §2.8 backlog 表述一致。
375+
- **真正残留(P1 #1 次路径,M3d 留 follow-up)**:`AdminSubmissionServiceImpl.rejudge` 的 3 处 `queueService.enqueueJudgeJob`(line 339/502/526,line 491 注释附近),flag-on 时仍走旧 RQueue;rejudge / lease 恢复频次远低于 submit 主路径,§2.8 backlog 已诚实承认,M3d cutover 前必须清,否则双投递。
373376

374377
## 5. References
375378

docs/adr/ADR-004-notification-intents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ M4d 后跑了 7-angle adversarial review (line-by-line / removed-behavior / cros
335335
- [x] Dispatcher 单测: 注入 3 mock channel, 其一抛异常, 验证其余两个仍被调用 — `NotificationDispatcherTest#channelFailureDoesNotBlockOthers`
336336
- [x] `intentId` 幂等性测试: 同一 intent 发 3 次, In-App channel 写一行 (DB 唯一约束) , Email 也只发 1 次 (channel 内部去重 cache) — `NotificationDispatcherTest#idempotencyThreeDispatches`
337337
- [x] 现有 `Notification` 表 schema 不变 (Flyway 校验) — M4a migration 仅新增 `notification_delivery_ledger`,无 ALTER
338-
- [x] grep 确认业务模块不再直接 import `EmailService` / `RealtimeService` (只允许 channel 实现 import) — `git grep` audit clean post-M4c
338+
- [ ] grep 确认业务模块不再直接 import `EmailService` / `RealtimeService` **deferred to M4b cutover**:当前 flag-gated 双轨仍保留 legacy 分支,channel 实现之外的 4 个业务模块(`SubmissionServiceImpl` / `AchievementNotificationListener` / `AchievementTriggerServiceImpl` / `ContestScheduler`)在 flag-off (legacy) 分支仍 import `EmailService` / `RealtimeService`;只有 flag-on (typed intent) 分支走 `NotificationDispatcher` + `*Intent`。legacy 分支保留至 §2.8 backlog #10 的 M4b cleanup (抽 `NotificationFacade` 后删双轨),届时本条转 `[x]`
339339
- [x] 性能: dispatcher 单次 dispatch 延迟 < 50ms (3 channel 串行, 大头是 Email SMTP) — `NotificationDispatcherTest#dispatcherLatencyUnder50ms`
340340

341341
## 5. References

0 commit comments

Comments
 (0)