From 0334e994061456982a606efd0e8810fc87762b45 Mon Sep 17 00:00:00 2001 From: xuans Date: Wed, 30 Sep 2026 14:12:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E6=B8=85=E7=90=86=E5=B9=B6=E5=8F=91=E4=BF=9D?= =?UTF-8?q?=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 expiredCleaning 状态,避免单项过期清理与批量清理重复处理同一临时凭据;支持批量接管失败的过期清理,补充专项回归测试,并同步更新四份进度清单。 --- backend/internal/bootstrap/database.go | 46 +++++++++++++------ .../bootstrap/database_lifecycle_test.go | 34 ++++++++++++++ doc/Scert开发任务清单.md | 12 +++++ doc/Scert整改任务执行清单.md | 12 +++++ doc/Scert整改合并优化清单.md | 12 +++++ doc/基础整改任务规格.md | 12 +++++ 6 files changed, 115 insertions(+), 13 deletions(-) diff --git a/backend/internal/bootstrap/database.go b/backend/internal/bootstrap/database.go index c3a9dc4..4515333 100644 --- a/backend/internal/bootstrap/database.go +++ b/backend/internal/bootstrap/database.go @@ -75,24 +75,26 @@ type DatabaseVerificationSession struct { } type DatabaseVerificationManager struct { - mu sync.Mutex - cleanupMu sync.Mutex - base config.Config - store DatabaseSecretStore - verify func(context.Context, config.DatabaseConfig) error - now func() time.Time - sessions map[string]databaseVerificationSession - expiredSession map[string]expiredDatabaseVerificationSession - pendingCleanup map[string]struct{} - inFlight int + mu sync.Mutex + cleanupMu sync.Mutex + base config.Config + store DatabaseSecretStore + verify func(context.Context, config.DatabaseConfig) error + now func() time.Time + sessions map[string]databaseVerificationSession + expiredSession map[string]expiredDatabaseVerificationSession + expiredCleaning map[string]struct{} + pendingCleanup map[string]struct{} + inFlight int } func NewDatabaseVerificationManager(base config.Config, store DatabaseSecretStore) *DatabaseVerificationManager { return &DatabaseVerificationManager{ base: base, store: store, verify: database.VerifyWritable, now: time.Now, - sessions: make(map[string]databaseVerificationSession), - expiredSession: make(map[string]expiredDatabaseVerificationSession), - pendingCleanup: make(map[string]struct{}), + sessions: make(map[string]databaseVerificationSession), + expiredSession: make(map[string]expiredDatabaseVerificationSession), + expiredCleaning: make(map[string]struct{}), + pendingCleanup: make(map[string]struct{}), } } @@ -224,6 +226,7 @@ func (m *DatabaseVerificationManager) Discard(token string) error { if ok { delete(m.sessions, token) m.expiredSession[token] = entry + m.expiredCleaning[token] = struct{}{} } m.mu.Unlock() m.deleteExpiredSessions(expired) @@ -317,6 +320,20 @@ func (m *DatabaseVerificationManager) DiscardAllExcept(protectedReferences ...st } entry := expiredDatabaseVerificationSession{Token: token, Session: session} m.expiredSession[token] = entry + m.expiredCleaning[token] = struct{}{} + entries = append(entries, entry) + } + for token, entry := range m.expiredSession { + if protected[strings.TrimSpace(entry.Session.CredentialRef)] { + if _, cleaning := m.expiredCleaning[token]; !cleaning { + delete(m.expiredSession, token) + } + continue + } + if _, cleaning := m.expiredCleaning[token]; cleaning { + continue + } + m.expiredCleaning[token] = struct{}{} entries = append(entries, entry) } m.mu.Unlock() @@ -370,6 +387,7 @@ func (m *DatabaseVerificationManager) clearExpiredLocked(now time.Time) []expire entry := expiredDatabaseVerificationSession{Token: token, Session: session} delete(m.sessions, token) m.expiredSession[token] = entry + m.expiredCleaning[token] = struct{}{} expired = append(expired, entry) } return expired @@ -441,6 +459,7 @@ func (m *DatabaseVerificationManager) restoreExpiredSession(entry expiredDatabas m.mu.Lock() if current, exists := m.expiredSession[entry.Token]; exists && current.Session.CredentialRef == entry.Session.CredentialRef { delete(m.expiredSession, entry.Token) + delete(m.expiredCleaning, entry.Token) if _, active := m.sessions[entry.Token]; !active { m.sessions[entry.Token] = entry.Session } @@ -455,6 +474,7 @@ func (m *DatabaseVerificationManager) removeExpiredSession(entry expiredDatabase m.mu.Lock() if current, exists := m.expiredSession[entry.Token]; exists && current.Session.CredentialRef == entry.Session.CredentialRef { delete(m.expiredSession, entry.Token) + delete(m.expiredCleaning, entry.Token) } m.mu.Unlock() } diff --git a/backend/internal/bootstrap/database_lifecycle_test.go b/backend/internal/bootstrap/database_lifecycle_test.go index 0037a7e..8ce9cb7 100644 --- a/backend/internal/bootstrap/database_lifecycle_test.go +++ b/backend/internal/bootstrap/database_lifecycle_test.go @@ -230,6 +230,40 @@ func TestDatabaseBatchDoesNotTakeOverConcurrentExpiryCleanup(t *testing.T) { } } +func TestDatabaseBatchTakesOverFailedExpiryCleanup(t *testing.T) { + store := &lifecycleDatabaseStore{} + manager := NewDatabaseVerificationManager(config.Default(t.TempDir()), store) + manager.verify = func(context.Context, config.DatabaseConfig) error { return nil } + result, err := manager.Verify(t.Context(), databaseLifecycleInput()) + if err != nil { + t.Fatal(err) + } + + manager.mu.Lock() + entry := expiredDatabaseVerificationSession{ + Token: result.VerificationToken, + Session: manager.sessions[result.VerificationToken], + } + delete(manager.sessions, result.VerificationToken) + manager.expiredSession[result.VerificationToken] = entry + manager.mu.Unlock() + + manager.DiscardAll() + + if store.deleteCalls.Load() != 1 { + t.Fatalf("expected one cleanup attempt, got %d", store.deleteCalls.Load()) + } + if len(manager.expiredSession) != 0 || len(manager.expiredCleaning) != 0 { + t.Fatal("batch cleanup left expired state behind") + } + store.mu.Lock() + _, retained := store.values[result.CredentialRef] + store.mu.Unlock() + if retained { + t.Fatal("batch cleanup did not delete the credential") + } +} + func TestDatabasePendingCleanupCountsTowardCapacity(t *testing.T) { store := &lifecycleDatabaseStore{beforeDelete: func(context.Context) error { return errors.New("测试删除失败") }} manager := NewDatabaseVerificationManager(config.Default(t.TempDir()), store) diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index c9988c8..5dfb428 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -3801,3 +3801,15 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无 - [~] 进行中:P0-10 初始化回滚、任务链、重试幂等、三数据库实机矩阵和统一复审仍需继续。 - [ ] 未开始:P0-12、真实生产数据库清理与初始化、密码 SSH、远端多服务器部署、真实 HTTPS/DNS/ACME、Linux/Windows 服务和最终生产门禁。 - [!] 不得标记 `[✓]`;总体验收仍为 **P0-03:完成,待复审**。 + + +## 本轮更新(2026-09-30,expiredCleaning 状态保护) + +总体验收位置仍为 **P0-03:完成,待复审**。本轮完成数据库过期清理的执行状态和批量接管边界,仅作为本地开发和回归验证证据,不等同于生产验收通过。 + +- [x] 过期会话开始清理时记录 `expiredCleaning`,避免单项清理与批量清理重复删除。 +- [x] 批量清理可接管失败后尚未执行的过期清理,但不接管正在执行的清理。 +- [x] 新增批量接管失败过期清理的回归测试,专项测试和竞态测试均通过。 +- [~] P0-10 其他任务、跨数据库实机矩阵和统一复审继续进行中。 +- [ ] 真实生产数据库、密码 SSH、远端部署、HTTPS/DNS、ACME、Linux/Windows 服务和最终生产门禁仍未完成。 +- [!] 不新增 `[✓]`,不以本地结果代替生产验收。 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index 3d9250b..437704e 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1106,3 +1106,15 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全 - [~] 进行中:P0-10 初始化回滚、任务链、重试幂等、三数据库实机矩阵和统一复审仍需继续。 - [ ] 未开始:P0-12、真实生产数据库清理与初始化、密码 SSH、远端多服务器部署、真实 HTTPS/DNS/ACME、Linux/Windows 服务和最终生产门禁。 - [!] 不得标记 `[✓]`;总体验收仍为 **P0-03:完成,待复审**。 + + +## 本轮更新(2026-09-30,expiredCleaning 状态保护) + +总体验收位置仍为 **P0-03:完成,待复审**。本轮完成数据库过期清理的执行状态和批量接管边界,仅作为本地开发和回归验证证据,不等同于生产验收通过。 + +- [x] 过期会话开始清理时记录 `expiredCleaning`,避免单项清理与批量清理重复删除。 +- [x] 批量清理可接管失败后尚未执行的过期清理,但不接管正在执行的清理。 +- [x] 新增批量接管失败过期清理的回归测试,专项测试和竞态测试均通过。 +- [~] P0-10 其他任务、跨数据库实机矩阵和统一复审继续进行中。 +- [ ] 真实生产数据库、密码 SSH、远端部署、HTTPS/DNS、ACME、Linux/Windows 服务和最终生产门禁仍未完成。 +- [!] 不新增 `[✓]`,不以本地结果代替生产验收。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index d79ba82..cdb88d9 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1755,3 +1755,15 @@ P0-11 生产验收门禁 - [~] 进行中:P0-10 初始化回滚、任务链、重试幂等、三数据库实机矩阵和统一复审仍需继续。 - [ ] 未开始:P0-12、真实生产数据库清理与初始化、密码 SSH、远端多服务器部署、真实 HTTPS/DNS/ACME、Linux/Windows 服务和最终生产门禁。 - [!] 不得标记 `[✓]`;总体验收仍为 **P0-03:完成,待复审**。 + + +## 本轮更新(2026-09-30,expiredCleaning 状态保护) + +总体验收位置仍为 **P0-03:完成,待复审**。本轮完成数据库过期清理的执行状态和批量接管边界,仅作为本地开发和回归验证证据,不等同于生产验收通过。 + +- [x] 过期会话开始清理时记录 `expiredCleaning`,避免单项清理与批量清理重复删除。 +- [x] 批量清理可接管失败后尚未执行的过期清理,但不接管正在执行的清理。 +- [x] 新增批量接管失败过期清理的回归测试,专项测试和竞态测试均通过。 +- [~] P0-10 其他任务、跨数据库实机矩阵和统一复审继续进行中。 +- [ ] 真实生产数据库、密码 SSH、远端部署、HTTPS/DNS、ACME、Linux/Windows 服务和最终生产门禁仍未完成。 +- [!] 不新增 `[✓]`,不以本地结果代替生产验收。 diff --git a/doc/基础整改任务规格.md b/doc/基础整改任务规格.md index 6ac2fe0..9017d06 100644 --- a/doc/基础整改任务规格.md +++ b/doc/基础整改任务规格.md @@ -679,3 +679,15 @@ P0-06 与 P0-07 均维持 `[~]`,六阶段中的隔离、生产和最终验收 - [~] 进行中:P0-10 初始化回滚、任务链、重试幂等、三数据库实机矩阵和统一复审仍需继续。 - [ ] 未开始:P0-12、真实生产数据库清理与初始化、密码 SSH、远端多服务器部署、真实 HTTPS/DNS/ACME、Linux/Windows 服务和最终生产门禁。 - [!] 不得标记 `[✓]`;总体验收仍为 **P0-03:完成,待复审**。 + + +## 本轮更新(2026-09-30,expiredCleaning 状态保护) + +总体验收位置仍为 **P0-03:完成,待复审**。本轮完成数据库过期清理的执行状态和批量接管边界,仅作为本地开发和回归验证证据,不等同于生产验收通过。 + +- [x] 过期会话开始清理时记录 `expiredCleaning`,避免单项清理与批量清理重复删除。 +- [x] 批量清理可接管失败后尚未执行的过期清理,但不接管正在执行的清理。 +- [x] 新增批量接管失败过期清理的回归测试,专项测试和竞态测试均通过。 +- [~] P0-10 其他任务、跨数据库实机矩阵和统一复审继续进行中。 +- [ ] 真实生产数据库、密码 SSH、远端部署、HTTPS/DNS、ACME、Linux/Windows 服务和最终生产门禁仍未完成。 +- [!] 不新增 `[✓]`,不以本地结果代替生产验收。