fix: Read all items when a tombstone has no key - #443
Open
jsonbailey wants to merge 3 commits into
Open
jsonbailey wants to merge 3 commits into
jsonbailey wants to merge 3 commits into
Conversation
jsonbailey
added this pull request to stack #445
September 18, 2026 16:06
jsonbailey
marked this pull request as ready for review
September 18, 2026 16:14
The keyless-tombstone spec already requires the store to key its all-items map by the key each item is stored under, because such an item carries no key of its own. A record whose body key names a different item is a shape no SDK writes, so the spec only covered data that cannot occur.
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Symptom
With Consul or DynamoDB, a single deleted item whose stored JSON carries no
keybreaks the entire all-flags read.
all_flags_statereturns{"$flagsState":{},"$valid":false}, so every flag falls back to its default.Individual
variationcalls keep working — only the all-items read breaks.Root cause
Both stores rebuilt the all-items map from the
keyinside the record body,discarding the store key they had just read:
consul_impl.rb:76—items_out[item[:key].to_sym] = itemdynamodb_impl.rb:116—items_out[item_out[:key].to_sym] = item_outFor a keyless tombstone
item[:key]isnil, andnil.to_symraisesNoMethodError, which propagates out of the store and fails the whole read.Keyless tombstones are the norm, not a corruption. .NET, Java, Node (Redis
upsert path) and Haskell all write
{"version":N,"deleted":true}, and any SDKcan be pointed at a store another SDK wrote. The inner key is redundant anyway:
the store already addresses the record by key — the Consul KV path, the DynamoDB
sort key.
Redis was never affected because it already keys the map by the outer hash
field. Segments were unaffected on all stores because that path goes through
get(kind, key), which receives the key as a parameter.Fix
Key the output map by the store's own key, which both implementations already
have in hand:
result[:key], strippingthe
kind_key(kind)prefix. Entries outside that prefix are skipped ratherthan mis-keyed.
unmarshal_item, and skip an item that unmarshals tonil.No change was needed in the model layer: Ruby's
FeatureFlagandSegmentconstructors read
data[:key]without requiring it, so a keyless tombstonealready decoded. (This is where the equivalent Python fix also had to change the
model.)
Tests
spec/feature_store_spec_base.rbgains a shared example, run for everypersistent store under all four caching/prefix permutations: a keyless tombstone
written straight to the database alongside a valid item must not spoil the
all-items read, and must read back as absent. Each store tester gains a
write_raw_itemhelper to set up data in a shape the store itself never writes.Verified against the tombstone contract tests in
launchdarkly/sdk-test-harness#438, run locally with Redis, Consul and
DynamoDB-local:
persistent data store(v2,-enable-persistence-tests)The two pre-fix failures were exactly
consul/daemon mode/tombstones/flags/body has no keyanddynamodb/daemon mode/tombstones/flags/body has no key.New rspec examples reproduce the same split: 8 of 12 fail without the fix
(Consul and DynamoDB), Redis's 4 pass either way.
Full suite:
rspec spec1354 examples / 0 failures,rubocopclean.Related
Note
Overview
Fixes Consul and DynamoDB bulk reads (
get_all_internal) so the returned map is keyed by each record’s storage key (Consul KV path suffix, DynamoDB sort key) instead of the optionalkeyfield inside the JSON. Keyless deletion tombstones ({version, deleted}) from other SDKs no longer triggernil.to_symand abort the wholeall/ all-flags read.Adds shared persistent-store coverage that writes a raw keyless tombstone beside a valid item and asserts
allstill returns only live data, pluswrite_raw_itemon Consul/DynamoDB/Redis testers and model deserialization examples for tombstones with and without a key.Reviewed by Cursor Bugbot for commit 8010c8f. Bugbot is set up for automated code reviews on this repo. Configure here.