Skip to content

Set ttl to 0 or past timpstamp with lua causing invisible keys on slave to stay indefinitely #5206

@xyzyng

Description

@xyzyng

Normally setting TTL to 0 with EXPIRE or to a past timestamp with EXPIREAT on master would delete the key on both master and slave, but using lua script to do the same task will only delete the keys on master, but not slave.

Steps to reproduce,

  1. set some keys to master, both master and slave have some number of keys
for i in {0..100}; do redis-cli set key$i val$i > /dev/null done

MASTER
# Keyspace
db0:keys=101,expires=0,avg_ttl=0

SLAVE:
# Keyspace
db0:keys=101,expires=0,avg_ttl=0
  1. set TTL to 0 on all keys with lua, keys are deleted on master, but still stay on slave
for i in {0..100}; do redis-cli eval "redis.call('expire', 'key$i', 0)" 0 > /dev/null; done

MASTER: 
# Keyspace

SLAVE:
# Keyspace
db0:keys=101,expires=101,avg_ttl=0

When this happens, the keys on slave will stay indefinitely and cannot be deleted since the keys are removed on master already.

I checked the source code of 4.0.10, when EXPIRE or EXPIREAT is called without using LUA, in expireGenericCommand() when the client sets the TTL to 0 or a past timestamp on the key, redis on master will delete the key immediately, then rewrites the client command to DEL with rewriteClientCommandVector(), the DEL command is then sent to slave by propagate()

if (when <= mstime() && !server.loading && !server.masterhost) {
...
       int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db,key) :
                                                    dbSyncDelete(c->db,key);
        serverAssertWithInfo(c,key,deleted);
        server.dirty++;

        /* Replicate/AOF this as an explicit DEL or UNLINK. */
        aux = server.lazyfree_lazy_expire ? shared.unlink : shared.del;
        rewriteClientCommandVector(c,2,aux,key);

But if EXPIRE or EXPIREAT is called with LUA, e.g. eval "redis.call('expire', 'key', 0)" 0, redis on master creates a fake client lua_client to execute the actual EXPIRE command, "expire key 0", then rewriteClientCommandVector() rewrites DEL command to the fake client, not the real client. The command in real client is still EVAL. Later when propagate() is called, it sends the command EVAL of the real client to slave, the DEL in the fake client is not replicated to the slave, causing the keys to stay indefinitely on slave. I found this behavior in both 4.0.10 and 3.2.x

Could you check whether this is expected behavior or an issue ? Thanks.

Metadata

Metadata

Assignees

No one assigned

    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