Skip to content

Commit efb67da

Browse files
intel352Copilot
andcommitted
fix: add mutex protection to FieldTracker and feeder fieldTracker access
Introduce FieldTrackerHolder with RWMutex in the feeders package for thread-safe get/set of the tracker reference on shared feeder instances. Add sync.Mutex to DefaultFieldTracker in both packages to protect FieldPopulations slice and logger from concurrent access. This fixes data races when multiple goroutines initialize separate applications that share the global ConfigFeeders slice. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9bf4092 commit efb67da

File tree

11 files changed

+372
-417
lines changed

11 files changed

+372
-417
lines changed

config_field_tracking.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"reflect"
66
"strings"
7+
"sync"
78
)
89

910
// FieldTracker interface allows feeders to report which fields they populate
@@ -37,6 +38,7 @@ type FieldTrackingFeeder interface {
3738

3839
// DefaultFieldTracker is a basic implementation of FieldTracker
3940
type DefaultFieldTracker struct {
41+
mu sync.Mutex
4042
FieldPopulations []FieldPopulation
4143
logger Logger
4244
}
@@ -50,9 +52,12 @@ func NewDefaultFieldTracker() *DefaultFieldTracker {
5052

5153
// RecordFieldPopulation records a field population event
5254
func (t *DefaultFieldTracker) RecordFieldPopulation(fp FieldPopulation) {
55+
t.mu.Lock()
5356
t.FieldPopulations = append(t.FieldPopulations, fp)
54-
if t.logger != nil {
55-
t.logger.Debug("Field populated",
57+
logger := t.logger
58+
t.mu.Unlock()
59+
if logger != nil {
60+
logger.Debug("Field populated",
5661
"fieldPath", fp.FieldPath,
5762
"fieldName", fp.FieldName,
5863
"fieldType", fp.FieldType,
@@ -69,15 +74,20 @@ func (t *DefaultFieldTracker) RecordFieldPopulation(fp FieldPopulation) {
6974

7075
// SetLogger sets the logger for the tracker
7176
func (t *DefaultFieldTracker) SetLogger(logger Logger) {
77+
t.mu.Lock()
7278
t.logger = logger
79+
t.mu.Unlock()
7380
}
7481

7582
// GetFieldPopulation returns the population info for a specific field path
7683
// It returns the first population found for the given field path
7784
func (t *DefaultFieldTracker) GetFieldPopulation(fieldPath string) *FieldPopulation {
85+
t.mu.Lock()
86+
defer t.mu.Unlock()
7887
for _, fp := range t.FieldPopulations {
7988
if fp.FieldPath == fieldPath {
80-
return &fp
89+
fpCopy := fp
90+
return &fpCopy
8191
}
8292
}
8393
return nil
@@ -86,6 +96,8 @@ func (t *DefaultFieldTracker) GetFieldPopulation(fieldPath string) *FieldPopulat
8696
// GetMostRelevantFieldPopulation returns the population info for a specific field path
8797
// It returns the last population that actually set a non-nil value
8898
func (t *DefaultFieldTracker) GetMostRelevantFieldPopulation(fieldPath string) *FieldPopulation {
99+
t.mu.Lock()
100+
defer t.mu.Unlock()
89101
var lastPopulation *FieldPopulation
90102
var lastValuedPopulation *FieldPopulation
91103

@@ -110,6 +122,8 @@ func (t *DefaultFieldTracker) GetMostRelevantFieldPopulation(fieldPath string) *
110122

111123
// GetPopulationsByFeeder returns all field populations by a specific feeder type
112124
func (t *DefaultFieldTracker) GetPopulationsByFeeder(feederType string) []FieldPopulation {
125+
t.mu.Lock()
126+
defer t.mu.Unlock()
113127
var result []FieldPopulation
114128
for _, fp := range t.FieldPopulations {
115129
if fp.FeederType == feederType {
@@ -121,6 +135,8 @@ func (t *DefaultFieldTracker) GetPopulationsByFeeder(feederType string) []FieldP
121135

122136
// GetPopulationsBySource returns all field populations by a specific source type
123137
func (t *DefaultFieldTracker) GetPopulationsBySource(sourceType string) []FieldPopulation {
138+
t.mu.Lock()
139+
defer t.mu.Unlock()
124140
var result []FieldPopulation
125141
for _, fp := range t.FieldPopulations {
126142
if fp.SourceType == sourceType {

feeders/affixed_env.go

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type AffixedEnvFeeder struct {
2929
logger interface {
3030
Debug(msg string, args ...any)
3131
}
32-
fieldTracker FieldTracker
32+
ft FieldTrackerHolder
3333
priority int
3434
}
3535

@@ -40,7 +40,6 @@ func NewAffixedEnvFeeder(prefix, suffix string) *AffixedEnvFeeder {
4040
Suffix: suffix,
4141
verboseDebug: false,
4242
logger: nil,
43-
fieldTracker: nil,
4443
priority: 0, // Default priority
4544
}
4645
}
@@ -69,7 +68,7 @@ func (f *AffixedEnvFeeder) SetVerboseDebug(enabled bool, logger interface{ Debug
6968

7069
// SetFieldTracker sets the field tracker for recording field populations
7170
func (f *AffixedEnvFeeder) SetFieldTracker(tracker FieldTracker) {
72-
f.fieldTracker = tracker
71+
f.ft.Set(tracker)
7372
}
7473

7574
// Feed reads environment variables and populates the provided structure
@@ -209,21 +208,18 @@ func (f *AffixedEnvFeeder) setFieldFromEnv(field reflect.Value, fieldType *refle
209208
}
210209

211210
// Record field population
212-
if f.fieldTracker != nil {
213-
convertedValue, _ := cast.FromType(envValue, field.Type())
214-
fp := FieldPopulation{
215-
FieldPath: fieldPath,
216-
FieldName: fieldType.Name,
217-
FieldType: field.Type().String(),
218-
FeederType: "AffixedEnvFeeder",
219-
SourceType: "env_affixed",
220-
SourceKey: envName,
221-
Value: convertedValue,
222-
SearchKeys: []string{envName},
223-
FoundKey: envName,
224-
}
225-
f.fieldTracker.RecordFieldPopulation(fp)
226-
}
211+
convertedValue, _ := cast.FromType(envValue, field.Type())
212+
f.ft.Record(FieldPopulation{
213+
FieldPath: fieldPath,
214+
FieldName: fieldType.Name,
215+
FieldType: field.Type().String(),
216+
FeederType: "AffixedEnvFeeder",
217+
SourceType: "env_affixed",
218+
SourceKey: envName,
219+
Value: convertedValue,
220+
SearchKeys: []string{envName},
221+
FoundKey: envName,
222+
})
227223

228224
if f.verboseDebug && f.logger != nil {
229225
f.logger.Debug("AffixedEnvFeeder: Successfully set field value", "envName", envName, "envValue", envValue)

feeders/base_config.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type BaseConfigFeeder struct {
1818
Environment string // Environment name (e.g., "prod", "staging", "dev")
1919
verboseDebug bool
2020
logger interface{ Debug(msg string, args ...any) }
21-
fieldTracker FieldTracker
21+
ft FieldTrackerHolder
2222
}
2323

2424
// NewBaseConfigFeeder creates a new base configuration feeder
@@ -30,7 +30,6 @@ func NewBaseConfigFeeder(baseDir, environment string) *BaseConfigFeeder {
3030
Environment: environment,
3131
verboseDebug: false,
3232
logger: nil,
33-
fieldTracker: nil,
3433
}
3534
}
3635

@@ -45,7 +44,7 @@ func (b *BaseConfigFeeder) SetVerboseDebug(enabled bool, logger interface{ Debug
4544

4645
// SetFieldTracker sets the field tracker for recording field populations
4746
func (b *BaseConfigFeeder) SetFieldTracker(tracker FieldTracker) {
48-
b.fieldTracker = tracker
47+
b.ft.Set(tracker)
4948
}
5049

5150
// Feed loads and merges base configuration with environment-specific overrides

feeders/dot_env.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type DotEnvFeeder struct {
1616
logger interface {
1717
Debug(msg string, args ...any)
1818
}
19-
fieldTracker FieldTracker
19+
ft FieldTrackerHolder
2020
envVars map[string]string // in-memory storage of parsed .env variables
2121
priority int
2222
}
@@ -27,7 +27,6 @@ func NewDotEnvFeeder(filePath string) *DotEnvFeeder {
2727
Path: filePath,
2828
verboseDebug: false,
2929
logger: nil,
30-
fieldTracker: nil,
3130
envVars: make(map[string]string),
3231
priority: 0, // Default priority
3332
}
@@ -57,7 +56,7 @@ func (f *DotEnvFeeder) SetVerboseDebug(enabled bool, logger interface{ Debug(msg
5756

5857
// SetFieldTracker sets the field tracker for recording field populations
5958
func (f *DotEnvFeeder) SetFieldTracker(tracker FieldTracker) {
60-
f.fieldTracker = tracker
59+
f.ft.Set(tracker)
6160
}
6261

6362
// Feed reads the .env file and populates the provided structure directly
@@ -255,20 +254,17 @@ func (f *DotEnvFeeder) setFieldValue(field reflect.Value, fieldType reflect.Stru
255254
field.Set(reflect.ValueOf(convertedValue))
256255

257256
// Record field population
258-
if f.fieldTracker != nil {
259-
fp := FieldPopulation{
260-
FieldPath: fieldPath,
261-
FieldName: fieldType.Name,
262-
FieldType: field.Type().String(),
263-
FeederType: "DotEnvFeeder",
264-
SourceType: "dot_env_file",
265-
SourceKey: envKey,
266-
Value: convertedValue,
267-
SearchKeys: []string{envKey},
268-
FoundKey: envKey,
269-
}
270-
f.fieldTracker.RecordFieldPopulation(fp)
271-
}
257+
f.ft.Record(FieldPopulation{
258+
FieldPath: fieldPath,
259+
FieldName: fieldType.Name,
260+
FieldType: field.Type().String(),
261+
FeederType: "DotEnvFeeder",
262+
SourceType: "dot_env_file",
263+
SourceKey: envKey,
264+
Value: convertedValue,
265+
SearchKeys: []string{envKey},
266+
FoundKey: envKey,
267+
})
272268

273269
return nil
274270
}

feeders/env.go

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type EnvFeeder struct {
1212
logger interface {
1313
Debug(msg string, args ...any)
1414
}
15-
fieldTracker FieldTracker
15+
ft FieldTrackerHolder
1616
priority int
1717
}
1818

@@ -49,7 +49,7 @@ func (f *EnvFeeder) SetVerboseDebug(enabled bool, logger interface{ Debug(msg st
4949

5050
// SetFieldTracker sets the field tracker for this feeder
5151
func (f *EnvFeeder) SetFieldTracker(tracker FieldTracker) {
52-
f.fieldTracker = tracker
52+
f.ft.Set(tracker)
5353
if f.verboseDebug && f.logger != nil {
5454
f.logger.Debug("EnvFeeder: Field tracker set", "hasTracker", tracker != nil)
5555
}
@@ -230,8 +230,7 @@ func (f *EnvFeeder) setFieldFromEnvWithModule(field reflect.Value, envTag, prefi
230230
}
231231

232232
// Record field population if tracker is available
233-
if f.fieldTracker != nil {
234-
fp := FieldPopulation{
233+
f.ft.Record(FieldPopulation{
235234
FieldPath: fieldPath,
236235
FieldName: fieldName,
237236
FieldType: field.Type().String(),
@@ -242,17 +241,14 @@ func (f *EnvFeeder) setFieldFromEnvWithModule(field reflect.Value, envTag, prefi
242241
InstanceKey: "",
243242
SearchKeys: searchKeys,
244243
FoundKey: foundKey,
245-
}
246-
f.fieldTracker.RecordFieldPopulation(fp)
247-
}
244+
})
248245

249246
if f.verboseDebug && f.logger != nil {
250247
f.logger.Debug("EnvFeeder: Successfully set field value", "fieldName", fieldName, "foundKey", foundKey, "envValue", envValue, "fieldPath", fieldPath)
251248
}
252249
} else {
253250
// Record that we searched but didn't find
254-
if f.fieldTracker != nil {
255-
fp := FieldPopulation{
251+
f.ft.Record(FieldPopulation{
256252
FieldPath: fieldPath,
257253
FieldName: fieldName,
258254
FieldType: field.Type().String(),
@@ -263,9 +259,7 @@ func (f *EnvFeeder) setFieldFromEnvWithModule(field reflect.Value, envTag, prefi
263259
InstanceKey: "",
264260
SearchKeys: searchKeys,
265261
FoundKey: "",
266-
}
267-
f.fieldTracker.RecordFieldPopulation(fp)
268-
}
262+
})
269263

270264
if f.verboseDebug && f.logger != nil {
271265
f.logger.Debug("EnvFeeder: Environment variable not found or empty", "fieldName", fieldName, "searchKeys", searchKeys, "fieldPath", fieldPath)
@@ -351,8 +345,7 @@ func (f *EnvFeeder) setPointerFieldFromEnvWithModule(field reflect.Value, envTag
351345
field.Set(newValue)
352346

353347
// Record field population if tracker is available
354-
if f.fieldTracker != nil {
355-
fp := FieldPopulation{
348+
f.ft.Record(FieldPopulation{
356349
FieldPath: fieldPath,
357350
FieldName: fieldName,
358351
FieldType: field.Type().String(),
@@ -363,17 +356,14 @@ func (f *EnvFeeder) setPointerFieldFromEnvWithModule(field reflect.Value, envTag
363356
InstanceKey: "",
364357
SearchKeys: searchKeys,
365358
FoundKey: foundKey,
366-
}
367-
f.fieldTracker.RecordFieldPopulation(fp)
368-
}
359+
})
369360

370361
if f.verboseDebug && f.logger != nil {
371362
f.logger.Debug("EnvFeeder: Successfully set pointer field", "fieldName", fieldName, "foundKey", foundKey, "fieldPath", fieldPath)
372363
}
373364
} else {
374365
// Record that we searched but didn't find
375-
if f.fieldTracker != nil {
376-
fp := FieldPopulation{
366+
f.ft.Record(FieldPopulation{
377367
FieldPath: fieldPath,
378368
FieldName: fieldName,
379369
FieldType: field.Type().String(),
@@ -384,9 +374,7 @@ func (f *EnvFeeder) setPointerFieldFromEnvWithModule(field reflect.Value, envTag
384374
InstanceKey: "",
385375
SearchKeys: searchKeys,
386376
FoundKey: "",
387-
}
388-
f.fieldTracker.RecordFieldPopulation(fp)
389-
}
377+
})
390378

391379
if f.verboseDebug && f.logger != nil {
392380
f.logger.Debug("EnvFeeder: Environment variable not found or empty for pointer field", "fieldName", fieldName, "searchKeys", searchKeys, "fieldPath", fieldPath)

feeders/field_tracking.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package feeders
22

3+
import "sync"
4+
35
// FieldPopulation represents a single field population event
46
type FieldPopulation struct {
57
FieldPath string // Full path to the field (e.g., "Connections.primary.DSN")
@@ -22,6 +24,7 @@ type FieldTracker interface {
2224

2325
// DefaultFieldTracker is a basic implementation of FieldTracker
2426
type DefaultFieldTracker struct {
27+
mu sync.Mutex
2528
populations []FieldPopulation
2629
}
2730

@@ -34,10 +37,48 @@ func NewDefaultFieldTracker() *DefaultFieldTracker {
3437

3538
// RecordFieldPopulation records that a field was populated by a feeder
3639
func (t *DefaultFieldTracker) RecordFieldPopulation(fp FieldPopulation) {
40+
t.mu.Lock()
3741
t.populations = append(t.populations, fp)
42+
t.mu.Unlock()
3843
}
3944

4045
// GetFieldPopulations returns all recorded field populations
4146
func (t *DefaultFieldTracker) GetFieldPopulations() []FieldPopulation {
42-
return t.populations
47+
t.mu.Lock()
48+
defer t.mu.Unlock()
49+
result := make([]FieldPopulation, len(t.populations))
50+
copy(result, t.populations)
51+
return result
52+
}
53+
54+
// FieldTrackerHolder provides thread-safe access to a FieldTracker.
55+
// Feeders embed this instead of storing a raw FieldTracker field.
56+
type FieldTrackerHolder struct {
57+
mu sync.RWMutex
58+
tracker FieldTracker
59+
}
60+
61+
// Set stores the tracker (called by SetFieldTracker).
62+
func (h *FieldTrackerHolder) Set(tracker FieldTracker) {
63+
h.mu.Lock()
64+
h.tracker = tracker
65+
h.mu.Unlock()
66+
}
67+
68+
// Record is a convenience that nil-checks the tracker and calls RecordFieldPopulation.
69+
func (h *FieldTrackerHolder) Record(fp FieldPopulation) {
70+
h.mu.RLock()
71+
t := h.tracker
72+
h.mu.RUnlock()
73+
if t != nil {
74+
t.RecordFieldPopulation(fp)
75+
}
76+
}
77+
78+
// Has returns true when a tracker has been set (non-nil).
79+
func (h *FieldTrackerHolder) Has() bool {
80+
h.mu.RLock()
81+
ok := h.tracker != nil
82+
h.mu.RUnlock()
83+
return ok
4384
}

0 commit comments

Comments
 (0)