修复已核验回滚事务终态并同步整改进度
This commit is contained in:
parent
c96cd434a4
commit
8899f7af9e
@ -457,10 +457,11 @@ func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifac
|
||||
closed = true
|
||||
}
|
||||
status := TransactionStatusCommitted
|
||||
if retain || result.Status != "success" {
|
||||
status = TransactionStatusRecoveryRequired
|
||||
} else if result.Message != "" && result.Status == "failed" {
|
||||
if !retain && result.Status == "failed" && result.FailureCode == FailureRecoveryPending {
|
||||
status = TransactionStatusRolledBack
|
||||
result.Message = publicFailureMessages[FailureRecoveryRestored]
|
||||
} else if retain || result.Status != "success" {
|
||||
status = TransactionStatusRecoveryRequired
|
||||
}
|
||||
finishTransaction(status, result.Message)
|
||||
return result
|
||||
|
||||
122
backend/internal/deployment/verified_journal_test.go
Normal file
122
backend/internal/deployment/verified_journal_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type failedUnlockClient struct{ *memoryClient }
|
||||
|
||||
func (c *failedUnlockClient) Unlock(context.Context, string) error {
|
||||
return errors.New("unlock failed")
|
||||
}
|
||||
|
||||
func TestVerifiedRollbackJournalRetainsFailedFinalization(t *testing.T) {
|
||||
for _, scenario := range []struct {
|
||||
name string
|
||||
configure func(*memoryClient) Client
|
||||
wantCode FailureCode
|
||||
}{
|
||||
{"unlock", func(c *memoryClient) Client { return &failedUnlockClient{c} }, FailureLockRelease},
|
||||
{"restore", func(c *memoryClient) Client { c.restoreError = true; return c }, FailureRollback},
|
||||
} {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
client, server := newMemoryClient(), testServer("failed-finalization")
|
||||
seedVerifiedFiles(client, server)
|
||||
journal := &memoryTransactionJournal{}
|
||||
service := NewServiceWithJournal(fixedClientFactory{scenario.configure(client)}, journal)
|
||||
ctx := WithTransactionContext(t.Context(), TransactionContext{TaskID: "task", AttemptID: "attempt", AttemptNumber: 1})
|
||||
report, err := service.DeployVerified(ctx, []Target{{Server: server}}, testArtifact(t), Verification{
|
||||
Verify: func(context.Context) error { return errors.New("verification failed") },
|
||||
VerifyRecovery: func(context.Context) error { return nil },
|
||||
})
|
||||
if err == nil || report.RecoveryStatus != "failed" || report.Results[0].FailureCode != scenario.wantCode {
|
||||
t.Fatalf("unexpected failure report: %+v, %v", report, err)
|
||||
}
|
||||
if len(journal.finished) != 1 || journal.finished[0].status != TransactionStatusRecoveryRequired {
|
||||
t.Fatalf("failed finalization must retain recovery state: %+v", journal.finished)
|
||||
}
|
||||
assertVerifiedLocks(t, client, server, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifiedPartialFailurePersistsRolledBackTarget(t *testing.T) {
|
||||
client, first := newMemoryClient(), testServer("first")
|
||||
second := testServer("second")
|
||||
original := seedVerifiedFiles(client, first)
|
||||
journal := &memoryTransactionJournal{}
|
||||
service := NewServiceWithJournal(memoryFactory{
|
||||
clients: map[string]*memoryClient{first.ID: client},
|
||||
errors: map[string]error{second.ID: errors.New("connection failed")},
|
||||
}, journal)
|
||||
ctx := WithTransactionContext(t.Context(), TransactionContext{TaskID: "task", AttemptID: "attempt", AttemptNumber: 1})
|
||||
report, err := service.DeployVerified(ctx, []Target{{Server: first}, {Server: second}}, testArtifact(t), Verification{
|
||||
Verify: func(context.Context) error { t.Fatal("partial preparation must not verify deployment"); return nil },
|
||||
VerifyRecovery: func(context.Context) error { assertVerifiedFiles(t, client, original); return nil },
|
||||
})
|
||||
if err == nil || report.Status != "failed" || report.RecoveryStatus != "success" {
|
||||
t.Fatalf("unexpected partial failure report: %+v, %v", report, err)
|
||||
}
|
||||
if len(journal.finished) != 1 || journal.finished[0].status != TransactionStatusRolledBack {
|
||||
t.Fatalf("restored target must be finalized as rolled back: %+v", journal.finished)
|
||||
}
|
||||
assertVerifiedLocks(t, client, first, false)
|
||||
}
|
||||
|
||||
func TestVerifiedRollbackJournalMatchesRecoveryEvidence(t *testing.T) {
|
||||
for _, scenario := range []struct {
|
||||
name string
|
||||
recoveryError error
|
||||
wantStatus string
|
||||
wantRecovery string
|
||||
wantLocks bool
|
||||
}{
|
||||
{"verified", nil, TransactionStatusRolledBack, "success", false},
|
||||
{"unverified", errors.New("recovery verification failed"), TransactionStatusRecoveryRequired, "unconfirmed", true},
|
||||
} {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
servers := []Server{testServer("first"), testServer("second")}
|
||||
clients := map[string]*memoryClient{}
|
||||
originals := map[string]map[string]memoryFile{}
|
||||
for _, server := range servers {
|
||||
clients[server.ID] = newMemoryClient()
|
||||
originals[server.ID] = seedVerifiedFiles(clients[server.ID], server)
|
||||
}
|
||||
journal := &memoryTransactionJournal{}
|
||||
service := NewServiceWithJournal(memoryFactory{clients: clients}, journal)
|
||||
ctx := WithTransactionContext(t.Context(), TransactionContext{
|
||||
TaskID: "task", AttemptID: "attempt", AttemptNumber: 1,
|
||||
})
|
||||
verificationError := errors.New("deployment verification failed")
|
||||
report, err := service.DeployVerified(ctx, []Target{{Server: servers[0]}, {Server: servers[1]}}, testArtifact(t), Verification{
|
||||
Verify: func(context.Context) error { return verificationError },
|
||||
VerifyRecovery: func(context.Context) error {
|
||||
for _, server := range servers {
|
||||
assertVerifiedFiles(t, clients[server.ID], originals[server.ID])
|
||||
assertVerifiedLocks(t, clients[server.ID], server, true)
|
||||
}
|
||||
return scenario.recoveryError
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, verificationError) || report.Status != "failed" || report.RecoveryStatus != scenario.wantRecovery {
|
||||
t.Fatalf("unexpected report: %+v, error: %v", report, err)
|
||||
}
|
||||
if len(journal.finished) != len(servers) {
|
||||
t.Fatalf("unexpected journal completion count: %d", len(journal.finished))
|
||||
}
|
||||
for _, record := range journal.finished {
|
||||
if record.status != scenario.wantStatus {
|
||||
t.Errorf("transaction %s: got %s, want %s", record.id, record.status, scenario.wantStatus)
|
||||
}
|
||||
if record.status == TransactionStatusRolledBack && record.message != publicFailureMessages[FailureRecoveryRestored] {
|
||||
t.Errorf("rolled back transaction has stale message: %s", record.message)
|
||||
}
|
||||
}
|
||||
for _, server := range servers {
|
||||
assertVerifiedLocks(t, clients[server.ID], server, scenario.wantLocks)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,24 @@
|
||||
# Linux 生产流程测试记录
|
||||
|
||||
## 当前核查与开发记录(2026年10月1日,北京时间)
|
||||
|
||||
本批本地验证收尾:后端 `go test ./...`、`go vet ./...`、`go build ./...` 均通过;部署、应用、仓库三个包的竞态回归分别约八秒、五十九秒、三十二秒,全部退出零。条件跳过的外部数据库和 SSH 场景没有据此宣称通过,正式生产验收仍未完成。
|
||||
|
||||
### 只读现场核查
|
||||
|
||||
- 10:24 使用密码 SSH 并启用已登记主机指纹严格校验连接第二台服务器;Scert、存储 socket、Nginx 均为运行状态。
|
||||
- 使用 SQLite 只读连接确认:最新任务仍为失败;两笔事务 `20260930-231313.144123222-ce71b1dfe14e`、`20260930-231310.231207974-5bf9d1e37542` 仍为需要恢复,提示旧文件已经恢复但等待站点恢复核验。
|
||||
- 目标域名目录仍有三个部署锁;中断测试证据目录也有三个锁。本批没有清库、重启服务、删除文件或改变这些记录。本次只读诊断不是清库首次生产流程验收。
|
||||
- 上轮报告中的两个域名、两台服务器严格 HTTPS 通过,属于当时站点状态证据;不能据此把当前失败任务判为成功。本批没有重新执行该四次 HTTPS 检查。
|
||||
|
||||
### 确认的代码缺陷与修复边界
|
||||
|
||||
- 新回归在修改前稳定失败:双目标恢复核验已经通过,事务终态却为需要恢复而非已回滚。
|
||||
- 根因是终态判断先匹配所有失败结果,随后已回滚分支永远不可达。修改后,只有恢复核验成功且锁释放成功才记录已回滚,并复用现有中文恢复完成提示;部署任务仍保持失败。
|
||||
- 补充双目标成功恢复、恢复核验失败、文件回滚失败、锁释放失败、部分目标连接失败场景。没有创建辅助验收脚本。
|
||||
- 这不是旧事务的自动修复:旧数据不重写,现场锁不移除。崩溃后的站点基线验证、锁归属证明和历史现场接管仍需继续整改与实机验证。
|
||||
- 修改前检查点为 `c96cd43`;第一次推送认证失败,第二次推送成功。最终测试、构建与部署状态以本批后续收尾记录为准。
|
||||
|
||||
## 最新任务状态(2026-09-30,双服务器候选部署完成)
|
||||
|
||||
- 本轮代码提交 `07229ad` 已推送;正式 Linux amd64 候选包 `build/acceptance-20260930-smail/Scert-20260930-V0.1.0-linux-amd64.tar.gz` 已构建并完成归档校验。
|
||||
@ -1094,4 +1114,4 @@ git diff --check
|
||||
- [x] 已移除远端 HTTPS 校验的跳过证书验证配置,改用受信任根证书进行严格 TLS 校验。
|
||||
- [x] `go test ./...`、`go vet ./...`、`git diff --check` 通过;新增指定地址与域名 SNI 回归测试。
|
||||
- [~] 尚未重新部署本轮最新候选包;因此本节代码结果不能替代两台服务器的实机业务验收。
|
||||
- [!] 第一台服务器仍需确认并通过真实 Nginx 站点配置、证书加载和独立 HTTPS 指纹校验;完整生产门禁继续未通过。
|
||||
- [!] 第一台服务器仍需确认并通过真实 Nginx 站点配置、证书加载和独立 HTTPS 指纹校验;完整生产门禁继续未通过。
|
||||
|
||||
@ -1,4 +1,21 @@
|
||||
# Scert 整改任务执行清单
|
||||
|
||||
## 当前有效任务看板(2026年10月1日,回滚事务终态整改)
|
||||
|
||||
本批收尾记录:后端 `go test ./...`、`go vet ./...`、`go build ./...` 通过;`go test -race ./internal/deployment ./internal/app ./internal/repository -count=1` 三个包通过。条件跳过用例不算通过,尚未部署,不提升生产门禁。
|
||||
|
||||
需求确认:沿用原始四十四项要求,继续完整生产整改;先修复明确代码缺陷,再构建候选并执行生产流程。运维使用密码登录,产品保留自动公钥部署能力。以下为当前状态,下方快照只作历史证据。
|
||||
|
||||
- [x] T5-1:以双目标回归复现恢复核验成功但事务写为需要恢复,修复不可达的已回滚分支。
|
||||
- [x] T5-2:已核验恢复只将事务记为已回滚,不将原失败任务改为成功;恢复失败或锁释放失败仍保留需要恢复状态。
|
||||
- [x] T5-3:补充双目标恢复核验、恢复失败、锁释放失败和部分目标失败的持久化接口回归。
|
||||
- [x] T5-4:后端全量、静态检查、构建及专项竞态检查;最终命令结果见本批收尾记录。
|
||||
- [ ] T5-5:规范构建新候选;旧部署事务先核对归属与恢复证据,不人工改库,不直接清理现场。
|
||||
- [ ] T5-6:正式页面多域名多服务器部署、负向恢复、锁收尾及重试幂等实机复测。
|
||||
- [ ] T4-3:清库后的首次初始化、系统 SMTP、超级管理员密码重置和新密码登录。
|
||||
- [ ] T7:三数据库、双平台服务、备份恢复、公网 DNS 与 ACME、最终生产门禁。
|
||||
- [!] 本批仅代码与回归整改,线上版本未变。整体生产验收未放行;P0-03 的历史局部复审不能替代 P0-10 与其他门禁。
|
||||
|
||||
## 本轮任务看板(2026-09-30,最新候选已完成双服务器基础部署)
|
||||
|
||||
- [x] T2 子项:识别 Smail `notify:` 系统通知账号,固定系统通知发件身份并禁止 HTTP 通道发送。
|
||||
@ -1429,4 +1446,4 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全
|
||||
- [~] T5 逐台验证一域名多服务器、单服务器多域名、真实证书推送、Nginx 校验/重载、失败回滚和重试幂等;第一台服务器的真实 Nginx 站点配置仍需补齐并复核。
|
||||
- [x] T6 整改主清单、执行清单和 Linux 测试记录已追加本轮代码证据。
|
||||
- [ ] T7 三数据库、Windows 服务、清库初始化、公网 HTTPS/DNS/ACME、备份恢复和最终上线结论仍未完成。
|
||||
- [!] 总体验收继续保持 **P0-03:完成,待复审**,不能以自动化测试或页面提示替代生产验收。
|
||||
- [!] 总体验收继续保持 **P0-03:完成,待复审**,不能以自动化测试或页面提示替代生产验收。
|
||||
|
||||
@ -1,4 +1,19 @@
|
||||
# Scert 整改、合并与优化清单
|
||||
|
||||
## 当前有效进度(2026年10月1日,回滚事务终态整改)
|
||||
|
||||
本批验证:后端全量测试、静态检查、构建以及部署、应用、仓库三个包的竞态测试均退出零;外部实库条件跳过项不计入验收。正式候选构建与实机复测尚未完成。
|
||||
|
||||
本节优先于下方历史快照。原始四十四项范围和生产门禁不缩减;不再使用没有逐项统计依据的完成百分比。
|
||||
|
||||
- [x] P0-10 子项:复现并修复恢复核验成功后事务仍记为需要恢复的问题。原判断提前捕获所有失败结果,导致已回滚分支不可达;现在仅在文件恢复、服务重载、站点恢复核验与锁释放全部成功时记录已回滚,原部署任务仍为失败。
|
||||
- [x] 增加双目标已恢复、恢复核验失败、文件恢复失败、锁释放失败及部分目标连接失败的日志终态回归;不把隔离测试当作实机验收。
|
||||
- [~] 当前修复尚未部署;旧失败任务及需要恢复的事务保持原样,禁止直接修改数据库或删除锁伪造验收通过。
|
||||
- [~] P0-10 整项仍缺进程崩溃后的可信恢复、历史现场接管、多目标负向实机及重试幂等证据。
|
||||
- [x] 口径纠正:运维连接继续使用密码;产品按用户后续要求自动部署公钥并使用托管密钥,不因数据库认证方式为私钥就判为缺陷。
|
||||
- [x] P0-03 已有源码与产物边界局部复审记录,不再以该项表示全部上线阻塞;不扩大历史局部复审范围。
|
||||
- [ ] 三数据库完整矩阵、双平台服务、备份恢复、清库首次流程、系统 SMTP 与密码重置及最终生产门禁继续待验收。
|
||||
|
||||
## 本轮整改进度(2026-09-30,最新候选已完成双服务器基础部署)
|
||||
|
||||
需求确认:继续按明日上线目标推进,但不缩减生产门禁;系统通知使用 Smail `notify:` SMTP 账号时,只允许通过 SMTP 发信,不使用普通 HTTP 发信接口,不记录真实凭据。
|
||||
@ -2063,4 +2078,4 @@ P0-11 生产验收门禁
|
||||
- [x] 已完成:本轮代码提交为 `840d48d`、`1164d50`、`5ef8e54`,均已尝试推送远程仓库。
|
||||
- [~] 进行中:重新构建并部署最新候选包,逐台复核两台服务器的真实 Nginx 站点、证书指纹和失败恢复。
|
||||
- [ ] 未开始:三数据库生产矩阵、Windows 服务、公网 DNS/ACME、完整备份恢复及最终生产门禁。
|
||||
- [!] 总体验收仍为 **P0-03:完成,待复审**,本轮不得新增 `[✓]`。
|
||||
- [!] 总体验收仍为 **P0-03:完成,待复审**,本轮不得新增 `[✓]`。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user