From 000991fc6526cfbc7fba7aefa10428737a3ce26d Mon Sep 17 00:00:00 2001 From: xuans Date: Thu, 1 Oct 2026 13:40:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=83=A8=E7=BD=B2=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E6=97=B6=E7=A9=BA=E9=94=81=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/deployment/service.go | 29 +++++++++++--- .../deployment/transaction_recovery_test.go | 38 +++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/backend/internal/deployment/service.go b/backend/internal/deployment/service.go index 445c67b..aa642e8 100644 --- a/backend/internal/deployment/service.go +++ b/backend/internal/deployment/service.go @@ -651,7 +651,7 @@ func (s *Service) recoverTransaction(ctx context.Context, record *TransactionRec return err } if record.Phase == TransactionPhaseAcquiring || record.Phase == TransactionPhaseRollbackVerified || record.Phase == TransactionPhaseCommitVerified { - return releaseTransactionLocks(recoveryContext, client, *record, true) + return releaseTransactionLocks(recoveryContext, client, *record, true, record.Phase == TransactionPhaseAcquiring) } if record.Phase == TransactionPhaseCommitting { return s.recoverCommittedTransaction(recoveryContext, record, server, client) @@ -679,7 +679,7 @@ func (s *Service) recoverTransaction(ctx context.Context, record *TransactionRec if err := s.persistRecoveryPhase(recoveryContext, record, TransactionPhaseRollbackVerified); err != nil { return err } - return releaseTransactionLocks(recoveryContext, client, *record, true) + return releaseTransactionLocks(recoveryContext, client, *record, true, false) } func (s *Service) recoverCommittedTransaction(ctx context.Context, record *TransactionRecord, server Server, client Client) error { @@ -719,7 +719,7 @@ func (s *Service) recoverCommittedTransaction(ctx context.Context, record *Trans if err := s.persistRecoveryPhase(ctx, record, TransactionPhaseCommitVerified); err != nil { return err } - return releaseTransactionLocks(ctx, client, *record, true) + return releaseTransactionLocks(ctx, client, *record, true, false) } func restoreTransactionFiles(ctx context.Context, client Client, record TransactionRecord) error { @@ -771,13 +771,32 @@ func restoreTransactionFiles(ctx context.Context, client Client, record Transact return nil } -func releaseTransactionLocks(ctx context.Context, client Client, record TransactionRecord, verifyOwner bool) error { +func releaseTransactionLocks(ctx context.Context, client Client, record TransactionRecord, verifyOwner, allowMissingOwner bool) error { for index := len(record.LockPaths) - 1; index >= 0; index-- { lockPath := record.LockPaths[index] if verifyOwner { - if err := checkOwnedLock(ctx, client, lockPath, record.Evidence.LockToken, false); err != nil { + if err := checkOwnedLock(ctx, client, lockPath, record.Evidence.LockToken, allowMissingOwner); err != nil { return fmt.Errorf("无法证明事务仍拥有部署锁 %s:%w", lockPath, err) } + if allowMissingOwner { + reader, ok := client.(EvidenceClient) + if !ok { + return fmt.Errorf("无法读取部署锁归属:%s", lockPath) + } + owner, err := reader.ReadLockOwner(ctx, lockPath) + if err != nil { + return fmt.Errorf("读取部署锁归属失败:%w", err) + } + if owner == "" { + exists, err := client.Exists(ctx, lockPath) + if err != nil { + return fmt.Errorf("检查部署锁目录失败:%w", err) + } + if !exists { + continue + } + } + } } if err := client.Unlock(ctx, lockPath); err != nil { return fmt.Errorf("释放部署锁失败:%w", err) diff --git a/backend/internal/deployment/transaction_recovery_test.go b/backend/internal/deployment/transaction_recovery_test.go index dc70ff7..d04da29 100644 --- a/backend/internal/deployment/transaction_recovery_test.go +++ b/backend/internal/deployment/transaction_recovery_test.go @@ -171,3 +171,41 @@ func TestDeployPartialLockConflictReleasesOwnedLocks(t *testing.T) { } } } + +func TestRecoverPendingAcquiringCleansLocksWithoutOwner(t *testing.T) { + server := testServer("acquiring-missing-owner").Normalize() + client := newMemoryClient() + id := "acquiring-missing-owner-transaction" + paths := []string{server.CertificatePath, server.CertificateChainPath, server.PrivateKeyPath} + locks := make([]string, 0, len(paths)) + files := make(map[string]string, len(paths)) + for _, path := range paths { + lockPath := path + ".scert-lock" + locks = append(locks, lockPath) + client.files[lockPath] = memoryFile{} + files[path] = contentDigest([]byte("old")) + } + journal := &memoryTransactionJournal{pending: []TransactionRecord{{ + ID: id, TaskID: "task", AttemptID: "attempt", AttemptNumber: 1, ResultIndex: 0, + ServerID: server.ID, ServerName: server.Name, Paths: paths, LockPaths: locks, + Phase: TransactionPhaseAcquiring, Status: TransactionStatusRecoveryRequired, + Evidence: &RecoveryEvidence{ + Version: 1, LockToken: contentDigest([]byte("scert-lock:" + id)), + Address: server.Address, Port: server.Port, HostKeyFingerprint: server.HostKeyFingerprint, + ValidationCommand: append([]string(nil), server.ValidationCommand...), + ReloadCommand: append([]string(nil), server.ReloadCommand...), Files: files, + }, + }}} + service := NewServiceWithJournal(memoryFactory{clients: map[string]*memoryClient{server.ID: client}}, journal) + if err := service.RecoverPending(t.Context(), func(context.Context, string) (Server, error) { return server, nil }); err != nil { + t.Fatalf("无归属的中断获取阶段恢复失败:%v", err) + } + for _, lockPath := range locks { + if _, ok := client.files[lockPath]; ok { + t.Fatalf("无归属锁目录未清理:%s", lockPath) + } + } + if len(journal.finished) != 1 || journal.finished[0].status != TransactionStatusRolledBack { + t.Fatalf("无归属锁恢复未完成回滚:%+v", journal.finished) + } +}