Skip to content

fix: Read every tombstone shape as deleted, not as a live item - #202

Draft
jsonbailey wants to merge 1 commit into
mainfrom
jb/sdk-2997/tombstone-read-path
Draft

jsonbailey wants to merge 1 commit into
mainfrom
jb/sdk-2997/tombstone-read-path

Conversation

@jsonbailey

Copy link
Copy Markdown

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.

Writer Shape Result before this change
Go / Relay Proxy full object, "key":"$deleted" parsed as a live flag — deleted flag resurrected, no error
this SDK {"version":N,"key":"$deleted","deleted":true} correct
.NET, Java, Node (Redis upsert), Haskell {"version":N,"deleted":true} Err — poisons the entire all_flags read
Python, Ruby, C++, Erlang, Node (init) {"key":"<real key>","version":N,"deleted":true} Err — same

Two causes. The item parse ran first, and Flag has no deleted field and does not deny unknown fields, so a tombstone written as a complete flag object satisfied every required field and "deleted":true was silently discarded. The fallback was then gated on tombstone.key == "$deleted", and the old tombstone struct required version, key, and deleted with no serde defaults — so a keyless tombstone could not deserialize at all and a real-key tombstone failed the string guard.

An Err on one item fails the whole collection, because persistent_store_wrapper.rs collects into Result<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.

  • Split the type by direction. SerializedTombstone is now Serialize only and still writes a complete tombstone; a new read-side DeserializedTombstone defaults both fields so every shape deserializes, and omits key entirely.
  • Added SerializedItem::tombstone_version(), called by the Flag and Segment conversions before the typed item parse. It replaces logic that was duplicated between the two.
  • The inner key is now ignored outright. A store addresses the record by key already, so "$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": null into a store. Keeping the write shape strict is the point of the split.

Corrupt data still errors: {}, [1,2,3], "nope", and a flag missing on all fall through to the typed parse and propagate the real error.

No other read path is affected. The streaming delete builds StorageItem::Tombstone from a typed event and never goes through SerializedItem.

Testing

11 new tests, 6 of which fail without this change:

  • conversion layer — every tombstone shape for flags and segments, and a tombstone with no version falling back to the stored version;
  • store entry points — flag(), segment(), and all_flags() with caching off, so every read reaches the store. Before the fix, all_flags returned [] 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 $deleted variant.

cargo test: 424 unit passed, 19 doc-tests passed, 0 failed. cargo fmt --check clean.

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 the launchdarkly-server-sdk-redis version bound that blocks it.

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.

1 participant