Repository navigation
RENAME/RENAMENX drop the key's TTL on AOF replay (replicas and recovery) #2208
Description
After RENAME or RENAMENX of a key that has a TTL, the primary keeps the TTL. The AOF entry for the destination doesn't carry it, though. Replicas, and a server recovering from the AOF, end up with the renamed key and no TTL, so the key never expires there. Strings, hashes and lists are all affected. A key that isn't renamed keeps its TTL. Reproduced on main at 0654725.
Environment: Windows 11, .NET SDK 10.0.401 (net10.0, Debug build). The cluster repro gave the same result in 3 runs on fresh servers.
Repro on a live replica
GarnetServer --cluster --aof --port 7401 --checkpointdir <primary>
GarnetServer --cluster --aof --port 7402 --checkpointdir <replica>
primary: CLUSTER ADDSLOTSRANGE 0 16383; CLUSTER MEET 127.0.0.1 7402
replica: CLUSTER REPLICATE <primary-id>; READONLY
primary:
SET {t}ctl v PX 900000
SET {t}s1 v PX 900000 ; RENAME {t}s1 {t}s_ren
HSET {t}h1 f v ; PEXPIRE {t}h1 900000 ; RENAME {t}h1 {t}h_ren
SET {t}s2 v PX 900000 ; RENAMENX {t}s2 {t}s_nx
RPUSH {t}l1 a ; PEXPIRE {t}l1 900000 ; RENAME {t}l1 {t}l_ren
SET {t}short v PX 3000 ; RENAME {t}short {t}short_ren
SET {t}short_ctl v PX 3000
| key | primary PTTL | replica PTTL |
|---|---|---|
{t}ctl (not renamed) |
899855 | 899595 |
{t}s_ren (RENAME, string) |
899859 | -1 |
{t}h_ren (RENAME, hash) |
899940 | -1 |
{t}s_nx (RENAMENX, string) |
899983 | -1 |
{t}l_ren (RENAME, list) |
899986 | -1 |
{t}short_ren (PX 3000, renamed) |
2988 | -1 |
{t}short_ctl (PX 3000, not renamed) |
2988 | 2727 |
After 5 seconds, {t}short_ren is gone on the primary (EXISTS returns 0) but still readable on the replica (EXISTS returns 1, GET returns v). {t}short_ctl has expired on both. The result was the same in 3 runs on fresh servers.
Repro via AOF recovery
--storage-tier --aof --aof-commit-freq -1: SET s1 v PX 900000, RENAME s1 s_ren, COMMITAOF, then restart with --recover. PTTL s_ren returns -1, while a control SET ctl v PX 900000 keeps its TTL. The same happens with RENAMENX, hashes and lists.
Cause
-
The destination write is logged by
WriteLogUpsert(libs/server/Storage/Functions/UnifiedStore/PrivateMethods.cs) in one of two ways:- Strings: as
UnifiedStoreStringUpsert, with the key, the value and the serializedUnifiedInput(cmd = RENAME,arg1 = RecordType). - Objects: as
UnifiedStoreObjectUpsert, which has no input at all.
Neither form carries the expiration.
- Strings: as
-
On replay:
UnifiedSessionFunctions.InitialWriter(UpsertMethods.cs) skipsarg1as an expiration for RENAME/RENAMENX ("RENAMEs use arg1 for RecordType, not expiration").- The object path (
AofProcessorUnifiedStoreObjectUpsert) upserts with no input.
So the TTL is lost when the record is written, and changing replay alone can't recover it from existing logs. The arg1-as-RecordType special case came in with #2080, the Vector Set RENAME replay fixes. I haven't checked whether TTLs survived replay before that change. #651/#652 fixed RENAME dropping the TTL in memory; this report is about the AOF record and its replay, where the TTL is never written.
Possible fix
Log the expiration for renames. For example, after the destination upsert, emit a PEXPIREAT RMW for the destination when the source has a TTL. That's the approach COPY uses for objects in #2168, and it needs no change to existing record formats. The alternative is to carry the expiration in the rename upsert record itself. That avoids the extra record, but it changes a persisted record format. Neither approach can repair logs that were already written, because they never contained the TTL. I'm happy to send a PR with a regression test in the AOF suite, plus a replica test if that's useful.
Activity