refactor(thothctl): share Pi lifecycle lock

This commit is contained in:
2026-08-14 17:36:33 +02:00
parent ae1215dc98
commit e902f758b1
4 changed files with 30 additions and 4 deletions
@@ -246,6 +246,10 @@ func (i Installation) UpdateStatePath() string {
return filepath.Join(i.ControlDirectory(), "update-state.json")
}
func (i Installation) RestartStatePath() string {
return filepath.Join(i.ControlDirectory(), "restart-state.json")
}
// ProjectName is stable for one installation and avoids collisions between different checkouts.
func (i Installation) ProjectName() string {
sum := sha256.Sum256([]byte(i.Path))
@@ -224,6 +224,9 @@ func TestInstallationControlPathsAreIsolatedForDescriptorsSharingOneCheckout(t *
if filepath.Dir(installation.UpdateStatePath()) != filepath.Dir(installation.CurrentImageOverridePath()) {
t.Fatalf("state %q and selector %q do not share one installation control directory", installation.UpdateStatePath(), installation.CurrentImageOverridePath())
}
if got, want := installation.RestartStatePath(), filepath.Join(installation.ControlDirectory(), "restart-state.json"); got != want {
t.Fatalf("RestartStatePath() = %q, want %q", got, want)
}
}
}
+6 -2
View File
@@ -180,13 +180,17 @@ type updateLock struct {
metadata string
}
var ErrLockHeld = errors.New("another Pi update or rollback is already in progress")
var ErrLockHeld = errors.New("another Pi update, restart, or rollback is already in progress")
func lifecycleLockPath(statePath string) string {
return filepath.Join(filepath.Dir(statePath), "pi-lifecycle.lock")
}
func acquireLock(statePath string) (*updateLock, error) {
if err := os.MkdirAll(filepath.Dir(statePath), 0o700); err != nil {
return nil, errors.New("could not create Pi update recovery directory")
}
path := statePath + ".lock"
path := lifecycleLockPath(statePath)
file := flock.New(path, flock.SetPermissions(0o600))
locked, err := file.TryLock()
if err != nil {
+17 -2
View File
@@ -24,6 +24,20 @@ func TestAdvisoryLockRejectsAConcurrentOwner(t *testing.T) {
}
}
func TestUpdateAndRestartStatePathsShareOneLifecycleLock(t *testing.T) {
dir := t.TempDir()
first, err := acquireLock(filepath.Join(dir, "update-state.json"))
if err != nil {
t.Fatal(err)
}
defer first.Release()
second, err := acquireLock(filepath.Join(dir, "restart-state.json"))
if !errors.Is(err, ErrLockHeld) || second != nil {
t.Fatalf("second lock = %#v, %v; want nil, ErrLockHeld", second, err)
}
}
func TestAdvisoryLockCrashReleasesAndReacquires(t *testing.T) {
statePath := filepath.Join(t.TempDir(), "update-state.json")
if os.Getenv("THOTHCTL_LOCK_CRASH_HELPER") == "1" {
@@ -47,10 +61,11 @@ func TestAdvisoryLockCrashReleasesAndReacquires(t *testing.T) {
func TestAdvisoryLockIgnoresPartialDiagnosticMetadata(t *testing.T) {
statePath := filepath.Join(t.TempDir(), "update-state.json")
if err := os.WriteFile(statePath+".lock", nil, 0o600); err != nil {
lockPath := lifecycleLockPath(statePath)
if err := os.WriteFile(lockPath, nil, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(statePath+".lock.owner.json", []byte("{partial"), 0o600); err != nil {
if err := os.WriteFile(lockPath+".owner.json", []byte("{partial"), 0o600); err != nil {
t.Fatal(err)
}
lock, err := acquireLock(statePath)