SetCommand::IsOverwrite() returns true unconditionally, including for SET key val KEEPTTL. Overwrite commands are pruned in the log record and replayed with ignore_previous_version (the object is rebuilt from scratch rather than mutated in place), so on recovery / standby-forward the kept TTL is dropped and the key becomes immortal.
Evidence
include/redis_command.h:1379-1382 — SetCommand::IsOverwrite() returns true (no exception for KEEPTTL).
- Overwrite pruning + fresh-object rebuild on replay:
data_substrate/tx_service/include/cc/cc_entry.h (overwrite handling, ignore_previous_version).
The live execution path (CommitOn) reads the existing object to preserve its TTL when KEEPTTL is set, but a replayed/forwarded SET … KEEPTTL rebuilds the object with no prior version available → TTL lost.
Repro
SET k v EX 100; SET k v2 KEEPTTL; then crash + replay (or observe on a standby) → TTL k returns -1 (no expiry) instead of the remaining ~100s.
Fix: SET … KEEPTTL must not be treated as a full overwrite (it depends on prior state), or the replay/forward image must carry the resolved absolute TTL.
Found during a code audit (docs PR #492). IsOverwrite()==true verified; the replay-rebuild interaction is the documented overwrite-pruning behavior.
🤖 Found with Claude Code
SetCommand::IsOverwrite()returnstrueunconditionally, including forSET key val KEEPTTL. Overwrite commands are pruned in the log record and replayed withignore_previous_version(the object is rebuilt from scratch rather than mutated in place), so on recovery / standby-forward the kept TTL is dropped and the key becomes immortal.Evidence
include/redis_command.h:1379-1382—SetCommand::IsOverwrite()returnstrue(no exception forKEEPTTL).data_substrate/tx_service/include/cc/cc_entry.h(overwrite handling,ignore_previous_version).The live execution path (
CommitOn) reads the existing object to preserve its TTL whenKEEPTTLis set, but a replayed/forwardedSET … KEEPTTLrebuilds the object with no prior version available → TTL lost.Repro
SET k v EX 100;SET k v2 KEEPTTL; then crash + replay (or observe on a standby) →TTL kreturns-1(no expiry) instead of the remaining ~100s.Fix:
SET … KEEPTTLmust not be treated as a full overwrite (it depends on prior state), or the replay/forward image must carry the resolved absolute TTL.Found during a code audit (docs PR #492).
IsOverwrite()==trueverified; the replay-rebuild interaction is the documented overwrite-pruning behavior.🤖 Found with Claude Code