home

Redis' Watch Command: Making Sure A Process Doesn't Run When Already Running

Jun 05, 2012

I want to run a script only if it isn't already running. There are a lot of ways to solve this problem. Since the script already makes use of Redis, I figured I could use it to track if the process is running.

The first incorrect implementation is to use the setnx command, which only sets the value if it doesn't exist. Can you spot the problem?

redis = Redis.new
if redis.setnx("running") == false
  puts "already running"
  exit
end

begin
  # do stuff
ensure
  redis.del("running")
end

What happens if the computer or program crashes right after setnx sets our value? The key will be set but never deleted, and thus the process will never run again.

Instead, what if we used Redis' key expiration to make sure a key never lasted forever? Here's one flawed solution. Again, can you spot the problem?

redis = Redis.new
if redis.exists("running") == true
  puts "already running"
  exit
end
redis.setex("running", 300, true)

begin
  # do stuff
ensure
  redis.del("running")
end

If two instances execute at the same time, it's possible that they both check for the existence of the key before either has set the key. In this case, both will execute.

What we need to do is run the above in a transaction. However, the 2nd command in our transaction is dependent on the result from the first. Redis transactions execute all commands at once. We can't conditionally set our key. Not within a normal transaction anyways.

This is where Redis' watch command comes into play. First the code:

redis.watch("running")
if redis.exists("running") == true
  puts "already running"
  exit
end
redis.multi do
  redis.setex("running", 300, true)
end

begin
  # do stuff
ensure
  redis.del("running")
end

watch and multi work together. If the key that we are watching is modified by a different connection multi will fail. If two programs come in at the exact same time, they'll both watch the key running. In both cases, it won't exist. Since Redis is single threaded, only one will begin the transaction and set the key with an expiration. Once the first process completes its transaction, the 2nd process will begin its transaction and fail, since the watched key has been modified.

Essentially, if you ever need to run commands in a transaction where the output of one command impacts the other commands, you use watch (which can also be used to watch multiple keys).