99 "net/http"
1010 "strings"
1111 "sync"
12+ "sync/atomic"
1213 "time"
1314
1415 "github.com/codeGROOVE-dev/retry"
@@ -59,12 +60,13 @@ type configCacheEntry struct {
5960}
6061
6162// configCache manages configuration caching with TTL and thread safety.
63+ // Statistics counters use atomic operations to avoid races during concurrent reads.
6264type configCache struct {
6365 entries map [string ]configCacheEntry
6466 ttl time.Duration
6567 mu sync.RWMutex
66- hits int64
67- misses int64
68+ hits atomic. Int64 // Atomic to avoid race during concurrent get() calls
69+ misses atomic. Int64 // Atomic to avoid race during concurrent get() calls
6870}
6971
7072// get retrieves a cached configuration if it exists and is not expired.
@@ -74,16 +76,16 @@ func (c *configCache) get(org string) (*RepoConfig, bool) {
7476
7577 entry , exists := c .entries [org ]
7678 if ! exists {
77- c .misses ++
79+ c .misses . Add ( 1 )
7880 return nil , false
7981 }
8082
8183 if time .Since (entry .timestamp ) > c .ttl {
82- c .misses ++
84+ c .misses . Add ( 1 )
8385 return nil , false
8486 }
8587
86- c .hits ++
88+ c .hits . Add ( 1 )
8789 return entry .config , true
8890}
8991
@@ -117,10 +119,9 @@ func (c *configCache) invalidateAll() {
117119}
118120
119121// stats returns cache statistics.
122+ // No lock needed - atomic reads are inherently thread-safe.
120123func (c * configCache ) stats () (hits , misses int64 ) {
121- c .mu .RLock ()
122- defer c .mu .RUnlock ()
123- return c .hits , c .misses
124+ return c .hits .Load (), c .misses .Load ()
124125}
125126
126127// Manager manages repository configurations.
@@ -133,7 +134,7 @@ type Manager struct {
133134}
134135
135136// New creates a new config manager.
136- func New (ctx context. Context ) * Manager {
137+ func New () * Manager {
137138 return & Manager {
138139 configs : make (map [string ]* RepoConfig ),
139140 clients : make (map [string ]* github.Client ),
@@ -183,7 +184,7 @@ func createDefaultConfig() *RepoConfig {
183184// LoadConfig loads the configuration for a GitHub org with retry logic.
184185func (m * Manager ) LoadConfig (ctx context.Context , org string ) error {
185186 // Check cache first
186- if cachedConfig , found := m .cache .get (org ); found {
187+ if cfg , found := m .cache .get (org ); found {
187188 hits , misses := m .cache .stats ()
188189 slog .Debug ("using cached config for organization" ,
189190 logFieldOrg , org ,
@@ -192,7 +193,7 @@ func (m *Manager) LoadConfig(ctx context.Context, org string) error {
192193 "cache_hit_ratio" , float64 (hits )/ float64 (hits + misses ))
193194
194195 m .mu .Lock ()
195- m .configs [org ] = cachedConfig
196+ m .configs [org ] = cfg
196197 m .mu .Unlock ()
197198 return nil
198199 }
@@ -284,9 +285,9 @@ func (m *Manager) LoadConfig(ctx context.Context, org string) error {
284285 )
285286 if err != nil {
286287 // Use default empty config if not found
287- defaultConfig := createDefaultConfig ()
288- m .configs [org ] = defaultConfig
289- m .cache .set (org , defaultConfig )
288+ cfg := createDefaultConfig ()
289+ m .configs [org ] = cfg
290+ m .cache .set (org , cfg )
290291
291292 hits , misses := m .cache .stats ()
292293 slog .Info ("using default configuration for org" ,
@@ -307,9 +308,9 @@ func (m *Manager) LoadConfig(ctx context.Context, org string) error {
307308
308309 var config RepoConfig
309310 if err := yaml .Unmarshal ([]byte (configContent ), & config ); err != nil {
310- defaultConfig := createDefaultConfig ()
311- m .configs [org ] = defaultConfig
312- m .cache .set (org , defaultConfig )
311+ cfg := createDefaultConfig ()
312+ m .configs [org ] = cfg
313+ m .cache .set (org , cfg )
313314
314315 hits , misses := m .cache .stats ()
315316 slog .Error ("failed to parse YAML configuration - using defaults" ,
@@ -331,31 +332,29 @@ func (m *Manager) LoadConfig(ctx context.Context, org string) error {
331332 "email_domain" , config .Global .EmailDomain )
332333
333334 // Count channel configurations
334- mutedChannels := 0
335- totalRepos := 0
336- wildcardChannels := 0
337- for channelName , channelConfig := range config .Channels {
338- if channelConfig .Mute {
339- mutedChannels ++
335+ var muted , repos , wildcard int
336+ for name , ch := range config .Channels {
337+ if ch .Mute {
338+ muted ++
340339 }
341- totalRepos += len (channelConfig .Repos )
340+ repos += len (ch .Repos )
342341
343- hasWildcard := false
344- for _ , repo := range channelConfig .Repos {
342+ hasWild := false
343+ for _ , repo := range ch .Repos {
345344 if repo == "*" {
346- wildcardChannels ++
347- hasWildcard = true
345+ wildcard ++
346+ hasWild = true
348347 break
349348 }
350349 }
351350
352351 slog .Debug ("channel configuration loaded" ,
353352 logFieldOrg , org ,
354- "channel" , channelName ,
355- "repos_count" , len (channelConfig .Repos ),
356- "repos" , channelConfig .Repos ,
357- "muted" , channelConfig .Mute ,
358- "has_wildcard" , hasWildcard )
353+ "channel" , name ,
354+ "repos_count" , len (ch .Repos ),
355+ "repos" , ch .Repos ,
356+ "muted" , ch .Mute ,
357+ "has_wildcard" , hasWild )
359358 }
360359
361360 m .configs [org ] = & config
@@ -371,9 +370,9 @@ func (m *Manager) LoadConfig(ctx context.Context, org string) error {
371370 "email_domain" : config .Global .EmailDomain ,
372371 "daily_reminders" : config .Global .DailyReminders ,
373372 "total_channels" : len (config .Channels ),
374- "muted_channels" : mutedChannels ,
375- "wildcard_channels" : wildcardChannels ,
376- "total_repo_mappings" : totalRepos ,
373+ "muted_channels" : muted ,
374+ "wildcard_channels" : wildcard ,
375+ "total_repo_mappings" : repos ,
377376 },
378377 "cached" , true ,
379378 "cache_hits" , hits ,
0 commit comments