From 8db56d4b5e47186cbb5ad50d48b893513cf05594 Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Sun, 1 Nov 2026 12:45:08 +0100 Subject: [PATCH] Parse Config once when first used concurrently parse set parsed before filling the durations, so a concurrent first call could see cooldown as off or read the maps while they were written. It now runs through a sync.Once. --- cooldown.go | 12 ++++++------ cooldown_test.go | 42 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/cooldown.go b/cooldown.go index 9f7af6d..b600754 100644 --- a/cooldown.go +++ b/cooldown.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" "strings" + "sync" "time" ) @@ -50,17 +51,16 @@ type Config struct { defaultDuration time.Duration ecosystemDurations map[string]time.Duration packageDurations map[string]time.Duration - parsed bool + parseOnce sync.Once } // parse resolves all string durations into time.Duration values. -// Called lazily on first use. +// Called lazily on first use, once even when the first calls are concurrent. func (c *Config) parse() { - if c.parsed { - return - } - c.parsed = true + c.parseOnce.Do(c.parseDurations) +} +func (c *Config) parseDurations() { c.defaultDuration, _ = ParseDuration(c.Default) c.ecosystemDurations = make(map[string]time.Duration, len(c.Ecosystems)) diff --git a/cooldown_test.go b/cooldown_test.go index ecc11e7..550a547 100644 --- a/cooldown_test.go +++ b/cooldown_test.go @@ -1,6 +1,7 @@ package cooldown import ( + "sync" "testing" "time" ) @@ -198,14 +199,14 @@ func TestConfigEvaluate(t *testing.T) { func TestConfigEnabled(t *testing.T) { tests := []struct { name string - cfg Config + cfg *Config want bool }{ - {"empty config", Config{}, false}, - {"default only", Config{Default: "3d"}, true}, - {"ecosystem only", Config{Ecosystems: map[string]string{"npm": "7d"}}, true}, - {"package only", Config{Packages: map[string]string{"pkg:npm/x": "1d"}}, true}, - {"all zero", Config{Default: "0", Ecosystems: map[string]string{"npm": "0"}}, false}, + {"empty config", &Config{}, false}, + {"default only", &Config{Default: "3d"}, true}, + {"ecosystem only", &Config{Ecosystems: map[string]string{"npm": "7d"}}, true}, + {"package only", &Config{Packages: map[string]string{"pkg:npm/x": "1d"}}, true}, + {"all zero", &Config{Default: "0", Ecosystems: map[string]string{"npm": "0"}}, false}, } for _, tt := range tests { @@ -217,3 +218,32 @@ func TestConfigEnabled(t *testing.T) { }) } } + +func TestConfigConcurrentFirstUse(t *testing.T) { + for range 50 { + c := &Config{ + Default: "3d", + Ecosystems: map[string]string{"npm": "7d"}, + Packages: map[string]string{"pkg:npm/lodash": "1d"}, + } + const n = 8 + got := make([]time.Duration, n) + var start, wg sync.WaitGroup + start.Add(1) + for i := range n { + wg.Add(1) + go func() { + defer wg.Done() + start.Wait() + got[i] = c.For("npm", "pkg:npm/express") + }() + } + start.Done() + wg.Wait() + for i, d := range got { + if d != 7*24*time.Hour { + t.Fatalf("goroutine %d: For = %v, want 168h", i, d) + } + } + } +}