Passing a variable number of arguments to a Redis Lua script - redis

I am using EVAL to pass several arguments to my Lua script. However, the last argument is optional, it may or it may be not passed to EVAL.
How can I check in a Redis Lua script whether an argument is there or not? For example, if ARGV[3] exists or not.

if ARGV[3] then
-- user pass in ARGV[3]
else
-- No ARGV[3]
end

redis.call('DEL', KEYS[1])
local members = {}
for i = 0, tonumber(ARGV[1]), 1 do
members[i] = ARGV[1+i]
end
redis.call('SADD', KEYS[1], unpack(members))
return 1

Related

How to pass `keys_and_args` to redis-py' `eval` function, and on to lua script

I wish to pass a number of keys and values from python to a lua script, via redis's eval function which is documented as:
eval(script, numkeys, *keys_and_args)
Execute the Lua script, specifying the numkeys the script will touch and the key names and argument values in keys_and_args. Returns the result of the script.
In practice, use the object returned by register_script. This function exists purely for Redis API completion.
I am following this answer as a starting point. That script increment the scores of all values in the sorted set specified by 1. As I wish to specify the values to update (key names) and the increment count for each (argument values) my script looks like this:
-- some logging
local loglist = "lualog"
redis.pcall("DEL", loglist)
local function logit(msg)
redis.pcall("RPUSH", loglist, msg)
end
logit("started")
-- count & log the keys provided
local countofkeys = table.getn(KEYS)
logit(countofkeys)
-- loop through each key and increment
for n = 1, countofkeys do
redis.call("zincrby", "test_set", ARGV[n], KEYS[n])
end
I can run this from the command line with:
$ redis-cli --eval script.lua apple orange , 1 1
Then in Python confirm that the values have incremented:
>>> r.zrange('test_set', start = 0, end = -1, withscores=True)
[(b'apple', 1.0), (b'orange', 1.0)]
However I don't know how to run this using eval:
>>> c.eval(script,1,{'orange':1,'apple':1})
redis.exceptions.DataError: Invalid input of type: 'dict'. Convert to a byte, string or number first.
>>> c.eval(script,2,'apple orange , 1 1')
redis.exceptions.ResponseError: Number of keys can't be greater than number of args
>>> c.eval(script,1,'apple orange , 1 1')
redis.exceptions.ResponseError: Error running script (call to f_aaecafd58b474f08bafa5d4fefe9db98a58b4084): #user_script:21:
#user_script: 21: Lua redis() command arguments must be strings or integers
The documentation isn't too clear on what keys_and_args should look like. Also at the comand line numkeys isn't actually required by the looks of things. Does anyone know what this should look like?
Bonus question: How to avoid hard coding "test_set" into the lua script.
*keys_and_args should be an iterable (e.g. a list) - the use of an asterisk as a prefix to the argument's name is the Pythonic way of saying that.
Bonus tip: look into redis-py Script helper.
Bonus answer: Any key names touched by the script need to be provided via the KEYS table. Your script is doing it all wrong - read the documentation about EVAL.
Also at the comand line numkeys isn't actually required by the looks of things
This is only with the cli when used in that fashion - the comma (',') delimits between key names and arguments.
The first argument in the documentation is numkeys and the rest of the arguments are termed as *keys_and_args. The way to provide arguments is similar to argc and argv. So you would do something like this:
redis.eval(lua_script, 1, "BUCKET_SIZE", total_bucket_size, refill_size)
The numkeys will specify that the first argument should be considered as a key and the following as args. The 1 specifies the number of keys present in your *keys_and_args array.
eval() receives 3 parameters or to make it easy to understand "4" parameters
script: string
number_of_keys: integer
key_list: unpacked iterable key objects, for example, *{1, 2, 3} => 1, 2, 3
We have 3 keys, so number_of_keys should be 3
argument_list: unpacked iterable argument objects, for example, *{'one', 'two', 'three'} => 'one', 'two', 'three'
If we want to access 2nd element of the key list, use KEYS[2] in the LUA script.
If we want to access the 1st element of the argument list, user ARGV[1].
To return a list of KEYS[2] and ARGV[1]:
cache.eval('return {KEYS[2], ARGV[1]}', 3, 1, 2, 3, 'one', 'two', 'three')
So, now if we are back with 3 parameters, the last one should be *keys_and_arguments: unpacked iterable keys and arguments.

'(error) ERR Error running script (call to ...)' when using --eval for LUA script

