diff --git a/spindle/instance_lock_other.go b/spindle/instance_lock_other.go new file mode 100644 index 000000000..3d2fa1f6f --- /dev/null +++ b/spindle/instance_lock_other.go @@ -0,0 +1,5 @@ +//go:build !unix + +package spindle + +func lockInstance(string) (func(), error) { return func() {}, nil } diff --git a/spindle/instance_lock_test.go b/spindle/instance_lock_test.go new file mode 100644 index 000000000..6a321c6c9 --- /dev/null +++ b/spindle/instance_lock_test.go @@ -0,0 +1,38 @@ +//go:build unix + +package spindle + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestSecondSpindleOnTheSameDatabaseIsRefused(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "spindle.db") + + release, err := lockInstance(dbPath) + if err != nil { + t.Fatal(err) + } + if _, err := lockInstance(dbPath); err == nil || !strings.Contains(err.Error(), "another spindle") { + t.Fatalf("second lock = %v, want a refusal while the first holds it", err) + } + release() + + again, err := lockInstance(dbPath) + if err != nil { + t.Fatalf("the lock was not given back: %v", err) + } + again() +} + +func TestNoDatabasePathMeansNoLock(t *testing.T) { + for range 2 { + release, err := lockInstance("") + if err != nil { + t.Fatal(err) + } + defer release() + } +} diff --git a/spindle/instance_lock_unix.go b/spindle/instance_lock_unix.go new file mode 100644 index 000000000..e2b9fbabb --- /dev/null +++ b/spindle/instance_lock_unix.go @@ -0,0 +1,29 @@ +//go:build unix + +package spindle + +import ( + "errors" + "fmt" + "os" + "syscall" +) + +// two spindles on one db would orphan workflows and double run pulls +func lockInstance(dbPath string) (release func(), err error) { + if dbPath == "" { + return func() {}, nil + } + file, err := os.OpenFile(dbPath+".lock", os.O_CREATE|os.O_RDWR, 0o644) + if err != nil { + return nil, fmt.Errorf("opening the instance lock: %w", err) + } + if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil { + _ = file.Close() + if errors.Is(err, syscall.EWOULDBLOCK) { + return nil, fmt.Errorf("another spindle is running on %s", dbPath) + } + return nil, fmt.Errorf("locking %s.lock: %w", dbPath, err) + } + return func() { _ = file.Close() }, nil +} diff --git a/spindle/server.go b/spindle/server.go index 84461056d..7f1881a3a 100644 --- a/spindle/server.go +++ b/spindle/server.go @@ -655,6 +655,12 @@ func Run(ctx context.Context) error { return fmt.Errorf("ensuring git version: %w", err) } + releaseInstance, err := lockInstance(cfg.Server.DBPath) + if err != nil { + return err + } + defer releaseInstance() + d, err := db.Make(ctx, cfg.Server.DBPath) if err != nil { return fmt.Errorf("failed to setup db: %w", err)