保存数据库截止时间修复与人工处置设计检查点

This commit is contained in:
xuans 2026-10-03 18:49:04 +08:00
parent 978f023a7f
commit 75b3b719cc
8 changed files with 218 additions and 2 deletions

View File

@ -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()

View File

@ -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)
}
})
}
}

View File

@ -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 手动全局续期后台化回归(未部署)

View File

@ -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日 当前开发与验收任务入口

View File

@ -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 双服务器上线冲刺

View File

@ -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上线冲刺任务清单(当前执行入口)

View File

@ -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 中不存在处置入口。
- [ ] 第一台正式包部署与第二台实机推送分别留证,构建、单测和真实验收分开记录。

View File

@ -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日 需求确认与执行顺序