Skip to content

Invisible Key (only visible from LUA) #779

Open
FGRibreau opened this Issue Nov 20, 2012 · 2 comments

2 participants

@FGRibreau

I've got the following LUA zunion script (soon available on Redsmin).

(please note that I added debug calls)

-- zunion numkeys key [key ..] [REVRANGE start stop [EXPIRE expire]]
-- extended zunion - MIT LICENSE
-- v1.1 - Only save what was requested
-- https://redsmin.com http://brin.gr

local expire = 5*60 -- expire after 5 minutes
local lgth   = #KEYS
local start  = 0
local stop   = -1

-- debug
redis.call('zadd', 'debug', 0, "start");

if(#ARGV > 0) then
  if(ARGV[1] == 'REVRANGE') then
    start = tonumber(ARGV[2]) or start
    stop  = tonumber(ARGV[3]) or stop
  end

  if(#ARGV > 3 and ARGV[4] == 'EXPIRE') then
    expire = tonumber(ARGV[5]) or expire
  end
end

-- debug
redis.call('zadd', 'debug', 1, 'start ' .. start .. ' stop '.. stop .. ' expire ' .. expire);

-- Define the key name
local name = 'zunion:' .. start .. ':' .. stop .. ':' ..redis.sha1hex(table.concat(KEYS))

local function getResults()
 local range =  redis.call('zrevrange', name, start, stop, 'WITHSCORES')
 return range
end


-- debug
local r = redis.call('exists', name)
redis.call('zadd', 'debug', 2, 'exists ' .. name .. ' = '.. r);

-- If the key already exist returns it
if redis.call('exists', name) == 1 then
  return getResults()
end

-- debug
redis.call('zadd', 'debug', 3, 'zunionstore ' .. name .. ' '.. lgth);

-- do a zunionstore
table.insert(KEYS, 1, 'zunionstore')
table.insert(KEYS, 2, name)
table.insert(KEYS, 3, lgth)

redis.call(unpack(KEYS));

-- truncate, only keep from 'highest score' to stop
if stop > -1 then
  -- debug
  redis.call('zadd', 'debug', 4, 'zremrangebyrank ' .. name .. ' '.. 0 .. ' ' .. -stop-2);

  redis.call('zremrangebyrank', name, 0, -stop-2);
end

-- debug
redis.call('zadd', 'debug', 5, 'expire ' .. name .. ' '.. expire);

-- Add an expire
redis.call('expire', name, expire);

-- Return the key
return getResults()

In our case reads are always done on the slaves and because zunion has an internal cache, writes on the slave are allowed as well:

redis_version:2.6.4
role:slave
os:Linux 2.6.32-5-amd64 x86_64
slave_read_only:0
db0:keys=10566,expires=578

I restarted my Redis client to add the debug informations in LUA script and as you can see the zunion:0:4:488003f1d62e2765f033db26187f093a1b68c073 key exists from the LUA script point of view:
screenshots

However if I try to access them from redis-cli (or in other words from the redis-protocol) they can't be listed even if exists and del returns a valid value.

redis 127.0.0.1:6379> exists zunion:0:4:488003f1d62e2765f033db26187f093a1b68c073 
(integer) 1
redis 127.0.0.1:6379> ttl zunion:0:4:488003f1d62e2765f033db26187f093a1b68c073 
(integer) -1
redis 127.0.0.1:6379> keys zunion:*
(empty list or set)
redis 127.0.0.1:6379> del zunion:0:4:488003f1d62e2765f033db26187f093a1b68c073 
(integer) 1
redis 127.0.0.1:6379> 

Note: ttl returns -1 but it should return 300 or the specified expire value
Note: keys zunion:* should at least display zunion:0:4:488003f1d62e2765f033db26187f093a1b68c073

[Update]

-- [Removed] I proved that my hypothesis was wrong --

[Update2]

I edited the script to directly remove the key afterwards and the script still works . So the error seems to be with the expire & exists.

local expire = 5*60 -- expire after 5 minutes
local lgth   = #KEYS
local start  = 0
local stop   = -1

if(#ARGV > 0) then
  if(ARGV[1] == 'REVRANGE') then
    start = tonumber(ARGV[2]) or start
    stop  = tonumber(ARGV[3]) or stop
  end

  if(#ARGV > 3 and ARGV[4] == 'EXPIRE') then
    expire = tonumber(ARGV[5]) or expire
  end
end

-- Define the key name
local name = 'zunion:' .. start .. ':' .. stop .. ':' ..redis.sha1hex(table.concat(KEYS))

local function getResults()
  -- return the result from the biggest to the lowest
  local range =  redis.call('zrevrange', name, start, stop, 'WITHSCORES')
  redis.call('del', name)
  return range
end

-- If the key already exist returns it
if redis.call('exists', name) == 1 then
  return getResults()
end

-- do a zunionstore
table.insert(KEYS, 1, 'zunionstore')
table.insert(KEYS, 2, name)
table.insert(KEYS, 3, lgth)

redis.call(unpack(KEYS));

-- truncate, only keep from 'highest score' to stop
if stop > -1 then
  redis.call('zremrangebyrank', name, 0, -stop-2);
end

-- Return the key
return getResults()

[Update 3]

I updated the first version of the LUA script and added a redis.call('expire', name, expire) just after the local range = redis.call('zrevrange', name, start, stop, 'WITHSCORES') and now keys zunion:* finally returns all the matching keys with the right TTL.

redis 127.0.0.1:6379> keys zunion:*
1) "zunion:0:7:95999f061a9ebf9240a2b3eb3e930896ce8af528"
2) "zunion:0:100:bff6bfca8f2e7ff25d65ac9b978fbdad92487b10"
3) "zunion:0:4:f63ceb546e1900bde8a1bbfe9eb95192ed2d6417"
4) "zunion:0:4:54e538f1126a9b11a9e5a201babcd28479460fc0"
5) "zunion:0:4:0877e846d1282f3f25e3e6e0dd9908dee7f71554"
6) "zunion:0:-1:368a524d10aa654e4809ef25fb20ab481a5db4b3"

