Class: DistributedMutex

Inherits:
Object
  • Object
show all
Defined in:
lib/distributed_mutex.rb

Overview

Cross-process locking using Redis. Expiration happens when the current time is greater than the expire time

Constant Summary collapse

DEFAULT_VALIDITY =
60
CHECK_READONLY_ATTEMPTS =
5
LOCK_SCRIPT =
DiscourseRedis::EvalHelper.new <<~LUA
  local now = redis.call("time")[1]
  local expire_time = now + ARGV[1]
  local current_expire_time = redis.call("get", KEYS[1])

  if current_expire_time and tonumber(now) <= tonumber(current_expire_time) then
    return nil
  else
    local result = redis.call("setex", KEYS[1], ARGV[1] + 1, tostring(expire_time))
    return expire_time
  end
LUA
UNLOCK_SCRIPT =
DiscourseRedis::EvalHelper.new <<~LUA
  local current_expire_time = redis.call("get", KEYS[1])

  if current_expire_time == ARGV[1] then
    local result = redis.call("del", KEYS[1])
    return result ~= nil
  else
    return false
  end
LUA

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, redis: nil, validity: DEFAULT_VALIDITY) ⇒ DistributedMutex

Returns a new instance of DistributedMutex.



37
38
39
40
41
42
43
# File 'lib/distributed_mutex.rb', line 37

def initialize(key, redis: nil, validity: DEFAULT_VALIDITY)
  @key = key
  @using_global_redis = true if !redis
  @redis = redis || Discourse.redis
  @mutex = Mutex.new
  @validity = validity
end

Class Method Details

.synchronize(key, redis: nil, validity: DEFAULT_VALIDITY, &blk) ⇒ Object



33
34
35
# File 'lib/distributed_mutex.rb', line 33

def self.synchronize(key, redis: nil, validity: DEFAULT_VALIDITY, &blk)
  self.new(key, redis: redis, validity: validity).synchronize(&blk)
end

Instance Method Details

#synchronizeObject

NOTE wrapped in mutex to maintain its semantics



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/distributed_mutex.rb', line 46

def synchronize
  result = nil

  @mutex.synchronize do
    expire_time = get_lock

    begin
      result = yield
    ensure
      current_time = redis.time[0]
      if current_time > expire_time
        warn(
          "held for too long, expected max: #{@validity} secs, took an extra #{current_time - expire_time} secs",
        )
      end

      unlocked = UNLOCK_SCRIPT.eval(redis, [prefixed_key], [expire_time.to_s])
      if !unlocked && current_time <= expire_time
        warn("the redis key appears to have been tampered with before expiration")
      end
    end
  end

  result
end