I am getting an error message when running this command:
redis-cli --eval myscript.lua myzset 3 "one"
Error message:
(error) ERR Error running script (call to f_9c623c243d74e75a4fe64de7a6826b47f8d7
d400): #user_script:1: #user_script: 1: Lua redis() command arguments must be st
rings or integers
Lua script:
local ans = redis.call('ZINCRBY', KEYS[1], ARGV[1], ARGV[2])
if tonumber(ans) <= 0 then
redis.call('ZREM', KEYS[1], ARGV[2])
end
return (tonumber(ans) < 1 and '-1' or ans)
The goal is to remove automatically Zero or Negative value using ZINCRBY.
I am using:
Windows 7 64bit
Redis Version: 3.2.100
Your help will be appreciated.
The problem you're experiencing due to the fact that you're not separating the KEYS and ARGS - use a comma in the command line to do so. Also, note that since the comma is the delimiter, you'll need to use an extra space in order to ensure that it isn't regarded as a part of the key.
Like so:
redis-cli --eval myscript.lua myzset , 3 "one"

Lua script on Redis Cluster

I have a Redis Cluster and I would like to execute a LUA script on a target node.
When I do that I receive an error from Redis Cluster that say that keys must be on the same SLOT, but the script executes two commands on only 1 key.
if redis.call('HEXISTS', 'TEST', KEYS[1]) == 1
then
redis.call('HSET', 'TEST', KEYS[1], ARGV[1])
return 1
end
return 0
I tried to use Hash Tag Key, but it doesn't work.
You are getting this error because you're trying to access a Redis key named TEST, but don't make it known to Redis or your Redis client by listing it in the EVAL command.
Redis Lua scripts take two argument lists, one is a general purpose (ARGV[]) and the other is a strict list of the keys the script is going to access (KEYS[]).
You may have swapped the key name and field names in the HEXISTS call, i.e. you may need to use:
if redis.call('HEXISTS', KEYS[1], 'TEST') == 1
then
redis.call('HSET', KEYS[1], 'TEST', ARGV[1])
return 1
end
return 0
This should work as long as the key name is properly passed on to the EVAL command.

How to SPOP from a list and SADD to another inside of lua scripting

I want to do the following in redis LUA scripting:
SPOP 1+ items from "source" set
SADD elements from #1 into "target" set
I'm using redis 5.
I have the below lua, but this is just for a single element:
local source = KEYS[1]
local target = KEYS[2]
local num = KEYS[3]
local ele = redis.call("SPOP", "source")
redis.call("SADD", target, ele)
return "OK"
How can I update the above with:
handle 1+ elements using the passed in param KEY[3]
ensure if 0 elements were returned from POP, it doesn't try and add to target set.
In Redis v5 and above this should "just work" due to the move to script effects replication as a default.
In v4 you'll have to execute redis.replicate_commands() before any random command in the script.
EDIT: per your edits and comment, here's an example:
-- uncomment the next line for Redis v4
-- redis.replicate_commands()
local source = KEYS[1]
local target = KEYS[2]
local num = ARGV[1]
local elems = redis.call("SPOP", source, num)
if #elems > 0 then
redis.call("SADD", target, unpack(elems))
end
return redis.status_reply("OK")

How to get all keys/values from redis in order to insert them into SQL db?

I have a lot of analytics data that I'm adding to redis. I plan on incrementally moving the data out of redis and into my database.
I know I can use KEYS [the_key]:* to get all keys that match. For example, I can do that to get the following:
127.0.0.1:6379> KEYS c_Track:*
1) "c_Track:6c93a5c1-77e9-4c4a-9232-bf182713a02e"
2) "c_Track:2c9d99c2-af37-4de9-ac64-b48f339e97a9"
3) "c_Track:9e7fd190-86d9-4b4a-9a70-7bf4c7768eef"
4) "c_Track:7f2d2e98-7440-4fd7-a80a-2af309ab15a4"
Is there a recommended way to get these values easily? I can get the keys, but how can I get all the values as well? I can loop through the keys to get the values, but is there some one-shot method for doing this?
Also I know I shouldn't use keys, but this is just an example. Thanks
Thanks
Also I know I shouldn't use keys
So don't. Use SCAN instead.
is there some one-shot method for doing this?
No, not as a core Redis command, but given the need this is fairly simple to achieve with a server-side Lua script. For example, assuming that your values are strings, you could do something like the following:
local cursor = tonumber(ARGV[1])
local pattern = ARGV[2]
local scan = redis.call('SCAN', cursor, 'MATCH', pattern)
for i, v in ipairs(scan[2]) do
local val = redis.call('GET', v)
scan[2][i] = { v, val }
end
return scan
Assuming that this script is saved under "scan.lua", you can run it as follows:
$ redis-cli SET foo bar
OK
$ redis-cli SET baz qaz
OK
$ redis-cli --eval scan.lua , 0 "*"
1) "0"
2) 1) 1) "baz"
2) "qaz"
2) 1) "foo"
2) "bar"
To scan your entire keyspace, call the script with the returned cursor until it returns 0.
Notes:
1) If your keys are of different types, you should change the script accordingly (e.g. https://github.com/itamarhaber/redis-lua-scripts/blob/master/scanfetch.lua).
2) While this script goes against the common recommendation of generating key names inside a script, it is still safe to run as SCAN returns keys that are in the server's keyspace (whether single-instance or clustered).