Skip to content

RENAME/RENAMENX drop the key's TTL on AOF replay (replicas and recovery) #2208

Open

Description

@darth-modder

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 serialized UnifiedInput (cmd = RENAME, arg1 = RecordType).
    • Objects: as UnifiedStoreObjectUpsert, which has no input at all.

    Neither form carries the expiration.

  • On replay:

    • UnifiedSessionFunctions.InitialWriter (UpsertMethods.cs) skips arg1 as an expiration for RENAME/RENAMENX ("RENAMEs use arg1 for RecordType, not expiration").
    • The object path (AofProcessor UnifiedStoreObjectUpsert) 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions