fix!: guard against two runner processes sharing one runner file (#1099)

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>
This commit is contained in:
bircni
2026-07-31 12:15:04 +00:00
parent 47d5b5ad03
commit 0cd0e52a24
8 changed files with 206 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ import (
"gitea.com/gitea/runner/internal/pkg/client"
"gitea.com/gitea/runner/internal/pkg/config"
"gitea.com/gitea/runner/internal/pkg/labels"
"gitea.com/gitea/runner/internal/pkg/lock"
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
@@ -346,6 +347,19 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
}
func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) error {
// Refuse to rewrite the runner file while another process is using it.
releaseLock, err := lock.TryLock(cfg.Runner.File)
if errors.Is(err, lock.ErrLocked) {
return fmt.Errorf("another process is already using %q; stop it before re-registering", cfg.Runner.File)
} else if err != nil {
// Best-effort guard: if the lock file can't be created, warn and
// register anyway; writing the runner file will surface any real
// permission problem with a clearer error.
log.Warnf("could not lock runner file %q, continuing without the single-process guard: %v", cfg.Runner.File, err)
} else {
defer func() { _ = releaseLock() }()
}
// initial http client
cli := client.New(
inputs.InstanceAddr,