补充数据库过期清理并发保护

新增 expiredCleaning 状态,避免单项过期清理与批量清理重复处理同一临时凭据;支持批量接管失败的过期清理,补充专项回归测试,并同步更新四份进度清单。
This commit is contained in:
xuans 2026-09-30 14:12:21 +08:00
parent 1620a83461
commit 0334e99406
6 changed files with 115 additions and 13 deletions

View File

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

View File

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

View File

@ -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 服务和最终生产门禁仍未完成。
- [!] 不新增 `[✓]`,不以本地结果代替生产验收。

View File

@ -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 服务和最终生产门禁仍未完成。
- [!] 不新增 `[✓]`,不以本地结果代替生产验收。

View File

@ -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 服务和最终生产门禁仍未完成。
- [!] 不新增 `[✓]`,不以本地结果代替生产验收。

View File

@ -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 服务和最终生产门禁仍未完成。
- [!] 不新增 `[✓]`,不以本地结果代替生产验收。