fix(skills): make publication rollback race-safe
This commit is contained in:
1 parent
4ad321557f
commit
a47740ca1c
20 files changed
+934
-135
No files matched your search
@@ -0,0 +1,153 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// SkillPathPublication records enough immutable identity to prove that a
|
||||
// destination still belongs to the transaction that published it. The
|
||||
// fingerprint is intentionally private so callers cannot forge records.
|
||||
type SkillPathPublication struct {
|
||||
Destination string
|
||||
fingerprint [sha256.Size]byte
|
||||
identity os.FileInfo
|
||||
}
|
||||
|
||||
// PublishSkillPathNoReplace atomically publishes a staged path without ever
|
||||
// replacing a destination created after the backup phase.
|
||||
func PublishSkillPathNoReplace(staged, destination string) (SkillPathPublication, error) {
|
||||
identity, err := skillPathLstat(staged)
|
||||
if err != nil {
|
||||
return SkillPathPublication{}, fmt.Errorf("读取待发布 Skill 身份失败 %s: %w", staged, err)
|
||||
}
|
||||
fingerprint, err := fingerprintSkillPath(staged)
|
||||
if err != nil {
|
||||
return SkillPathPublication{}, fmt.Errorf("计算待发布 Skill 身份失败 %s: %w", staged, err)
|
||||
}
|
||||
if err := skillPathRenameNoReplace(staged, destination); err != nil {
|
||||
return SkillPathPublication{}, fmt.Errorf("目标必须不存在的 Skill 发布失败 %s: %w", destination, err)
|
||||
}
|
||||
return SkillPathPublication{Destination: destination, fingerprint: fingerprint, identity: identity}, nil
|
||||
}
|
||||
|
||||
// RollbackSkillPathPublications removes only objects that can still be proven
|
||||
// to have been published by this transaction. Each live path is first claimed
|
||||
// into a private sibling quarantine. A concurrent replacement is restored when
|
||||
// possible, otherwise retained in quarantine and reported explicitly.
|
||||
func RollbackSkillPathPublications(publications []SkillPathPublication) error {
|
||||
var rollbackErr error
|
||||
for i := len(publications) - 1; i >= 0; i-- {
|
||||
if err := rollbackSkillPathPublication(publications[i]); err != nil {
|
||||
rollbackErr = errors.Join(rollbackErr, err)
|
||||
}
|
||||
}
|
||||
return rollbackErr
|
||||
}
|
||||
|
||||
func rollbackSkillPathPublication(publication SkillPathPublication) (err error) {
|
||||
destination := publication.Destination
|
||||
quarantineRoot, err := skillPathMkdirTemp(filepath.Dir(destination), "."+filepath.Base(destination)+".rollback-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建 Skill 回滚隔离目录失败 %s: %w", destination, err)
|
||||
}
|
||||
quarantine := filepath.Join(quarantineRoot, "payload")
|
||||
cleanupRoot := func() error {
|
||||
if cleanupErr := skillPathRemoveAll(quarantineRoot); cleanupErr != nil {
|
||||
return fmt.Errorf("清理 Skill 回滚隔离目录失败 %s: %w", quarantineRoot, cleanupErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := skillPathRename(destination, quarantine); err != nil {
|
||||
cleanupErr := cleanupRoot()
|
||||
if os.IsNotExist(err) {
|
||||
return cleanupErr
|
||||
}
|
||||
return errors.Join(fmt.Errorf("隔离待回滚 Skill 失败 %s: %w", destination, err), cleanupErr)
|
||||
}
|
||||
|
||||
actualIdentity, identityErr := skillPathLstat(quarantine)
|
||||
actual, fingerprintErr := fingerprintSkillPath(quarantine)
|
||||
if identityErr == nil && fingerprintErr == nil && publication.identity != nil &&
|
||||
os.SameFile(publication.identity, actualIdentity) && actual == publication.fingerprint {
|
||||
if removeErr := removePublishedSkillSource(quarantine); removeErr != nil {
|
||||
return fmt.Errorf("移除事务发布的 Skill 失败 %s(隔离于 %s): %w", destination, quarantine, removeErr)
|
||||
}
|
||||
return cleanupRoot()
|
||||
}
|
||||
|
||||
verificationErr := errors.Join(identityErr, fingerprintErr)
|
||||
if verificationErr == nil {
|
||||
verificationErr = errors.New("发布对象身份已变化")
|
||||
}
|
||||
if restoreErr := skillPathRenameNoReplace(quarantine, destination); restoreErr != nil {
|
||||
return fmt.Errorf("拒绝删除非本事务 Skill %s: %v;并发对象保留于 %s,恢复原路径失败: %w", destination, verificationErr, quarantine, restoreErr)
|
||||
}
|
||||
return errors.Join(
|
||||
fmt.Errorf("拒绝删除非本事务 Skill %s: %w", destination, verificationErr),
|
||||
cleanupRoot(),
|
||||
)
|
||||
}
|
||||
|
||||
func fingerprintSkillPath(path string) ([sha256.Size]byte, error) {
|
||||
h := sha256.New()
|
||||
if err := fingerprintSkillPathInto(h, path); err != nil {
|
||||
return [sha256.Size]byte{}, err
|
||||
}
|
||||
var result [sha256.Size]byte
|
||||
copy(result[:], h.Sum(nil))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func fingerprintSkillPathInto(h hash.Hash, path string) error {
|
||||
info, err := skillPathLstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeFingerprintField(h, info.Mode().Type().String())
|
||||
writeFingerprintField(h, info.Mode().Perm().String())
|
||||
switch {
|
||||
case info.IsDir():
|
||||
entries, err := skillPathReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
|
||||
writeFingerprintField(h, fmt.Sprintf("%d", len(entries)))
|
||||
for _, entry := range entries {
|
||||
writeFingerprintField(h, entry.Name())
|
||||
if err := fingerprintSkillPathInto(h, filepath.Join(path, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case info.Mode()&os.ModeSymlink != 0:
|
||||
target, err := skillPathReadlink(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeFingerprintField(h, target)
|
||||
return nil
|
||||
case info.Mode().IsRegular():
|
||||
digest, err := skillPathFileDigest(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeFingerprintField(h, fmt.Sprintf("%d", info.Size()))
|
||||
_, err = h.Write(digest[:])
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("不支持识别特殊 Skill 路径 %s(mode=%s)", path, info.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
func writeFingerprintField(h hash.Hash, value string) {
|
||||
_, _ = fmt.Fprintf(h, "%d:", len(value))
|
||||
_, _ = h.Write([]byte(value))
|
||||
}
|
||||
Reference in new issue
Block a user