From 75b3b719cce6b60f30480b590a5d4b3d61b44ada Mon Sep 17 00:00:00 2001 From: xuans Date: Sat, 3 Oct 2026 18:49:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E6=88=AA=E6=AD=A2=E6=97=B6=E9=97=B4=E4=BF=AE=E5=A4=8D=E4=B8=8E?= =?UTF-8?q?=E4=BA=BA=E5=B7=A5=E5=A4=84=E7=BD=AE=E8=AE=BE=E8=AE=A1=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/database/sqlite_deadline.go | 8 +- .../database/sqlite_deadline_boundary_test.go | 105 ++++++++++++++++++ doc/Linux生产流程测试记录.md | 11 ++ doc/Scert开发任务清单.md | 9 ++ doc/Scert整改任务执行清单.md | 9 ++ doc/Scert整改合并优化清单.md | 12 ++ doc/部署事务人工处置设计.md | 56 ++++++++++ doc/部署事务人工核查任务清单.md | 10 ++ 8 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 backend/internal/database/sqlite_deadline_boundary_test.go create mode 100644 doc/部署事务人工处置设计.md diff --git a/backend/internal/database/sqlite_deadline.go b/backend/internal/database/sqlite_deadline.go index 053b47a..8714491 100644 --- a/backend/internal/database/sqlite_deadline.go +++ b/backend/internal/database/sqlite_deadline.go @@ -51,10 +51,14 @@ func sqliteWithDeadline[T any](c *sqliteDeadlineConn, ctx context.Context, run f return result, err } defer func() { + contextErr := ctx.Err() + if deadline, ok := ctx.Deadline(); contextErr == nil && ok && !time.Now().Before(deadline) { + contextErr = context.DeadlineExceeded + } var sqliteErr *sqlite.Error - if ctx.Err() != nil && errors.As(err, &sqliteErr) && (sqliteErr.Code()&255 == 5 || sqliteErr.Code()&255 == 9) { + if contextErr != nil && errors.As(err, &sqliteErr) && (sqliteErr.Code()&255 == 5 || sqliteErr.Code()&255 == 9) { // 原生 BEGIN 不一定转换上下文错误;保留 SQLite 错误供调用者诊断。 - err = errors.Join(ctx.Err(), err) + err = errors.Join(contextErr, err) } }() deadline, limited := ctx.Deadline() diff --git a/backend/internal/database/sqlite_deadline_boundary_test.go b/backend/internal/database/sqlite_deadline_boundary_test.go new file mode 100644 index 0000000..e154003 --- /dev/null +++ b/backend/internal/database/sqlite_deadline_boundary_test.go @@ -0,0 +1,105 @@ +package database + +import ( + "context" + "database/sql/driver" + "errors" + "testing" + "time" + + "modernc.org/sqlite" +) + +// 模拟截止时间已到但取消通知尚未被调度的短暂窗口。 +type sqliteDelayedDeadlineContext struct { + context.Context + deadline time.Time +} + +func (c sqliteDelayedDeadlineContext) Deadline() (time.Time, bool) { + return c.deadline, true +} + +func sqliteBusyFixture(t *testing.T) error { + t.Helper() + first, second := newSQLiteConcurrencyPools(t) + writer, err := first.BeginTx(t.Context(), nil) + if err != nil { + t.Fatal(err) + } + defer writer.Rollback() + if _, err := writer.ExecContext(t.Context(), "UPDATE lock_probe SET value = 1 WHERE id = 1"); err != nil { + t.Fatal(err) + } + conn, err := second.Conn(t.Context()) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + err = conn.Raw(func(value any) error { + raw := value.(*sqliteDeadlineConn).sqliteDriverConn + if _, err := raw.ExecContext(t.Context(), "PRAGMA busy_timeout = 1", nil); err != nil { + return err + } + defer raw.ExecContext(context.Background(), "PRAGMA busy_timeout = 5000", nil) + _, err := raw.ExecContext(t.Context(), "BEGIN IMMEDIATE", nil) + return err + }) + var busy *sqlite.Error + if !errors.As(err, &busy) || busy.Code()&255 != 5 { + t.Fatalf("未获得真实 SQLite 锁冲突:%v", err) + } + return err +} + +func TestSQLiteDeadlineDelayedCancellation(t *testing.T) { + busy := sqliteBusyFixture(t) + for _, name := range []string{"expired_busy", "early_busy", "expired_success", "expired_other_error", "canceled_busy", "expired_restore_failure"} { + t.Run(name, func(t *testing.T) { + raw := &sqliteDeadlineFaultConn{err: errors.New("注入恢复失败")} + conn := &sqliteDeadlineConn{sqliteDriverConn: raw} + parent, cancel := context.WithCancel(context.Background()) + defer cancel() + deadline := time.Now().Add(10 * time.Millisecond) + if name == "early_busy" || name == "canceled_busy" { + deadline = time.Now().Add(time.Second) + } + ctx := sqliteDelayedDeadlineContext{Context: parent, deadline: deadline} + operationErr := busy + if name == "expired_success" { + operationErr = nil + } else if name == "expired_other_error" { + operationErr = errors.New("业务执行失败") + } else if name == "expired_restore_failure" { + raw.failAt = 2 + } + runs := 0 + result, err := sqliteWithDeadline(conn, ctx, func() (int, error) { + runs++ + if name == "canceled_busy" { + cancel() + } else if name != "early_busy" { + time.Sleep(time.Until(deadline) + time.Millisecond) + } + return 7, operationErr + }) + if runs != 1 || result != 7 || errors.Is(err, driver.ErrBadConn) { + t.Fatalf("执行次数、结果或错误改变:次数=%d,结果=%d,错误=%v", runs, result, err) + } + wantDeadline := name == "expired_busy" || name == "expired_restore_failure" + if errors.Is(err, context.DeadlineExceeded) != wantDeadline { + t.Fatalf("截止时间错误判定不符:%v", err) + } + if errors.Is(err, context.Canceled) != (name == "canceled_busy") || !errors.Is(err, operationErr) { + t.Fatalf("未保留取消或原始执行结果:%v", err) + } + if name == "expired_restore_failure" { + if conn.IsValid() || !errors.Is(err, raw.err) { + t.Fatalf("恢复失败后仍复用连接或丢失错误:%v", err) + } + } else if !conn.IsValid() || len(raw.commands) != 2 || raw.commands[1] != "PRAGMA busy_timeout = 5000" { + t.Fatalf("连接等待配置未恢复:%v", raw.commands) + } + }) + } +} diff --git a/doc/Linux生产流程测试记录.md b/doc/Linux生产流程测试记录.md index 0e86c4c..7dbaf05 100644 --- a/doc/Linux生产流程测试记录.md +++ b/doc/Linux生产流程测试记录.md @@ -1,5 +1,16 @@ # Linux 生产流程测试记录 +## 2026年10月3日18:28 最新执行证据 + +本节优先于下方历史状态;第一台部署测试,第二台仅推送测试。源码隔离测试不替代清库后的正式安装与浏览器业务验收,未清理生产数据库或部署现场。 + +- [x] 第一批 R2 三包回归及 HTTP 本机核查、密码重置专项通过,证据分别为第一台 `/var/tmp/scert-local-recovery-20261003-1757/unit.log`、`http.log`。 +- [!] `/var/tmp/scert-local-recovery-20261003-1811/full.log` 对应单元已终止,退出码一。安装器、发布边界、服务单元检查缺少外围源码;数据库路径 `immediate_transaction` 在约 151 毫秒返回 SQLITE_BUSY 而非截止时间错误。 +- [x] 完整基线归档 SHA-256 为 `436ee793b03e980642400992515833d559faefb615c0f4124f2f077a3b4a2b20`,与最新后端源码合并到 `/var/tmp/scert-local-recovery-20261003-1828`;建立无远端凭据的隔离 Git 索引,未改运行中的源码。 +- [x] 新增确定性截止时间用例在旧实现复现两项失败,早期锁冲突、成功结果、其他错误和主动取消四项通过;日志 `deadline-before.log`,退出码一是预期的缺陷复现,不是验收通过。 +- [~] 修复后回归、R2 竞态与静态检查待运行,尚未构建正式包。 +- [x] 第二台于 18:21 清理可重建 APT 索引及缓存,释放约 315 MiB,根分区使用率从 12% 降至 10%,可用约 26 GiB;Scert、证书激活、nginx、mysql、pmail 均 active,nginx 配置检查通过,业务数据保留。 + > 本轮最新证据见文末「2026年10月3日17:22 只读恢复核查原生 Linux 回归」,不代表正式部署验收通过。 ## 2026年10月2日21:30 手动全局续期后台化回归(未部署) diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index 0132511..9295d26 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -1,5 +1,14 @@ # Scert 开发任务清单 +## 2026年10月3日18:28 当前开发入口 + +本节优先于下方历史状态。最新确认范围:Linux;第一台部署 Scert,第二台仅作证书推送目标,Smail 使用 `web.cdis.cn`。 + +- [x] 本机只读核查入口已编码并推送检查点 `978f023`,Linux 三包及 HTTP 专项通过。 +- [~] 正在修复 SQLite 截止时间与取消通知调度窗口错误,新增确定性用例已证明旧实现失败;完整源码验证环境已补齐。 +- [ ] 本机核查竞态、静态检查、人工处置及审计、第一台正式候选包与第二台推送验收仍待完成。 +- [!] 全后端回归尚未通过,三数据库首次 Web、系统邮件重置及其他生产门禁不变。 + > 当前进度见文末「2026年10月3日17:22 恢复核查开发状态补充」,总体开发和生产验收仍未完成。 ## 2026年10月3日 当前开发与验收任务入口 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index a1f7860..08c33d2 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1,5 +1,14 @@ # Scert 整改任务执行清单 +## 2026年10月3日18:28 当前执行入口 + +本节优先于下方历史状态。第一台 `8.137.23.95` 用于部署测试,第二台 `45.136.13.241` 仅用于证书推送测试;不增加双机主服务要求,不处理 Windows。 + +- [x] 修改前检查点 `978f023` 已推送;本机核查 Linux 三包和 HTTP 专项通过。 +- [~] 补齐完整仓库验证环境,修复 SQLite 截止时间错误;随后重跑全量、竞态及静态检查。 +- [ ] 完成人工处置、原子审计和互斥,再构建第一台正式候选包并验证第二台推送。 +- [!] 总体生产验收未通过;保留全部尚未闭环的三库、邮件、DNS/ACME、HTTPS 和回滚门禁。 + > 当前进度见文末「2026年10月3日17:22 当前执行进度补充」,历史章节保留供追溯。 ## 2026年10月3日 当前任务清单:Linux 双服务器上线冲刺 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index a18d0cb..c3dc18a 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1,5 +1,17 @@ # Scert 整改、合并与优化清单 +## 2026年10月3日18:28 范围确认与最新任务入口 + +本节取代下方历史章节中的“当前”进度描述;后续结果继续按时间记录。用户已确认:第一台 `8.137.23.95` 用于 Scert 部署测试,第二台 `45.136.13.241` 仅用于证书推送测试;不以两台均部署 Scert 为验收要求。Smail 地址为 `web.cdis.cn`,仅处理 Linux。 + +- [x] 修改前检查点 `978f023` 已提交并推送;根目录未跟踪文件 `=` 保留。 +- [x] 本机只读核查入口已编码,第一批 Linux 三包及 HTTP 密码重置、核查专项通过;尚未部署正式候选包。 +- [!] 原后端全量退出码一:隔离目录缺少仓库外围文件,并存在 SQLite 截止时间锁等待错误;不能记为通过。 +- [~] 已补齐完整源码和隔离 Git 索引;新增确定性用例已复现截止时间已到、取消通知未调度时仅返回 SQLITE_BUSY 的问题,正在修复并回归。 +- [ ] 完成本机入口竞态与静态检查、人工处置与独立审计,再将正式候选包部署到第一台。 +- [ ] 第一台完成三数据库首次 Web、真实系统邮件和密码重置、DNS/ACME 正负向、严格公共 HTTPS;第二台验证证书推送、重载、失败回滚及恢复。仍需覆盖域名与服务器多对多。 +- [~] 总体生产验收未通过;不因第二台磁盘清理或局部测试通过关闭门禁。 + > 当前进度以文末「2026年10月3日17:22 最新执行状态」为准:恢复核查底层专项已通过,本机入口和生产门禁仍未完成。 ## 2026年10月3日 Linux上线冲刺任务清单(当前执行入口) diff --git a/doc/部署事务人工处置设计.md b/doc/部署事务人工处置设计.md new file mode 100644 index 0000000..9f7002c --- /dev/null +++ b/doc/部署事务人工处置设计.md @@ -0,0 +1,56 @@ +# 部署事务人工处置设计 + +## 2026年10月3日 设计与任务拆分 + +本文件对应《部署事务人工核查任务清单》R3、R4,是待实现的约束和执行清单,不是已完成能力或验收结论。范围仅限 Linux:第一台部署 Scert,第二台作为证书推送目标。 + +## 已确认的安全要求 + +- 入口只使用既有受保护 Unix socket,调用者身份取自操作系统对端,不接受客户端自行声明的身份。不增加 HTTP 路由或 TCP 监听。 +- 密码重置和部署处置是互斥会话;处置不能绕过密码重置的邮箱验证码。只读核查默认不执行任何命令。 +- 当前现场观察不能补造历史 Evidence;没有可信历史证据的事务只允许记录人工核查结论,不能转成历史提交成功或自动回滚。 +- 不允许删除无归属、归属不符的锁,不允许覆盖与新旧摘要均不匹配的目标文件;出现不一致即保留现场。 +- 远端文件动作不能与数据库组成原子事务。必须先持久化处置意图与审计,再执行有界动作;最终状态与完成审计原子落库。进程崩溃必须留下可识别的未完成处置,而不是假成功。 + +## 需求和任务清单 + +- [~] R3-A:确定动作模型、独立审计以及失败边界。本设计已落文档,代码尚未开始。 +- [ ] R3-B:只读准备处置。固定事务标识、操作类型、服务器身份、历史记录摘要及现场摘要;生成有界、短期、一次性确认凭据,不向终端返回锁令牌、凭据或原始错误。 +- [ ] R3-C:确认时重新读取事务和服务器,重新核验全部目标、备份、临时文件和锁;任何变化使原确认失效,必须重新准备。确认界面必须展示事务、服务器、动作及风险。 +- [ ] R3-D:实现独立处置记录与结果查询。原事务、任务尝试和历史报告不因“已阅读”或“已确认”而改成成功。 +- [ ] R3-E:复用可信恢复链路执行明确的提交收尾或回滚;提交和回滚不能在失败后静默互换。完成后用真实严格 HTTPS 验证相应证书基线。 +- [ ] R4-A:数据库持久化处置占用,建立事务级唯一活动处置与幂等请求约束;配合进程内互斥覆盖普通部署、部署后 HTTPS 校验、事务收尾及自动恢复的完整生命周期。 +- [ ] R4-B:原子写入处置意图和开始审计;原子写入处置结果、允许的原事务终态及完成审计。任一写入失败均不得报告成功。 +- [ ] R4-C:覆盖重复确认、响应丢失、并发确认、不同动作复用幂等键、取消、超时、进程重启和审计失败。 +- [ ] R4-D:三数据库迁移和升级边界验证;MySQL 需包含 DDL 中断后重试与结构冲突拒绝,不能只验证全新 SQLite。 +- [ ] R5:通过原生 Linux 回归后构建正式候选包,第一台经安装器部署,在第二台执行证书推送与恢复验证。不得用测试目录或辅助脚本替代正式流程。 + +## 操作类型与允许条件 + +| 操作 | 必要条件 | 原事务变化 | +| --- | --- | --- | +| 仅记录核查结论 | 身份受保护、重新采样成功、明确确认 | 不改变原事务终态,不清锁,不解除部署阻塞 | +| 完成提交收尾 | 可信历史证据、允许提交的历史阶段、目标摘要正确、完整锁归属正确、严格 HTTPS 为预期新证书 | 核验及审计均成功才允许提交终态 | +| 回滚到历史基线 | 可信历史证据、允许回滚的历史阶段、旧文件归属可验证、完整锁归属正确、可验证旧站点基线 | 还原及严格 HTTPS 核验成功后才允许回滚终态 | + +缺证据事务的“记录核查结论”不是解除阻塞的替代方案。若需要对这类现场重新接管,必须另行定义以当前状态为基线的新独立操作及授权边界,不能通过改写历史证据、SQL 改状态或擅自删除锁完成。 + +## 持久化与恢复边界 + +1. 独立处置记录至少保存处置标识、原事务标识、受信任操作者、动作、请求摘要、前置采样摘要、状态、创建及完成时间。只存脱敏结构化摘要,不存私钥、密码、SSH 凭据、确认凭据原文或锁令牌。 +2. 重复请求只返回同一处置的现有结果,不重复执行远端动作;同一幂等键和不同请求必须冲突拒绝。 +3. 未完成处置不依赖进程内存判定终态;重启后仍阻止无确认重放。启动自动恢复必须识别持久化占用,避免同一历史锁令牌被两条恢复路径同时使用。 +4. 仅凭处置占用超时不能认定远端动作已停止,不自动释放占用或另起一份动作。重新接管需查询并再次核验远端现场。 +5. 当前恢复实现中的提交已核验、回滚已核验阶段可作为清理重入依据,但人工处置必须另有完整开始和结束审计,不能把旧历史阶段直接认成人工处置已执行成功。 +6. 正常部署互斥必须覆盖 `DeployVerified` 的文件推送、HTTPS 核验与最终清理,不能只锁住 `deployOne` 后提前释放。 + +## 验证矩阵 + +- [ ] 无历史证据、服务器指纹变化、命令变化、路径变化:拒绝写操作,原现场不变。 +- [ ] 外来锁、无归属锁、符号链接锁、超大归属文件、未知临时文件、旧备份摘要不符:拒绝并保留现场。 +- [ ] 准备后文件、服务器、事务阶段或管理员身份变化:旧确认失效,不执行部分动作。 +- [ ] 提交与回滚各自正向;HTTPS 不匹配、重载失败及中途断连负向;不得关闭 TLS 校验。 +- [ ] 并发部署与确认、两次确认、重启时遗留占用:不能产生第二个修改现场的执行者。 +- [ ] 开始审计失败、完成审计失败、终态保存失败:不假成功、不重放、不清理无法证明安全的现场。 +- [ ] 本机协议混合载荷、重放、过期、身份不符及断开连接;公开 HTTP 中不存在处置入口。 +- [ ] 第一台正式包部署与第二台实机推送分别留证,构建、单测和真实验收分开记录。 diff --git a/doc/部署事务人工核查任务清单.md b/doc/部署事务人工核查任务清单.md index dd7b2ba..05955fe 100644 --- a/doc/部署事务人工核查任务清单.md +++ b/doc/部署事务人工核查任务清单.md @@ -1,5 +1,15 @@ # 部署事务人工核查任务清单 +## 2026年10月3日18:28 最新范围与执行状态 + +本节取代下方历史“当前”状态。第一台负责 Scert 部署测试,第二台仅负责证书推送测试;R5 不再要求双机部署 Scert 主服务,已有服务不擅自卸载。 + +- [x] R1:只读底层已完成,等待完整流程复审。 +- [~] R2:入口与 Linux/HTTP 专项已完成;最新全量存在环境缺失及 SQLite 截止时间失败,正在修复。竞态、静态检查和正式包验证未完成。 +- [ ] R3、R4:独立人工处置、再次核验、确认幂等、部署互斥、审计原子落库仍未完成。 +- [ ] R5:第一台正式安装器部署新包,第二台作为推送目标验证恢复闭环;历史缺证据事务不得伪造提交成功。 +- [~] R6:同步各清单。修改前检查点 `978f023` 已推送,生产验收未通过。 + > 当前状态以文末 2026年10月3日17:22 补充为准:R1 已完成等待复审,R2 至 R5 未完成,R6 持续同步。 ## 2026年10月3日 需求确认与执行顺序