0

Is it possible to run lua script on particular server endpoint using StackExchange.Redis server.Execute ( ExecuteAsync) call?

currently using following code:

        public async Task<bool> ExecuteScriptAsync(string scriptName, object[] keyArgs = null, object[] valueArgs = null)
        {
        List<byte[]> scriptHashes;
        _logger.Log(LogLevel.Information, $"Evaluating script: {scriptName}");
        if (!_scriptHashes.TryGetValue(scriptName, out scriptHashes))
        {
            throw new ArgumentException($"Requested script {scriptName} was not found");
        }

        RedisValue[] redisValues = valueArgs == null ? null : Array.ConvertAll(valueArgs, RedisValueHelper.ToRedisValue);
        RedisKey[] redisKeys = keyArgs == null
            ? null
            : Array.ConvertAll(keyArgs,
                source =>
                {
                    if (source == null)
                    {
                        return default;
                    }

                    return (RedisKey)source.ToString();
                });
        string script = GetScriptByName(scriptName);
        foreach (EndPoint endPoint in _connectionMultiplexer.GetEndPoints())
        {
            IServer server = _connectionMultiplexer.GetServer(endPoint);
            if (server != null && server.IsConnected && !server.IsReplica)
            {
                _logger.Log(LogLevel.Information, $"Executing script {scriptName} on endpoint: {server.EndPoint}");
                scriptTasks.Add(server.ExecuteAsync("EVAL", script, 0, redisKeys, redisValues));
            }
        }

        foreach (RedisResult result in await Task.WhenAll(scriptTasks))
        {
            _logger.Log(LogLevel.Information, $"Got script eval result:{result}");
        }
     return true;
    }

but it seems to be not working. It fails with TimeoutException.

StackExchange.Redis.RedisTimeoutException: Timeout awaiting response (outbound=0KiB, inbound=0KiB, 5360ms elapsed, timeout is 5000ms), command=UNKNOWN, next: EVAL, inst: 0, qu: 0, qs: 1, aw: False, rs: ReadAsync, ws: Idle, in: 0, in-pipe: 0, out-pipe: 0, serverEndpoint: <server_name>:6379, mc: 1/1/0, mgr: 10 of 10 available, clientName: CLIENT, IOCP: (Busy=0,Free=1000,Min=12,Max=1000), WORKER: (Busy=1,Free=32766,Min=12,Max=32767), v: 2.2.88.56325 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)

I want to execute the script on every primary node of redis cluster. The lua script remove keys by specified key prefix:

local keys = {};
local done = false;
local cursor = "0";
local numDeleted = 0;
local unlinkKey = function(key)
    local result;
    local num = redis.pcall("UNLINK", key);
    if type(num) == "table" then
        result = 0;
    else
        result = num;
    end
    return result;
end

repeat
    local result = redis.call("SCAN", cursor, "match", KEYS[1], "count", 10000)
    cursor = result[1];
    keys = result[2];
    for i, key in ipairs(keys) do
        numDeleted = numDeleted + unlinkKey(key);
    end
    if cursor == "0" then
        done = true;
    end
until done
return numDeleted;

db.ScriptEvaluate doesn't work properly in case of cluster as it doesn't execute it on all nodes but rather on some random one.

Thanks.

1 Answer 1

1

Your immediate issue

First off, let me point out that this line:

scriptTasks.Add(server.ExecuteAsync("EVAL", script, 0, redisKeys, redisValues));

Appears to be wrong for a couple of reasons, first off you ARE using a key, therefore, the key count is not 0, it should match the length of the key array you are passing into the script.

local result = redis.call("SCAN", cursor, "match", KEYS[1], "count", 10000)

the third argument should be the length of your redis keys array rather than 0. So that's a problem just for the execution of your script.

Secondly, you really ought to be passing in a single array of arguments, passing in a few ad-hoc arguments followed by an array isn't ideal, so for example:

this DOES hang (very similar to what you appear to be doing)

await server.ExecuteAsync("EVAL", "return KEYS[1]", 0, new RedisKey[]{new ("blah")});

whereas

var arguments = new List<object>();
arguments.Add("return KEYS[1]");
arguments.Add(1);
arguments.Add(new RedisKey("blah"));
    
var result = muxer.GetDatabase().Execute("EVAL", arguments.ToArray());

executes without issue

So something more like:

var arguments = new List<object>();
arguments.Add(script);
arguments.Add(redisKeys.Count());
arguments.AddRange(redisKeys);
arguments.AddRange(redisValues);
scriptTasks.Add(server.ExecuteAsync("EVAL", arguments));

will probably work better for you

Another heads up

I'd be remiss if I didn't point out to you that this is likely to be in itself a very long-running script.

Exhausting a SCAN's cursor inside of an EVAL is the equivalent of using the KEYS command, meaning that your script needs to scan over every single key on the shard (it will only return matches but it still needs to look at every key), so this script will be linear in time-complexity to the number of keys on your shard. This can result in a really long-running script. Long-running scripts are an anti-pattern in Redis, and exhausting a SCAN's cursor in a blocking fashion is itself an anti-pattern. since Redis is single-threaded, nothing else is going to be able to talk to your Redis shard while your script is executing.

You may want to consider a better way of ensuring the atomicity of the scan/delete operation (assuming that's what you are looking to do). This becomes a slightly more complicated data modeling question, but you should be able to build a combination of sets/locks that prevent you from locking down redis while you're doing these cleanups.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for your detailed answer. It really helped. I've made a slight change in script itself to use ARGV[1] instead of KEYS[1] and passed arguments as you've suggested. And it works perfectly now. As to the script itself and SCAN in EVAL. It's indeed quite heavy operation. But this call is used for maintenance purposes so it's ok.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.