skip to Main Content

I’m trying to use the following Lua script using C# StackExchange library:

private const string LuaScriptToExecute = @"
local current
current = redis.call(""incr"", KEYS[1])
if current == 1 then
    redis.call(""expire"", KEYS[1], KEYS[2])
    return 1
else
    return current
end

Whenever i’m evaluating the script "as a string", it works properly:

var incrementValue = await Database.ScriptEvaluateAsync(LuaScriptToExecute,
  new RedisKey[] { key, ttlInSeconds });

If I understand correctly, each time I invoke the ScriptEvaluateAsync method, the script is transmitted to the redis server which is not very effective.

To overcome this, I tried using the "prepared script" approach, by running:

_setCounterWithExpiryScript = LuaScript.Prepare(LuaScriptToExecute);
...
...
var incrementValue = await Database.ScriptEvaluateAsync(_setCounterWithExpiryScript,
    new[] { key, ttlInSeconds });

Whenever I try to use this approach, I receive the following error:

ERR Error running script (call to f_7c891a96328dfc3aca83aa6fb9340674b54c4442): @user_script:3: @user_script: 3: Lua redis() command arguments must be strings or integers

What am I doing wrong?

What is the right approach in using "prepared" LuaScripts that receive dynamic parameters?

2

Answers


  1. Chosen as BEST ANSWER

    First of all, Jan's comment above is correct.

    The script line that updated the key's TTL should be redis.call(""expire"", KEYS[1], ARGV[1]).

    Regarding the issue itself, after searching for similar issues in RedisStackExchange's Github, I found that Lua scripts do not work really well in cluster mode.

    Fortunately, it seems that "loading the scripts" isn't really necessary. The ScriptEvaluateAsync method works properly in cluster mode and is sufficient (caching-wise). More details can be found in the following Github issue.

    So at the end, using ScriptEvaluateAsync without "preparing the script" did the job.

    As a side note about Jan's comment above that this script isn't needed and can be replaced with two C# calls, it is actually quite important since this operation should be atomic as it is a "Rate limiter" pattern.


  2. If I look in the documentation: no idea.

    If I look in the unit test on github it looks really easy.

    (by the way, is your ttlInSeconds really RedisKey and not RedisValue? You are accessing it thru KEYS[2] – shouldnt that be ARGV[1]? Anyway…)

    It looks like you should rewrite your script to use named parameters and not arguments:

    private const string LuaScriptToExecute = @"
    local current
    current = redis.call(""incr"", @myKey)
    if current == 1 then
        redis.call(""expire"", @myKey, @ttl)
        return 1
    else
        return current
    end";
    
    // We should load scripts to whole redis cluster. Even when we dont have any. 
    // In that case, there will be only one EndPoint, one iteration etc...
    _myScripts = _redisMultiplexer.GetEndPoints()
      .Select(endpoint => _redisMultiplexer.GetServer(endpoint))
      .Where(server => server != null)
      .Select(server => lua.Load(server))
      .ToArray();
    
    

    Then just execute it with anonymous class as parameter:

    
    for(var setCounterWithExpiryScript in _myScripts)
    {
      var incrementValue = await Database.ScriptEvaluateAsync(
        setCounterWithExpiryScript,
        new {
          myKey: (RedisKey)key, // or new RedisKey(key) or idk
          ttl: (RedisKey)ttlInSeconds 
        }
      )// .ConfigureAwait(false); // ? ;-)
    
      // when ttlInSeconds is value and not key, just dont cast it to RedisKey
      /*
      var incrementValue = await 
      Database.ScriptEvaluateAsync(
        setCounterWithExpiryScript,
        new { 
          myKey: (RedisKey)key,
          ttl: ttlInSeconds 
        }
      ).ConfigureAwait(false);*/
    }
    

    Warning:

    Please note that Redis is in full-stop mode when executing scripts. Your script looks super-easy (you sometimes save one trip to redis (when current != 1) so i have a feeling that this script will be counter productive in greater-then-trivial scale. Just do one or two calls from c# and dont bother with this script.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search