keys works !

Am I missing something with expire behaviour ? Or is it really a Redis/Lua bug ?

@antirez
Owner
antirez commented Dec 3, 2012

This is odd, but is the currently expected behavior...

Basically what happens is that a slave is not authorized to expire a key, since the key eviction is driven by the master that sends explicit DEL commands to the slave.

However KEYS avoid showing you keys already expired.

At the same time the other commands are able to access the keys if the keys are used directly.

You may ask why the master is not sending the DEL command to the slave. The reason is that when the key is not accessed in the master side by a client, only the background incremental collection performed by Redis can discover the key, and it takes time when there are many keys to sample from.

There is some idea to improve this by improving the expiration algorithm.

For instance an approach could be that instead of using random sampling we store the expire information on a skiplist instead of using an hashable (db->expires), so that we can simply traverse the skiplist to expire things older than the current time. At the same time the skiplist can be used to add a new expire in O(1) time every time the expire has a time greater than any other expire currently set on Redis. This is very common actually, because many applications use SETEX with a fixed TTL.

Taking this issue open until a new issue centered on this problem is open and properly documented.

@FGRibreau

First thanks for this detailed response!

However, few more points:

  • the LUA script run on the slave, it creates its own "zunion:" keys (they are not present in the master keyspace, its the desired effect because these keys are only used for cache).
  • KEYS was issued on the slave, not the master and I was able to see the TTL for each keys (as demonstrated in the last screenshot)

Therefore I don't understand how this is even possible regarding the fact that slave are not authorized to expire keys.

@JackieXie168 JackieXie168 pushed a commit that referenced this issue Sep 16, 2014
Ivan Tarasov Add information about machines status to address #779 03c0340
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.