fix: Read every tombstone shape as deleted, not as a live item - #202
Draft
jsonbailey wants to merge 1 commit into
Draft
jsonbailey wants to merge 1 commit into
jsonbailey wants to merge 1 commit into
Conversation
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.
Summary
The persistent-store read path got every tombstone shape wrong, in two different directions. Against a store another SDK had written, a deleted flag either came back to life or took down the whole all-flags read.
"key":"$deleted"{"version":N,"key":"$deleted","deleted":true}{"version":N,"deleted":true}Err— poisons the entireall_flagsread{"key":"<real key>","version":N,"deleted":true}Err— sameTwo causes. The item parse ran first, and
Flaghas nodeletedfield and does not deny unknown fields, so a tombstone written as a complete flag object satisfied every required field and"deleted":truewas silently discarded. The fallback was then gated ontombstone.key == "$deleted", and the old tombstone struct requiredversion,key, anddeletedwith no serde defaults — so a keyless tombstone could not deserialize at all and a real-key tombstone failed the string guard.An
Erron one item fails the whole collection, becausepersistent_store_wrapper.rscollects intoResult<HashMap, _>. That behavior is intentional and unchanged: flags have prerequisite dependencies, so serving a partial view is worse than declaring the source untrustworthy. A valid tombstone from another SDK is not corrupt data, so the fix is to make it parse rather than to loosen the error handling.SerializedTombstoneis nowSerializeonly and still writes a complete tombstone; a new read-sideDeserializedTombstonedefaults both fields so every shape deserializes, and omitskeyentirely.SerializedItem::tombstone_version(), called by theFlagandSegmentconversions before the typed item parse. It replaces logic that was duplicated between the two."$deleted"carries no information.A single permissive struct used for both directions would also work, but it would make the writer as loose as the reader —
version: Option<u64>would permit emitting"version": nullinto a store. Keeping the write shape strict is the point of the split.Corrupt data still errors:
{},[1,2,3],"nope", and a flag missingonall fall through to the typed parse and propagate the real error.No other read path is affected. The streaming delete builds
StorageItem::Tombstonefrom a typed event and never goes throughSerializedItem.Testing
11 new tests, 6 of which fail without this change:
flag(),segment(), andall_flags()with caching off, so every read reaches the store. Before the fix,all_flagsreturned[]where it should have returned the one live flag.5 of the 11 are regression guards that pass before and after, which is the evidence the reordering is safe in both directions: a live flag, a flag and a segment carrying
"deleted": false, a corrupt body still reporting the item parse error, and a corrupt record still failing the whole all-flags read.Tests cover five shapes rather than the three above — the extra is a full object carrying the real key plus
deleted:true, which fails the same way as the$deletedvariant.cargo test: 424 unit passed, 19 doc-tests passed, 0 failed.cargo fmt --checkclean.Not in scope
The persistence contract tests cannot exercise any of this: this SDK declares no
persistent-data-store-*capability, so the harness suite skips it. Tracked in SDK-3187 along with thelaunchdarkly-server-sdk-redisversion bound that blocks it.