修复配置事务准备残留阻塞启动并完成崩溃恢复实机回归
This commit is contained in:
parent
a55e2b68eb
commit
ccc1d5af03
@ -132,7 +132,7 @@ func TestUnavailableSelectedDatabaseDoesNotCreateSQLite(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Name() != ".config-state.lock" && entry.Name() != "transaction" {
|
||||
if entry.Name() != ".config-state.lock" && entry.Name() != "transaction" && entry.Name() != "transaction-staging" {
|
||||
t.Fatalf("远程数据库失败后创建了额外文件:%s", entry.Name())
|
||||
}
|
||||
}
|
||||
@ -263,7 +263,7 @@ func TestSelectedDatabaseLiveIntegration(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == ".config-state.lock" || entry.Name() == "transaction" || kind == DatabaseTypeSQLite && entry.Name() == "smail.db" {
|
||||
if entry.Name() == ".config-state.lock" || entry.Name() == "transaction" || entry.Name() == "transaction-staging" || kind == DatabaseTypeSQLite && entry.Name() == "smail.db" {
|
||||
continue
|
||||
}
|
||||
t.Fatalf("真实数据库模式产生额外数据库:%s", entry.Name())
|
||||
|
||||
@ -41,6 +41,9 @@ type TransactionManifest struct {
|
||||
|
||||
type TransactionHooks struct {
|
||||
BeforePhase func(TransactionPhase) error
|
||||
// 准备钩子仅供内部测试使用,不通过运行配置或网络请求开放。
|
||||
BeforePrepare func() error
|
||||
BeforePublish func() error
|
||||
}
|
||||
|
||||
type TransactionStore struct {
|
||||
@ -119,11 +122,16 @@ func (s *TransactionStore) Commit(current, candidate RuntimeConfig) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
staging, err := secureTransactionStagingDirectory(transactions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := newTransactionID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transactionDirectory := filepath.Join(transactions, id)
|
||||
// 清单及归档完整前,不得把目录暴露给启动恢复扫描。
|
||||
transactionDirectory := filepath.Join(staging, id)
|
||||
candidateDirectory := filepath.Join(transactionDirectory, "candidate")
|
||||
originalDirectory := filepath.Join(transactionDirectory, "original")
|
||||
manifestWritten := false
|
||||
@ -141,6 +149,11 @@ func (s *TransactionStore) Commit(current, candidate RuntimeConfig) error {
|
||||
if err := validateTransactionDirectory(transactionDirectory); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.hooks.BeforePrepare != nil {
|
||||
if err := s.hooks.BeforePrepare(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for name, data := range currentFiles {
|
||||
if err := atomicfile.Write(filepath.Join(originalDirectory, name), data, 0600); err != nil {
|
||||
return err
|
||||
@ -164,7 +177,36 @@ func (s *TransactionStore) Commit(current, candidate RuntimeConfig) error {
|
||||
if err := writeTransactionManifest(transactionDirectory, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.hooks.BeforePublish != nil {
|
||||
if err := s.hooks.BeforePublish(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := atomicfile.SyncDirectory(transactionDirectory); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := atomicfile.SyncDirectory(staging); err != nil {
|
||||
return err
|
||||
}
|
||||
activeDirectory := filepath.Join(transactions, id)
|
||||
if _, err := os.Lstat(activeDirectory); err == nil {
|
||||
return ErrConfigRecoveryRequired
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(transactionDirectory, activeDirectory); err != nil {
|
||||
return err
|
||||
}
|
||||
transactionDirectory = activeDirectory
|
||||
candidateDirectory = filepath.Join(transactionDirectory, "candidate")
|
||||
// 发布后即使目录同步失败也必须保留清单,由启动恢复决定回滚或保留。
|
||||
manifestWritten = true
|
||||
if err := atomicfile.SyncDirectory(transactions); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := atomicfile.SyncDirectory(staging); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.beforePhase(manifest.Phase); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -563,6 +605,20 @@ func secureTransactionDirectory(configDirectory string) (string, error) {
|
||||
return directory, nil
|
||||
}
|
||||
|
||||
func secureTransactionStagingDirectory(transactions string) (string, error) {
|
||||
directory := filepath.Join(filepath.Dir(transactions), "transaction-staging")
|
||||
if err := os.MkdirAll(directory, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := ensureDirectory(directory); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := atomicfile.SyncDirectory(filepath.Dir(transactions)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return directory, nil
|
||||
}
|
||||
|
||||
func validateTransactionDirectory(directory string) error {
|
||||
if err := ensureDirectory(directory); err != nil {
|
||||
return err
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@ -58,7 +59,28 @@ func TestTransactionCrashChild(t *testing.T) {
|
||||
if point == "root_before_manifest" {
|
||||
phase = TransactionSectionsCommitted
|
||||
}
|
||||
pause := func() error {
|
||||
if err := os.Mkdir(filepath.Join(root, "crash-ready"), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
// 必须等待父进程强制终止,不能返回错误而执行提交函数的 defer。
|
||||
for {
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
}
|
||||
store := NewTransactionStore(root, crashOptions().Validation).WithHooks(TransactionHooks{
|
||||
BeforePrepare: func() error {
|
||||
if point == "before_prepare" {
|
||||
return pause()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
BeforePublish: func() error {
|
||||
if point == "before_publish" {
|
||||
return pause()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
BeforePhase: func(reached TransactionPhase) error {
|
||||
if reached != phase {
|
||||
return nil
|
||||
@ -74,13 +96,7 @@ func TestTransactionCrashChild(t *testing.T) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(root, "crash-ready"), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
// 必须等待父进程强制终止,不能返回错误而执行提交函数的 defer。
|
||||
for {
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
return pause()
|
||||
},
|
||||
})
|
||||
if err := store.Commit(current, crashCandidate(current)); err != nil {
|
||||
@ -251,7 +267,7 @@ func TestTransactionProcessCrashRecovery(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Name() != ".config-state.lock" && entry.Name() != "transaction" {
|
||||
if entry.Name() != ".config-state.lock" && entry.Name() != "transaction" && entry.Name() != "transaction-staging" {
|
||||
t.Fatalf("纯配置恢复意外创建数据文件:%s", entry.Name())
|
||||
}
|
||||
}
|
||||
@ -306,3 +322,76 @@ func TestTransactionUnpublishedDirectoryFailsClosed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionPreparationProcessCrashRecovery(t *testing.T) {
|
||||
for _, kind := range []DatabaseType{DatabaseTypeSQLite, DatabaseTypeMySQL, DatabaseTypePostgreSQL} {
|
||||
t.Run(string(kind), func(t *testing.T) {
|
||||
for _, point := range []string{"before_prepare", "before_publish"} {
|
||||
t.Run(point, func(t *testing.T) {
|
||||
root, current := crashFixture(t, kind)
|
||||
files := readCrashConfigFiles(t, filepath.Join(root, "config"))
|
||||
killTransactionChild(t, root, point)
|
||||
active, err := os.ReadDir(filepath.Join(root, "data", "transaction"))
|
||||
if err != nil || len(active) != 0 {
|
||||
t.Fatalf("准备中的事务被提前发布:%v", err)
|
||||
}
|
||||
base := filepath.Join(root, "data", "transaction-staging")
|
||||
staged, err := os.ReadDir(base)
|
||||
if err != nil || len(staged) != 1 {
|
||||
t.Fatalf("没有保留强制终止时的暂存现场:%v", err)
|
||||
}
|
||||
stagedDirectory := filepath.Join(base, staged[0].Name())
|
||||
if point == "before_publish" {
|
||||
manifest, err := readTransactionManifest(stagedDirectory)
|
||||
if err != nil || manifest.Phase != TransactionPrepared {
|
||||
t.Fatalf("发布前清单不完整:%v", err)
|
||||
}
|
||||
} else if _, err := os.Stat(filepath.Join(stagedDirectory, "manifest.json")); !os.IsNotExist(err) {
|
||||
t.Fatalf("准备开始前不应有事务清单:%v", err)
|
||||
}
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
manager, err := NewConfigManager(root, crashOptions())
|
||||
if err != nil || manager.Snapshot().Generation != current.Generation {
|
||||
t.Fatalf("准备阶段崩溃后无法启动原配置:%v", err)
|
||||
}
|
||||
assertCrashConfigFiles(t, root, files)
|
||||
}
|
||||
// 旧暂存仅保留为证据,不得阻塞后续正式配置提交。
|
||||
store := NewTransactionStore(root, crashOptions().Validation)
|
||||
if err := store.Commit(current, crashCandidate(current)); err != nil {
|
||||
t.Fatalf("准备残留阻塞后续提交:%v", err)
|
||||
}
|
||||
manager, err := NewConfigManager(root, crashOptions())
|
||||
if err != nil || manager.Snapshot().Generation != current.Generation+1 {
|
||||
t.Fatalf("后续提交未能正常启动:%v", err)
|
||||
}
|
||||
staged, err = os.ReadDir(base)
|
||||
if err != nil || len(staged) != 1 || filepath.Join(base, staged[0].Name()) != stagedDirectory {
|
||||
t.Fatal("后续提交删除旧证据或遗留了新的暂存目录")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionStagingRejectsSymlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Windows 符号链接需要额外权限;在 Linux 云端验证")
|
||||
}
|
||||
root, current := crashFixture(t, DatabaseTypeSQLite)
|
||||
files := readCrashConfigFiles(t, filepath.Join(root, "config"))
|
||||
outside := t.TempDir()
|
||||
if err := os.Symlink(outside, filepath.Join(root, "data", "transaction-staging")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := NewTransactionStore(root, crashOptions().Validation)
|
||||
if err := store.Commit(current, crashCandidate(current)); !errors.Is(err, ErrTransactionInvalid) {
|
||||
t.Fatalf("未拒绝暂存目录符号链接:%v", err)
|
||||
}
|
||||
entries, err := os.ReadDir(outside)
|
||||
if err != nil || len(entries) != 0 {
|
||||
t.Fatal("事务写入逃逸到外部目录")
|
||||
}
|
||||
assertCrashConfigFiles(t, root, files)
|
||||
}
|
||||
|
||||
@ -376,3 +376,15 @@ PostgreSQL → SQLite 两次在初始化恢复码生成时发生 30 秒 HTTP 读
|
||||
22:56 独立复核 40 份实机证据、两份程序及两组八个工具的远端摘要一致,709 份构建输入(含 619 份 Go 源文件)未漂移;六个测试账号锁定,PostgreSQL 建库权限撤销,测试库连接及本轮测试进程归零。正式六服务和程序未变,用户原全项目报告没有编辑或暂存。本项复审通过,但未替换正式部署。
|
||||
|
||||
事务提交和配置切换临界点、跨引擎复制中断及最终完整发行候选统一验收仍未关闭;其他生产门禁维持原状态,不将本轮双 SQL 结果扩展为整体生产放行。
|
||||
|
||||
### 9 月 21 日晚启动的配置事务崩溃恢复补充
|
||||
|
||||
详见 `配置事务崩溃恢复补充验收.md`。补齐真实测试子进程强制终止、正式配置管理器启动恢复、根配置切换前后、损坏归档拒绝及恢复幂等验证。分区部分写入和根清单滞后明确标为构造落盘状态,不冒充完整应用中的指令间隙观测。
|
||||
|
||||
发现并修复新事务首份清单发布前被终止时留下无效活动目录、从而阻塞启动的问题:准备归档改放在独立 `transaction-staging` 目录,完整后原子发布,再开始替换活动配置。旧版活动区的不可信残留继续保留并失败关闭,不擅自删除。暂存目录不是额外数据库。
|
||||
|
||||
最终 Linux 崩溃专项重复三次,114 个测试及子测试通过,78 次确认的 SIGKILL,零失败、零跳过;Linux 完整配置包 417 项通过、1 项真实数据库集成测试跳过。Windows 全量后端 1945 项通过、42 项跳过、零失败。三种数据库配置选择下的文件恢复测试不等同三种真实数据库提交联合验收。
|
||||
|
||||
最新后端候选 `89e115d94ef2f12fd2ab082888f22517460e87cbe9e1b5a98d32f55ae38426b8` 在 `20260921-235814` 隔离实例完成 SQLite 正常迁移、内部与进程重启、目标库故障失败关闭及恢复后的邮件正文和附件验证。最终活动库为 `migrated.db`,业务与配置历史同库,源库及占用目标仅作为离线证据保留。前端及插件资源复用,不宣称完整发行包重构建,也未替换正式部署。
|
||||
|
||||
独立复核 21 份远端程序、工具与日志文件摘要一致、710 份构建输入未漂移,六个已核查测试单元均停止,隔离应用活动事务和暂存区为空。正式六服务及程序、用户原全项目报告未变。本轮专项范围复审通过;完整应用 SQL 提交与配置切换联合故障点、恢复过程再次中断、最终统一发行候选及其他生产门禁继续未关闭。
|
||||
|
||||
129
docs/配置事务崩溃恢复补充验收.md
Normal file
129
docs/配置事务崩溃恢复补充验收.md
Normal file
@ -0,0 +1,129 @@
|
||||
# 配置事务崩溃恢复补充验收
|
||||
|
||||
## 范围与结论
|
||||
|
||||
本项接续双 SQL 未提交复制中断验收,验证配置事务落盘、根配置切换与启动恢复。**本项不等于整个项目生产验收通过。**
|
||||
|
||||
- [✓] 真实终止测试子进程,而非仅在阶段钩子返回错误。
|
||||
- [✓] 验证三种数据库配置选择下的回滚、保留新配置、重复启动与损坏拒绝启动。
|
||||
- [✓] 修复新事务准备阶段崩溃残留导致下次启动被阻塞的问题。
|
||||
- [✓] 云端重复终止测试、完整配置包回归与本地全量后端回归。
|
||||
- [✓] 最新后端候选的隔离应用级 SQLite 迁移、邮件和重启回归。
|
||||
- [ ] 完整应用的 SQL 提交与配置切换联合故障点、恢复过程再次被终止。
|
||||
- [ ] 同一最终完整发行候选的统一生产验收与正式部署。
|
||||
|
||||
用户提供的管理员、域名和部署入口仍仅为测试参数,不转为产品验收标准。没有修改或暂存用户原来的 `云端全项目测试报告.md`,没有停止正式实例或其他项目。
|
||||
|
||||
## 修复的问题
|
||||
|
||||
旧实现先在 `data/transaction/<id>` 创建原始和候选目录,再写配置归档及首份事务清单。若在清单发布前突然退出,恢复扫描会遇到没有清单的活动目录,保留现场并拒绝启动。原配置虽然未变,但无法自动恢复可用。
|
||||
|
||||
新实现先写入 `data/transaction-staging/<id>`,完成归档、清单与目录同步后,再把整个目录原子重命名到活动事务区。发布之后才允许修改活动配置文件。
|
||||
|
||||
- 发布前突然退出:活动配置保持原样,残留暂存不参与启动恢复,也不阻塞下一次提交。
|
||||
- 发布后发生错误:保留完整活动事务,由既有恢复算法决定回滚还是保留新配置。
|
||||
- 暂存路径拒绝符号链接,避免配置归档写出预期目录。
|
||||
- 旧版活动区中的无清单、损坏或不可信记录继续失败关闭,不自动删除或当作正常事务接受。
|
||||
- 崩溃暂存保留为证据;这是文件归档目录,不是新增数据库。清理策略不在本次修复范围内。
|
||||
|
||||
没有向 HTTP、环境变量或正式运行配置增加故障注入开关。新增准备阶段钩子由测试直接设置,等待和强制终止逻辑均位于测试二进制。
|
||||
|
||||
## 故障矩阵与证据边界
|
||||
|
||||
每种 SQLite、MySQL、PostgreSQL 配置分别覆盖:
|
||||
|
||||
| 故障位置或状态 | 恢复结果 | 注入方式 |
|
||||
| --- | --- | --- |
|
||||
| 准备目录已创建、尚未写归档 | 保持旧配置,后续提交正常 | 正式提交路径钩子处终止子进程 |
|
||||
| 归档与清单完整、尚未发布活动事务 | 保持旧配置,后续提交正常 | 正式提交路径钩子处终止子进程 |
|
||||
| 已发布 prepared 清单 | 恢复旧配置 | 正式阶段钩子处终止子进程 |
|
||||
| 仅一个分区被替换 | 恢复旧配置 | 使用正式原子替换函数构造落盘状态后终止 |
|
||||
| 所有分区已替换、根配置未切换 | 恢复旧配置 | 正式阶段钩子处终止子进程 |
|
||||
| 根配置已切换,清单仍是 sections_committed | 保留新配置 | 使用正式原子替换函数构造落盘状态后终止 |
|
||||
| root_committed 清单已写入 | 保留新配置 | 正式阶段钩子处终止子进程 |
|
||||
| verified 清单已写入 | 保留新配置 | 正式阶段钩子处终止子进程 |
|
||||
|
||||
另有两个强制终止后损坏归档的用例:分别改坏 original、candidate,启动必须拒绝,活动配置及损坏现场保留。独立测试保留旧版无清单活动目录的失败关闭行为,并验证暂存符号链接拒绝。
|
||||
|
||||
父进程确认到达目标点后强制终止子进程并等待退出;Linux 日志记录 `signal: killed`。随后两次调用正式 `NewConfigManager` 启动路径,检查代次、数据库选择、配置文件逐字节一致、锁释放及恢复幂等。
|
||||
|
||||
**上述三种数据库是配置选择矩阵,不是三种真实数据库的提交联合测试。** SQL 配置故意使用不可用的本地端口,证明文件恢复不依赖连接 SQL,也不会回退创建额外 SQLite。分区部分写入和根清单滞后是明确构造的磁盘状态,不声称在真实应用中捕获了对应指令间隙。再次成功启动不等于恢复途中再次被杀;进程强制终止不等于整机断电或物理存储耐久性验证。
|
||||
|
||||
## 验证记录
|
||||
|
||||
证据目录:`test-log/production-remediation-20260921/`。时间均为北京时间;本轮从 2026 年 9 月 21 日晚间开始。
|
||||
|
||||
### 修复前专项
|
||||
|
||||
云端目录:`/var/tmp/smail-review-transaction-20260921-234828/`。
|
||||
|
||||
- 原阶段专项重复三次:81 个测试及子测试通过,60 次强制终止,零失败、零跳过。
|
||||
- 配置包:406 项通过,1 项跳过。
|
||||
- 该结果不包含后续暂存发布修复,不能替代最终修复版本。
|
||||
|
||||
### 修复后专项
|
||||
|
||||
云端目录:`/var/tmp/smail-review-transaction-20260921-235343/`。
|
||||
|
||||
| 范围 | 通过 | 失败 | 跳过 | 说明 |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Linux 崩溃专项重复三次 | 114 | 0 | 0 | 78 次确认的 SIGKILL |
|
||||
| Linux 完整配置包 | 417 | 0 | 1 | 内含 26 次 SIGKILL;真实数据库集成测试未配置 |
|
||||
| Windows 完整配置包 | 401 | 0 | 15 | 权限、符号链接及真实 SQL 等平台条件分别保留跳过 |
|
||||
| Windows 全量后端 | 1945 | 0 | 42 | 62 个包通过,另有 22 个无测试文件包 |
|
||||
|
||||
测试及子测试计数包含父级条目,不是独立业务用例数;不同范围有重叠,不相加当作全量生产验收。
|
||||
|
||||
最终 Linux 配置测试二进制 SHA256:
|
||||
|
||||
```text
|
||||
5f36f1cd0c5f5f182b3ea96fb9dcf599ee6db0773cc93d3368406e8b4ac46d76
|
||||
```
|
||||
|
||||
对应构建输入:`transaction-build-20260921-235343.json`。其中 revision 是修改前源码基线,实际工作区改动以逐文件源码摘要为准,并非该提交的干净构建。
|
||||
|
||||
关键日志及结果:
|
||||
|
||||
- `transaction-linux-20260921-235343-result.json`
|
||||
- `transaction-linux-20260921-235343-crash.log`
|
||||
- `transaction-linux-20260921-235343-config.log`
|
||||
- `transaction-staging-windows.jsonl`
|
||||
- `transaction-full-backend-windows.jsonl`
|
||||
|
||||
首次编写专项时,预期配置没有包含加载后的规范化结果,导致断言失败;改为读取归档原始字节并单独核对数据库选择。扩大 Windows 回归时发现就绪文件读取与文件操作之间的短暂共享冲突,改用目录标记。两次失败日志均保留,不覆盖为成功结果。
|
||||
|
||||
### 隔离应用级回归
|
||||
|
||||
使用最新后端重新编译的 Linux amd64 候选,隔离实例为 `/var/tmp/smail-review-runtime-20260921-235814/`,构建基线为 `b8dbd3b` 加本轮未提交修复;逐文件摘要在最终复核时确认未漂移。程序 SHA256:
|
||||
|
||||
```text
|
||||
89e115d94ef2f12fd2ab082888f22517460e87cbe9e1b5a98d32f55ae38426b8
|
||||
```
|
||||
|
||||
以下应用级项目通过:
|
||||
|
||||
- 全新隔离初始化及邮件协议探针。
|
||||
- 占用目标拒绝覆盖,失败后释放维护状态;正常迁移完成配置切换。
|
||||
- 切换后待重启期间拒绝写入;内部重启保持进程,进程级重启使用新进程。
|
||||
- 原登录会话、历史邮件与新增邮件的正文、附件通过 IMAP、POP3 验证。
|
||||
- 目标库不可用时失败关闭;源库和配置未被改写;恢复访问后邮件闭环继续正常。
|
||||
- 配置历史随业务迁入目标库;新增邮件只进入目标,源库不再写入。
|
||||
- 最终活动数据库为 `migrated.db`,完整性检查为 `ok`。`smail.db` 与 `occupied.db` 为离线源库及占用目标证据,不是第二个活动数据库。
|
||||
|
||||
应用级证据 13 份位于 `transaction-runtime-evidence/smail-review-runtime-20260921-235814/`,汇总为 `transaction-runtime-result-20260921-235758.json`,构建输入为 `transaction-runtime-build-20260921-235758.json`。
|
||||
|
||||
**该应用回归是 SQLite → SQLite,不把它扩展成最新候选的 MySQL/PostgreSQL 实机迁移验证。** 前端与 Linux 插件资源只复用并核对摘要,不宣称完整发行包已重新构建。正式实例未替换。
|
||||
|
||||
## 安全与运行保护
|
||||
|
||||
云端配置测试使用全新目录及每用例独立临时目录,不连接正式数据库。测试以普通 smail 用户运行,使用私有网络、私有临时目录、只读系统、受保护路径和资源上限:192 MiB 硬内存上限、128 MiB Go 目标、单 Go 调度线程、25% CPU、禁止交换。
|
||||
|
||||
修复前后两轮配置测试均确认正式六服务运行标识、PID 和重启次数未变,正式程序摘要未变;测试单元结束后全部停止且 PID 为零。原用户报告摘要保持:
|
||||
|
||||
```text
|
||||
A142FAA660487656124A8BD4A3EF4F58ED80B3DEC0EDA4AAB0F20B8C3297FF08
|
||||
```
|
||||
|
||||
应用回归后的独立复核记录为 `transaction-final-recheck.json`:21 个远端程序、工具及日志文件摘要一致,710 份构建输入未漂移,六个已核查测试单元停止且 PID 为零。隔离应用的活动事务与暂存区均为空;正式六服务和程序、用户原报告保持不变。验证只读查询摘要,不再次下载大程序或重复执行已完成测试。
|
||||
|
||||
历史凭据轮换、剩余静态安全告警、真实浏览器闭环、Windows 提权恢复通道、真实自动续期与全协议换证、外部最终投递、长时容量、告警、异机灾备及最终统一发行候选等门禁仍未关闭。
|
||||
Loading…
Reference in New Issue
Block a user