mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-12 05:32:36 +00:00
fix(api): hold bannedUsersMu across reload and release flock on all paths
The read-error path in loadBannedUsers returned while it held the shared file lock. One transient read failure blocked every later ban write for the process lifetime. The reload also applied a stale snapshot over a ban that landed between the read and the replace. Acquire bannedUsersMu first, then the file lock, for the whole read and replace. Release both locks with defer on every path. A deterministic test proves the lock is released after a read error.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gofrs/flock"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -262,3 +263,38 @@ func TestBannedUsersConcurrentModifyAndReload(t *testing.T) {
|
||||
t.Fatal("kept-user ban was lost after concurrent modify + reload")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadBannedUsersReleasesLockOnReadError is a regression test for the
|
||||
// file-lock leak: a read error inside loadBannedUsers (after the shared file
|
||||
// lock is taken) must still release the lock. Before the fix, the error
|
||||
// return at the os.ReadFile check skipped the unlock, so the flock's open fd
|
||||
// stayed shared-locked for the process lifetime, and every later
|
||||
// storeBannedUsers (which needs the exclusive lock) would block and fail.
|
||||
func TestLoadBannedUsersReleasesLockOnReadError(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// A directory in place of the banned-users file makes os.Stat report the
|
||||
// path exists (skipping the create-file branch) while os.ReadFile fails.
|
||||
file := filepath.Join(dir, "banned.json")
|
||||
if err := os.Mkdir(file, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
origCfg := cfg
|
||||
cfg = &config{Logger: libpack_logger.New()}
|
||||
cfg.Api.BannedUsersFile = file
|
||||
defer func() { cfg = origCfg }()
|
||||
|
||||
loadBannedUsers() // must hit the os.ReadFile error path and return
|
||||
|
||||
// If the shared read lock leaked, a fresh non-blocking exclusive TryLock
|
||||
// on the same lock file fails instead of succeeding immediately.
|
||||
fileLock := flock.New(fmt.Sprintf("%s.lock", file))
|
||||
locked, err := fileLock.TryLock()
|
||||
if err != nil {
|
||||
t.Fatalf("TryLock returned an error: %v", err)
|
||||
}
|
||||
if !locked {
|
||||
t.Fatal("exclusive lock unavailable after loadBannedUsers read error - shared lock leaked")
|
||||
}
|
||||
_ = fileLock.Unlock()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user