Commit 4e55089
authored
* fix: Add thread safety to core koanf to resolve race conditions
Fixes #305 and #335 by implementing proper synchronization in koanf core.
## Issues Fixed
- Issue #305: File watcher race condition causing empty string reads
- Issue #335: "concurrent map writes" panic during concurrent Load() calls
## Root Cause
The koanf struct's internal maps (confMap, confMapFlat, keyMap) were
accessed concurrently without synchronization, causing race conditions
when multiple goroutines performed read/write operations.
## Solution
- Add sync.RWMutex to Koanf struct for thread safety
- Exclusive locks (Lock) for write operations: merge(), Delete()
- Shared locks (RLock) for read operations: Get(), Keys(), All(), etc.
- Zero breaking changes - identical public API
- Optimized for read-heavy workloads (RWMutex allows concurrent reads)
## Testing
- Added comprehensive race condition tests that reproduce both issues
- TestConcurrentLoadRaceCondition: Reproduces issue #335
- TestFileWatcherRaceCondition: Reproduces issue #305
- TestConcurrentReadWriteMix: Mixed read/write scenarios
- TestConcurrentEdgeCases: Edge case methods (Cut, Copy, etc.)
- All tests pass with -race flag
- All existing functionality tests continue to pass
## Performance Impact
- Zero overhead for single-threaded usage
- Optimal for concurrent reads (multiple readers can proceed simultaneously)
- Minimal contention cost due to RWMutex design
Created using prompts: "both options are not correct, can you look at the underlying code, and see if adding a mutex somewhere is needed" and "we should begin by writing tests that replicate this. and any other race conditions that could occur."
* fix: Resolve race condition in file provider and TestUnwatchFile
Fixes race conditions detected when running tests with -race flag.
## Root Cause
The file provider had a race condition where:
1. Watch() assigns to f.w (fsnotify.Watcher field)
2. Previous watcher's cleanup goroutine calls f.w.Close()
3. These operations could happen concurrently when re-watching after unwatch
Additionally, TestUnwatchFile had a race between:
- Main test goroutine reading/writing `reloaded` boolean variable
- Watch callback goroutine writing to `reloaded` variable
## Solution
1. **File Provider**: Add mutex protection around watcher state changes
- Added `mu sync.Mutex` to File struct
- Protected Watch() and Unwatch() methods with mutex
- Protected cleanup code in watch goroutine with mutex
- Added nil checks for defensive programming
2. **TestUnwatchFile**: Use atomic operations instead of plain boolean
- Changed `reloaded bool` to `reloaded int32`
- Use atomic.StoreInt32/LoadInt32 for thread-safe access
- Test now properly verifies re-watching capability after unwatch
## Testing
- All watch-related tests pass with race detector: TestWatchFile, TestWatchFileSymlink, TestWatchFileDirectorySymlink, TestUnwatchFile
- All submodule tests continue to pass with race detection
- CI pattern `github.com/knadh/koanf...` properly tests all submodules
Created using prompt: "alright, lets then do a separate commit now to fix the test unwatch file race" and "maybe a better approach will be to add a mutex for file watcher instad?"
* Simplify file provider synchronization to use only mutex
- Remove atomic operations in favor of simple boolean flags
- Fix potential deadlock in Watch() by releasing lock before goroutine spawn
- Use minimal locking in cleanup goroutine
- Cleaner, more maintainable synchronization pattern
- All tests pass with race detector
* Simplify locking strategy by using localized locking instead of defer patterns
Switch from global defer-based locking to localized locking in read methods to reduce code duplication and improve readability. This eliminates the need for duplicate logic that was added to avoid deadlocks between Sprint() and Keys() methods.
- Keys(), KeyMap(), Get(), Exists() now use minimal lock duration
- Sprint() can safely call Keys() without deadlock concerns
- Remove duplicate key extraction logic from Sprint()
- Maintain defer pattern only where needed for maps.Copy() operations
- All existing tests pass including race condition and deadlock tests
* perf: optimize Sprint() method to reduce lock contention
- Move RLock/RUnlock outside the iteration loop to reduce lock overhead
- Maintain alphabetical sorting behavior for API compatibility
- Eliminate repeated lock acquisitions from O(n) to O(1)
- All tests pass including race condition detection
"optimize Sprint method performance while maintaining thread safety and sorting"
1 parent cc80f4f commit 4e55089
4 files changed
Lines changed: 642 additions & 27 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| 23 | + | |
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
| |||
123 | 125 | | |
124 | 126 | | |
125 | 127 | | |
| 128 | + | |
126 | 129 | | |
127 | 130 | | |
128 | 131 | | |
129 | 132 | | |
| 133 | + | |
130 | 134 | | |
131 | 135 | | |
132 | 136 | | |
133 | 137 | | |
134 | 138 | | |
135 | 139 | | |
136 | 140 | | |
| 141 | + | |
137 | 142 | | |
138 | 143 | | |
139 | 144 | | |
140 | 145 | | |
141 | 146 | | |
| 147 | + | |
142 | 148 | | |
143 | 149 | | |
144 | 150 | | |
145 | 151 | | |
146 | 152 | | |
147 | 153 | | |
148 | 154 | | |
| 155 | + | |
| 156 | + | |
149 | 157 | | |
150 | 158 | | |
151 | 159 | | |
152 | 160 | | |
153 | 161 | | |
154 | 162 | | |
155 | 163 | | |
| 164 | + | |
| 165 | + | |
156 | 166 | | |
157 | 167 | | |
158 | 168 | | |
159 | 169 | | |
160 | 170 | | |
161 | 171 | | |
162 | 172 | | |
163 | | - | |
164 | | - | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
165 | 178 | | |
| 179 | + | |
166 | 180 | | |
167 | 181 | | |
168 | 182 | | |
| |||
287 | 301 | | |
288 | 302 | | |
289 | 303 | | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
290 | 307 | | |
291 | 308 | | |
292 | 309 | | |
| |||
316 | 333 | | |
317 | 334 | | |
318 | 335 | | |
| 336 | + | |
319 | 337 | | |
320 | 338 | | |
| 339 | + | |
321 | 340 | | |
322 | 341 | | |
323 | 342 | | |
| 343 | + | |
324 | 344 | | |
325 | 345 | | |
326 | 346 | | |
| |||
370 | 390 | | |
371 | 391 | | |
372 | 392 | | |
| 393 | + | |
373 | 394 | | |
| 395 | + | |
374 | 396 | | |
375 | 397 | | |
376 | 398 | | |
| |||
404 | 426 | | |
405 | 427 | | |
406 | 428 | | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
407 | 432 | | |
408 | 433 | | |
409 | 434 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
23 | | - | |
24 | | - | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| 45 | + | |
| 46 | + | |
45 | 47 | | |
46 | | - | |
| 48 | + | |
| 49 | + | |
47 | 50 | | |
48 | 51 | | |
49 | 52 | | |
| |||
61 | 64 | | |
62 | 65 | | |
63 | 66 | | |
| 67 | + | |
64 | 68 | | |
65 | 69 | | |
66 | 70 | | |
67 | | - | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
68 | 85 | | |
69 | 86 | | |
70 | 87 | | |
| |||
77 | 94 | | |
78 | 95 | | |
79 | 96 | | |
80 | | - | |
81 | | - | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
82 | 103 | | |
83 | 104 | | |
84 | 105 | | |
| |||
126 | 147 | | |
127 | 148 | | |
128 | 149 | | |
129 | | - | |
130 | | - | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
131 | 156 | | |
132 | 157 | | |
133 | 158 | | |
| |||
140 | 165 | | |
141 | 166 | | |
142 | 167 | | |
143 | | - | |
144 | | - | |
145 | | - | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
146 | 175 | | |
147 | 176 | | |
148 | | - | |
149 | | - | |
| 177 | + | |
150 | 178 | | |
151 | 179 | | |
152 | 180 | | |
153 | 181 | | |
154 | | - | |
155 | | - | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
156 | 197 | | |
0 commit comments