Skip to content

refactor(internal): extract shared file data loading into internal/filedata#405

Merged
kinyoklion merged 2 commits into
v7from
rlamb/sdk-2654/internal-filedata
Jul 8, 2026
Merged

refactor(internal): extract shared file data loading into internal/filedata#405
kinyoklion merged 2 commits into
v7from
rlamb/sdk-2654/internal-filedata

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Jul 7, 2026

Copy link
Copy Markdown
Member

ldfiledata and ldfiledatav2 each 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 new internal/filedata package 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 Merge produces 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 for Init, 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 says flag/segment in both packages; the two previously disagreed (features vs flag).

Two ldfiledatav2 fixes ride along:

  • closeReloaderCh was declared and passed to the reloader factory but never assigned, so Close() never stopped the file watcher goroutine, and the reload log line was unreachable. It is now created before the reloader starts, matching ldfiledata.
  • Fetch spun up a goroutine and two broadcaster listeners that were never removed (a leak on every call), with a select/default that 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 kind UNKNOWN and merge failures INVALID_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 ldfiledata and ldfiledatav2 move into new internal/filedata (ReadFile, AbsFilePaths, Merge, flag-value expansion). Each data source maps the neutral MergeResult into 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 say flag / segment.

ldfiledatav2 also fixes: closeReloaderCh is created when the reloader starts so Close stops the watcher; Fetch loads files synchronously via load() 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.

@kinyoklion kinyoklion requested a review from a team as a code owner July 7, 2026 21:12
@kinyoklion kinyoklion marked this pull request as draft July 7, 2026 21:27
…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).
@kinyoklion

Copy link
Copy Markdown
Member Author

Addressed the multi-agent review findings in 1c754e7:

  • Finding 1 (regression coverage): added TestCloseStopsReloader to both ldfiledata and ldfiledatav2 — a reloader factory captures the close channel and the test asserts Close() closes it. Reverting either lifecycle fix now fails a test.
  • Finding 2 (closeReloaderCh race): the channel is now created in the constructor rather than lazily in Sync/Start, so Close never races an assignment; an explicit flag gates the change-driven reload log instead of the channel's nil-ness. Applied to ldfiledata as well, which had the same (pre-existing) shape.
  • Finding 3 (version race): the counter is atomic.Int64 now that both Fetch and the reload path go through the same load code.
  • Finding 4 (ordering doc): MergeResult's doc now states the guarantee precisely — deterministic at document granularity only, within-document order unspecified, consumers must key by Key.

Findings 5–7 are informational/pre-existing and intentionally left alone. Fixes propagated through the stacked branches (#406, #407) and the rlamb/overrides integration branch; all file-source tests pass under -race, and the flag-overrides contract suite still passes end-to-end.

@kinyoklion kinyoklion marked this pull request as ready for review July 8, 2026 17:33

@kinyoklion kinyoklion left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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{}),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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{}),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@kinyoklion kinyoklion merged commit 5690e27 into v7 Jul 8, 2026
31 checks passed
@kinyoklion kinyoklion deleted the rlamb/sdk-2654/internal-filedata branch July 8, 2026 19:38
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.

2 participants