Local variables in Redis Lua scripts


In Redis and Lua scripting I described getting started with the Lua scripting language for Redis. It is a great way to make sure that all the Redis actions that need to, can execute without interruption. In this example, I'll use the local statement, which allows you create and manipulate local data in the Lua script - like the var statement in JavaScript.

The Problem

As always, we start with a problem that needs a robust solution. In Ephemeral Exchange, it is possible to state an intention to update an item when you read it. This allows some processing based on its contents, potentially to update them. 

Something like this , which will do a read using exponential backoff till it gets a lock, do something with the data, then write back some updated results.
efx.read (item , key , {intention:"update", backoff:true})
  .then ((result)=> doSomething(result))
  .then ((result)=> efx.update (result.data.value , item, key , "post" , {intent:result.data.intent});
  
During this short period, everyone else is locked out from updating that item to maintain the integrity of the store. The lock is automatically deleted when either the update associated with the intention is executed, or the lock times outs. You can read more about the details of the API here.

But what happens if the reader decides not to do the update after all ? Well, certainly the lock will time out and just go away, but that means there's an unnecessary lock out for a little while on an item, so the API provides a release method so the lock can be released without going ahead with the planned updates.

Something like this would better. 
efx.read (item , key , {intention:"update", backoff:true})
  .then ((result)=> doSomething(result) )
  .then ((result)=> { 
       return result.data.value ? 
         efx.update (result.data.value , item , key , "post" , {intent:result.data.intent}) :
         efx.release (item , key , result.data.intent);
   });

But this .release method could present an opportunity for failure. The lock is an item keyed by the item id, which contains the intention id and some other stuff as the content. 
  1. You can't just blindly delete that item because if the lock timeout has happened in the meantime and your lock has expired, and someone else has taken out a new lock on that item, you'll be deleting his lock, not yours. 
  2. You could increase the scope of the intent key to include both the intent key and the content, but that would then require multiple operations when checking for a lock in the first place - which itself could lead to race conditions.
  3. You could read it first to check that the lock is still the one you think it is by checking the content, but in the period between checking and deleting, problem 1 might still happen.

The solution

Lua comes to the rescue again. Because Redis is single threaded, it means that a Lua script will do its entire thing without being interrupted. But how to get data into Lua, so the content can be checked as in problem 3 above ? 

Lua provides a local statement, which can be used along with a redis.get to populate a local variable. You can then do things with that data and make decisions based upon it. This Lua script using ioredis, solves my problem

First define the custom method
    // returns nil .. the thing didnt exist , -1 it existed but didnt match , 0 it failed to delete , 1 it deleted
    // use redis.remove_if_matches (keytomatch, contenttoexpect)
    redisIntent_.defineCommand('remove_if_matches', {
      numberOfKeys: 1,
      lua: `if redis.call("exists", KEYS[1]) == 1 then
              local data = redis.call ("GET", KEYS[1])
              if (data == ARGV[1]) then
                return redis.call ("DEL", KEYS[1])
              else
                return -1
              end
            else
              return nil
            end`
    });


Then use it as a custom method added to ioredis.
redisIntent_.remove_if_matches(key, content);

Walkthrough


The KEYS table has a single key in it - the key of the intent lock.
The ARGV table has a single value in it - the content to expect inside the intent item

call redis to check if the item exists
if redis.call("exists", KEYS[1]) == 1 then

assign the result of calling redis.get using that key to a local variable
             local data = redis.call ("GET", KEYS[1])

If the data matches the expected content, delete it otherwise return -1 to indicate it was found but the contents have changed
              if (data == ARGV[1]) then
                return redis.call ("DEL", KEYS[1])
              else
                return -1
              end

If the item doesn't exist return null (nil in Lua)
else
   return nil
end

And that's a wrap on another example of how you can use Lua scripting to preserve atomicity in Redis.




For more like this, see React, redux, redis, material-UI and firebase. Why not join our forum, follow the blog or follow me on twitter to ensure you get updates when they are available.
Comments