refactor(internal): extract shared file data loading into internal/filedata#405
Conversation
…ning Creates the reloader close channel up front so Close never races a concurrent Sync/Start assigning it (an explicit flag now gates the change-driven reload log instead); makes the change-set version counter atomic since loads can run concurrently from Fetch and the reloader; adds regression tests proving Close closes the channel given to the reloader; and documents MergeResult's ordering guarantee precisely (deterministic at document granularity only).
|
Addressed the multi-agent review findings in 1c754e7:
Findings 5–7 are informational/pre-existing and intentionally left alone. Fixes propagated through the stacked branches (#406, #407) and the |
kinyoklion
left a comment
There was a problem hiding this comment.
Inline notes on the behavior fixes in this PR (vs. the original code).
| duplicateKeysHandling: duplicateKeysHandling, | ||
| reloaderFactory: reloaderFactory, | ||
| loggers: context.GetLogging().Loggers, | ||
| closeReloaderCh: make(chan struct{}), |
There was a problem hiding this comment.
Fix: this channel was declared and passed to the reloader factory but never assigned, so the factory always received nil and Close() never stopped the watcher goroutine. Creating it up front (rather than lazily in Sync) also means Close cannot race an assignment.
| // be modified. | ||
| func (fs *fileDataSource) reload() { | ||
| if fs.closeReloaderCh != nil { | ||
| if fs.reloaderStarted { |
There was a problem hiding this comment.
Fix: this log was previously gated on closeReloaderCh != nil, which was never true because the channel was never assigned — the line was unreachable. Now gated on an explicit flag so the initial load still does not log it.
| @@ -136,233 +134,99 @@ func (fs *fileDataSource) Sync(ds subsystems.DataSelector) <-chan subsystems.Dat | |||
| } | |||
|
|
|||
| func (fs *fileDataSource) Fetch(ds subsystems.DataSelector, ctx context.Context) (*subsystems.Basis, bool, error) { | |||
There was a problem hiding this comment.
Fix: Fetch previously spawned a goroutine and added two broadcaster listeners it never removed (a leak on every call), and its select/default could return an empty no-changes basis instead of the file data, marking an initializer initialized with no data. It now reads and merges synchronously and returns the change set or an error.
| version int | ||
| // moment will not report a selector. It is atomic because loads can happen | ||
| // concurrently from Fetch and from the reloader. | ||
| version atomic.Int64 |
There was a problem hiding this comment.
Fix: was a plain int incremented from both Fetch (caller goroutine) and the reload path — a data race now that both share one load routine. Atomic preserves the Payload.Target progression.
| duplicateKeysHandling: duplicateKeysHandling, | ||
| reloaderFactory: reloaderFactory, | ||
| loggers: context.GetLogging().Loggers, | ||
| closeReloaderCh: make(chan struct{}), |
There was a problem hiding this comment.
Hardening, same shape as the v2 fix: previously assigned in Start, which Close could race. No observed bug here (Start/Close are sequential in practice), but the channel is now created up front for the same reason.
ldfiledataandldfiledatav2each carried their own copies of the file reading, JSON/YAML detection, flag-value expansion, absolute-path resolution, and multi-file merge logic. This extracts one implementation into a newinternal/filedatapackage and ports both data sources onto it. A file-based override source (OVERRIDE spec) is about to become a third consumer of the same logic.The shared
Mergeproduces a consumer-neutral result (ordered flag and segment item lists); each data source adapts it to its own output in a few lines — v1 into store collections forInit, v2 into a full-transfer change set. Merge order is now deterministic for both (v1 previously emitted map-iteration order, which nothing depended on). The duplicate-key error now saysflag/segmentin both packages; the two previously disagreed (featuresvsflag).Two ldfiledatav2 fixes ride along:
closeReloaderChwas declared and passed to the reloader factory but never assigned, soClose()never stopped the file watcher goroutine, and the reload log line was unreachable. It is now created before the reloader starts, matching ldfiledata.Fetchspun up a goroutine and two broadcaster listeners that were never removed (a leak on every call), with aselect/defaultthat could silently return an empty no-changes basis instead of the file data. It now loads the files synchronously and returns the change set or an error directly. Read/parse failures still report error kindUNKNOWNand merge failuresINVALID_DATA, as before.No public API changes. All existing ldfiledata/ldfiledatav2/ldfilewatch tests pass unchanged; the shared package has its own unit tests.
SDK-2654
Note
Medium Risk
Touches core file-based flag loading for v1 and v2 data sources and changes Fetch/reload behavior in v2, though behavior is intended to match prior semantics with bug fixes.
Overview
Duplicates file read/parse/merge logic from
ldfiledataandldfiledatav2move into newinternal/filedata(ReadFile,AbsFilePaths,Merge, flag-value expansion). Each data source maps the neutralMergeResultinto its own store init or full-transfer change set. Merge now keeps first-seen document order (v1 previously used map iteration); duplicate-key errors consistently sayflag/segment.ldfiledatav2also fixes:closeReloaderChis created when the reloader starts soClosestops the watcher;Fetchloads files synchronously viaload()instead of leaking broadcaster listeners and sometimes returning an empty basis.Reviewed by Cursor Bugbot for commit 5433c04. Bugbot is set up for automated code reviews on this repo. Configure here.