Lock DATA_DIR against a second instance (closes #201)
All checks were successful
check / check (push) Successful in 3m37s
All checks were successful
check / check (push) Successful in 3m37s
Nothing stopped two processes opening the same DATA_DIR. Both open the
same per-webhook databases, both run delivery recovery over the same
rows, and both deliver: every pending delivery reaches the destination
twice, from nothing worse than an overlapping deploy.
The entry point now takes an exclusive advisory flock(2) on
{DATA_DIR}/webhooker.lock before anything opens a database, and holds it
for the process lifetime. A second process pointed at the same directory
prints a message naming that directory and exits 1. The lock is the
kernel's, not the file's, so a process killed with SIGKILL leaves a lock
file that blocks nothing -- which is what a pidfile would get wrong. The
file is never unlinked: doing so would let the next process lock a fresh
inode while a third still held the old one.
Acquisition lives in internal/datadir rather than in the server's fx
graph, so any entry point touching DATA_DIR takes it the same way, and
ErrLocked lets a caller tell a live deployment from any other failure.
config.DataDir() resolves DATA_DIR once, for both the lock and Config,
so the two cannot disagree.
Regression coverage: a real second process is refused, and a restart
after kill -9 succeeds with the stale lock file in place.
github.com/gofrs/flock carries the lock; its own module minimums pull
testify to v1.11.1 and golang.org/x/sys to v0.37.0.
This commit is contained in:
@@ -424,6 +424,36 @@ func TestDefaultDataDir(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDataDirHelper pins the exported resolver against the value
|
||||
// Config carries. The entry point takes the DATA_DIR lock through the
|
||||
// helper before the fx graph exists, so the two disagreeing would mean
|
||||
// locking one directory and writing to another.
|
||||
func TestDataDirHelper(t *testing.T) {
|
||||
for _, set := range []string{"", "/tmp/webhooker-datadir-helper"} {
|
||||
name := "set"
|
||||
if set == "" {
|
||||
name = "unset"
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Cannot use t.Parallel() here because t.Setenv
|
||||
// is incompatible with parallel subtests.
|
||||
if set == "" {
|
||||
require.NoError(t, os.Unsetenv("DATA_DIR"))
|
||||
} else {
|
||||
t.Setenv("DATA_DIR", set)
|
||||
}
|
||||
|
||||
expected := set
|
||||
if expected == "" {
|
||||
expected = config.DefaultDataDir
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, config.DataDir())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReceiverRateLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user