mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-03 05:13:10 +00:00
Starting two runner daemons with the same `.runner` file makes both present an identical UUID+token, so Gitea treats them as one runner and they cancel each other's jobs. This adds a non-blocking advisory lock on a sibling `<runner-file>.lock`: the daemon (and `register`) acquire it at startup, and a second process on the same host fails fast with a clear error instead of silently interfering. The OS releases the lock when the process exits — including a hard kill — so no stale lock is left behind. Legitimate multi-runner setups are unaffected since each already uses its own `runner.file`. Note: this covers the common single-host case; two hosts sharing a copied `.runner` (e.g. over NFS) would still need server-side detection in Gitea. --------- Co-authored-by: Zettat123 <zettat123@gmail.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1099 Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
55 lines
1.5 KiB
Go
55 lines
1.5 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build !plan9
|
|
|
|
package lock
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestTryLock(t *testing.T) {
|
|
runnerFile := filepath.Join(t.TempDir(), ".runner")
|
|
|
|
release, err := TryLock(runnerFile)
|
|
if err != nil {
|
|
t.Fatalf("first TryLock failed: %v", err)
|
|
}
|
|
|
|
// A second lock on the same file must be refused while the first is held.
|
|
if _, err := TryLock(runnerFile); !errors.Is(err, ErrLocked) {
|
|
t.Fatalf("second TryLock: want ErrLocked, got %v", err)
|
|
}
|
|
|
|
if err := release(); err != nil {
|
|
t.Fatalf("release failed: %v", err)
|
|
}
|
|
|
|
// After release the lock is available again.
|
|
release2, err := TryLock(runnerFile)
|
|
if err != nil {
|
|
t.Fatalf("TryLock after release failed: %v", err)
|
|
}
|
|
if err := release2(); err != nil {
|
|
t.Fatalf("second release failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestTryLockUncreatable ensures a lock file that cannot be created reports a
|
|
// non-ErrLocked error, so callers can tell "already locked by another process"
|
|
// apart from "couldn't lock" and degrade gracefully (e.g. a read-only mount).
|
|
func TestTryLockUncreatable(t *testing.T) {
|
|
runnerFile := filepath.Join(t.TempDir(), "missing-dir", ".runner")
|
|
|
|
_, err := TryLock(runnerFile)
|
|
if err == nil {
|
|
t.Fatal("TryLock on an uncreatable lock file: want error, got nil")
|
|
}
|
|
if errors.Is(err, ErrLocked) {
|
|
t.Fatal("TryLock on an uncreatable lock file: want non-ErrLocked error, got ErrLocked")
|
|
}
|
|
}
|