Skip to content

fix: data races in progress bar counters and stat maps - #881

Open
terryrankine wants to merge 1 commit into
peak:masterfrom
terryrankine:fix/data-races
Open

terryrankine wants to merge 1 commit into
peak:masterfrom
terryrankine:fix/data-races

Conversation

@terryrankine

@terryrankine terryrankine commented Sep 17, 2026 •

Copy link
Copy Markdown

Context

Three data races / re-entrancy bugs, each reproducible under go test -race.

Root cause and fix

  • progressbar: IncrementCompletedObjects() / IncrementTotalObjects() updated one counter atomically but read both with plain field access in fmt.Sprintf. Now uses the atomic.AddInt64 return value plus atomic.LoadInt64 for the other counter.
  • log/stat: Statistics() iterated the stat maps without holding their locks while workers were still calling Collect(). Now locks totalCount then succCount (the only place both are held; add() holds one at a time, so no inverse ordering exists).
  • log: Close() closed outputCh unconditionally, so a second call panicked with "close of closed channel". Guarded with a sync.Once on the Logger.

Tests

TestCommandProgress_ConcurrentIncrement, TestStatisticsConcurrentCollect, TestCloseMultipleTimes (plus TestStatisticsCollect / TestStatisticsDisabled for the changed function). With the fixes reverted, -race reports WARNING: DATA RACE at progressbar.go:69 and stat.go:46/102, and the log test panics.

Linux, Go 1.24, go test -count=1 -race ./...: all packages ok. gofmt, go vet, staticcheck, unparam, semgrep: clean.

CI note: the test matrix fails before running anything — bitnami/minio:2023.7.18 no longer exists on Docker Hub (fixed by #840). All build and qa jobs pass. The full suite was run locally on Linux as above.

🤖 Generated with Claude Code

- progressbar: IncrementCompletedObjects and IncrementTotalObjects used
  atomic.AddInt64 to write the counters but read them back with plain
  loads in fmt.Sprintf. Concurrent workers raced on the read. Use the
  value returned by AddInt64 and atomic.LoadInt64 for the other counter.

- log/stat: Statistics() iterated the internal maps without holding
  their mutexes, racing with Collect() from worker goroutines and
  risking a "concurrent map iteration and map write" fatal error. Hold
  both locks for the duration of the read.

- log: Close() closed the shared output channel unconditionally, so a
  second call panicked with "close of closed channel". Guard it with a
  sync.Once on the Logger so it is safe to call multiple times.

Adds a test for each: `go test -race` fails all three without the fix
and passes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant