Skip to content

fix: Fail an override file load on a deleted entry, naming the file and the key - #493

Merged
kinyoklion merged 1 commit into
feat/overridesfrom
rlamb/overrides-ruby-reject-deleted
Nov 11, 2026
Merged

kinyoklion merged 1 commit into
feat/overridesfrom
rlamb/overrides-ruby-reject-deleted

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions (run on Ruby 3.2 only)

Related issues

  • Spec: launchdarkly/sdk-specs#288 (Requirements 1.1.1.1, 1.2.8.1 and 1.3.6.2: the override layer never holds a deleted item).
  • SDK-3249

This PR is stacked on #454 so that the PRs below it do not change. Its base is rlamb/overrides-ruby-contract-tests.

Describe the solution you've provided

Before:

  • The override sink already rejected a snapshot with a deleted flag or segment with ArgumentError, before the layer changed. Nothing logged the rejection when a custom source supplied the snapshot after it had started. The message used the internal kind namespace: the override features entry f is deleted.
  • The file override source failed the reload only through that sink check. The log named neither the file nor the kind as users know it: Unable to load flags: the override features entry f is deleted.

After:

  • The file override source checks each file as it reads it, before it supplies a snapshot. A file with a flag or segment entry marked as deleted fails the reload in the same way as a file that cannot be parsed: the previous overrides stay, the existing retry applies, and the failure is logged at Error level with the file and the key, for example [LDClient] FileOverrideSource: Unable to load flags: flag "f" in /path/overrides.json is marked as deleted; remove the entry to remove its override. The check runs through a new check_document option of the shared reloader (impl/file_data/reloader.rb), and only the override source sets it.
  • The sink still raises ArgumentError for a snapshot with a deleted flag or segment. It now also logs the rejection at Error level, with "flag" or "segment" and the key: [LDClient] Rejected an override snapshot, keeping the overrides in effect: override flag "f" is marked as deleted; leave the flag out of the snapshot to remove its override. The whole snapshot is rejected, the layer is unchanged, and no change notifications are sent.
  • deleted: false is an ordinary definition and still loads.
  • Public docs: FileData.override_source and OverrideSink#set_overrides each gain one sentence about entries marked as deleted.

The ordinary file data sources (v1 FileDataSourceImpl and FDv2 FileDataSourceV2) are unchanged on purpose. Rejecting deleted entries there would be a breaking change, which is planned for a later major version. Neither of them uses the shared impl/file_data engine. The new specs pin that both of them, and the reloader without a document check, still load entries marked as deleted.

Tests added or changed:

  • spec/impl/overrides/sink_spec.rb: a deleted flag, and a deleted segment given as a model object, each reject the whole snapshot. The other entries are not applied, the layer is unchanged, nothing is notified, and the Error log names the kind and the key. Entries with deleted: false are accepted. The existing test's message expectations moved into these tests.
  • spec/integrations/file_override_source_spec.rb: a deleted flag fails the initial load, and the log names the file and the key. On a running source, a deleted flag keeps the previous overrides, and the fix is applied. A deleted segment in a YAML file keeps every file's overrides, including a change to the other file, until the segment entry is fixed. deleted: false loads. With a client, the override is still served, the log names the file, and the sink's rejection never appears because the source fails the load first.
  • spec/impl/file_data/reloader_spec.rb: a document check that raises fails the load and keeps the last good result. The other files are still checked. Without a check, deleted entries are combined as before.
  • spec/integrations/file_data_source_v2_spec.rb, spec/integrations/file_data_source_spec.rb: pins that the data sources still load entries marked as deleted.

How I ran them (Ruby 3.2.3):

  • bundle exec rspec spec/impl/overrides spec/impl/file_data spec/integrations/file_override_source_spec.rb spec/integrations/file_data_source_v2_spec.rb spec/integrations/file_data_source_spec.rb: 312 examples, 0 failures.
  • bundle exec rspec spec (database store specs skip by default): 1681 examples, 1 failure. The failure is spec/launchdarkly-server-sdk_spec.rb:5, the Bundler.require autoload test. Its inline bundler gets Permission denied on /var/lib/gems/3.2.0/bundler.lock on this machine, so it is a local environment problem and unrelated to this change.
  • I ran the new specs against the parent commit with only the spec files copied. The 7 behavior tests fail there, and the 5 pins and deleted: false tests pass. Two mutants also fail the expected tests: removing the sink's log line, and skipping segments in the file check.
  • bundle exec rubocop on the changed files: no offenses.

Describe alternatives you've considered

  • Checking only in the sink: the sink cannot name the file. Under retries, the reloader logs repeats of the same failure at Debug, so the first Error line would still not say which file to fix.
  • Checking inside FileData.merge: merge does not know the file paths, and the ordinary data sources would share it once they move onto the engine. A per-document hook keeps the rule on the override path.
  • Logging every sink rejection, including an entry without a key: a file with an empty key ("": ...) reaches the sink, and the reloader retries every second. An Error line from the sink on each retry would repeat what the reloader already logs once. So only the deleted-item rejection, which the file source never reaches now, is logged by the sink.

Additional context

Separate follow-up, not fixed here: the v1 file data source has its own bug. LDClient.new raises NoMethodError when a file has an entry marked as deleted, because dependency_tracker.rb:60 checks deleted only on a hash. Line 70 then reads the prerequisites of a deleted model, which are nil. The same happens on main. The v1 pin in this PR writes to a data store without an update sink, so it does not touch that path.

Opened from termag


Note

Overview
Adds a check_document hook to FileData::Reloader so each parsed file can be validated before merge; a MergeError from the hook fails the reload like a parse error and keeps the last good snapshot.

The file override source wires this to reject_deleted_entries, which rejects files containing flag or segment entries with deleted set (other than false/null), logging errors that name the file path and key. Override sink still rejects full snapshots with deleted items but now uses user-facing "flag"/"segment" wording and logs an ERROR when rejecting, so custom sources get visibility too.

Public docs for FileData.override_source and OverrideSink#set_overrides note that deleted entries fail loads / are rejected. v1 and FDv2 file data sources are unchanged and still accept deleted entries (pinned by new specs).

Reviewed by Cursor Bugbot for commit cbf2a1d. Bugbot is set up for automated code reviews on this repo. Configure here.

@kinyoklion

Copy link
Copy Markdown
Member Author

bugbot review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 26ff32a. Configure here.

Base automatically changed from rlamb/overrides-ruby-contract-tests to feat/overrides November 10, 2026 23:35
…nd the key

An override defines a flag or segment, and an override is removed by
removing its entry or deleting its file, so an entry marked as deleted is
a mistake in the file. The file override source now fails the reload of a
file that contains a flag or segment entry marked as deleted, in the same
way as a file that cannot be parsed: the overrides in effect stay, the
failure is logged with the file and the key, and the retry applies. Before,
the reload failed only through the sink's check, with a message that named
neither the file nor the kind as users know it. The check runs through a
new check_document option of the shared reloader, which only the override
source sets.

The sink still rejects a snapshot with a deleted flag or segment with
ArgumentError, and now also logs the rejection at Error level, naming
"flag" or "segment" and the key, so a custom source that supplies a
snapshot after it has started cannot drop the rejection silently.

The file data sources are unchanged: specs pin that they, and the
reloader without a document check, still load entries marked as deleted.
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-ruby-reject-deleted branch from 26ff32a to cbf2a1d Compare November 10, 2026 23:36
@kinyoklion
kinyoklion merged commit 0c9003b into feat/overrides Nov 11, 2026
10 checks passed
@kinyoklion
kinyoklion deleted the rlamb/overrides-ruby-reject-deleted branch November 11, 2026 00:05
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