Fix concurrent map write in DataReport by centralizing report locking - #401
Merged
Conversation
DataReport.Results was mutated from Backtest.worker with no synchronization, same bug class as the HTMLReport race fixed in #393. Rather than adding a mutex to DataReport too, centralize serialization of Report calls in Backtest.worker itself, document the concurrency contract on the Report interface, and drop HTMLReport's now-redundant mutex. Also guard the reportBuilders registry map in report_factory.go. Fixes #400. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #401 +/- ##
=======================================
Coverage 92.05% 92.06%
=======================================
Files 229 229
Lines 7237 7245 +8
=======================================
+ Hits 6662 6670 +8
Misses 488 488
Partials 87 87 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Explicit Lock()/Unlock() pairs leave reportMu locked forever if a Report call panics. Route every call through withReportLock, which locks and defers the unlock around a closure, so the mutex is always released even on panic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #400.
Problem
DataReport.Resultsis a plain map, mutated byAssetBegin/Writewith no synchronization, whileBacktest.workercalls those methods concurrently (one goroutine perBacktest.Workers) for different assets. This is the same bug class fixed forHTMLReportin #393, butDataReportnever got the equivalent fix, and it's the report typemcp/strategy.gobuilds its backtest results on.Fix
Instead of bolting a second mutex onto
DataReport(repeating the same fix a third time for the nextReportimplementation), this centralizes serialization inBacktest.worker:Backtestnow owns areportMu sync.Mutexand locks around every call intoreport.AssetBegin/report.Write/report.AssetEnd.Report's doc comment now states the contract: implementations don't need their own synchronization, sinceBacktestserializes calls across workers.HTMLReport's now-redundantmu sync.Mutexis removed, sinceBacktestalready guarantees serialized access.report_factory.go'sreportBuildersmap (flagged as a lower-stakes instance of the same smell) is now guarded by async.RWMutex.Testing
TestBacktestAllAssetsAndStrategiesWithDataReport, mirroring the existingHTMLReportWorkers = 16test but forDataReport— this is the exact case that was previously untested.go test -race ./backtest/...andgo test ./...pass.go build ./...,go vet ./...,gofmt -l backtest/all clean.🤖 Generated with Claude Code