Class: Redlock

Inherits:
Object
  • Object
show all
Defined in:
lib/redlock.rb,
lib/redlock/version.rb

Constant Summary collapse

DEFAULT_RETRY_COUNT =
3
DEFAULT_RETRY_DELAY =
200
CLOCK_DRIFT_FACTOR =
0.01
UNLOCK_SCRIPT =
'
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end'
VERSION =
'0.0.4'

Instance Method Summary collapse

Constructor Details

#initialize(*server_urls) ⇒ Redlock

Returns a new instance of Redlock.



15
16
17
18
19
20
21
22
23
24
# File 'lib/redlock.rb', line 15

def initialize(*server_urls)
  @servers = []
  server_urls.each{|url|
    @servers << Redis.new(:url => url)
  }
  @quorum = server_urls.length / 2 + 1
  @retry_count = DEFAULT_RETRY_COUNT
  @retry_delay = DEFAULT_RETRY_DELAY
  load_script
end

Instance Method Details

#get_unique_lock_idObject



57
58
59
60
61
62
63
64
# File 'lib/redlock.rb', line 57

def get_unique_lock_id
  val = ""
  bytes = SecureRandom.random_bytes(20)
  bytes.each_byte{|b|
    val << b.to_s(32)
  }
  val 
end

#load_scriptObject



26
27
28
29
30
# File 'lib/redlock.rb', line 26

def load_script
  @servers.each do |server|
    @unlock_sha = server.script(:load, UNLOCK_SCRIPT)
  end
end

#lock(resource, ttl, val = nil) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/redlock.rb', line 66

def lock(resource,ttl,val=nil)
  val = get_unique_lock_id if val.nil?

  if @testing_mode == :bypass
    return {
      validity: ttl,
      resource: resource,
      val: val
    }
  elsif @testing_mode == :fail
    return false
  end

  @retry_count.times {
    n = 0
    start_time = (Time.now.to_f*1000).to_i
    @servers.each{|s|
      n += 1 if lock_instance(s,resource,val,ttl)
    }
    # Add 2 milliseconds to the drift to account for Redis expires
    # precision, which is 1 milliescond, plus 1 millisecond min drift 
    # for small TTLs.
    drift = (ttl*CLOCK_DRIFT_FACTOR).to_i + 2
    validity_time = ttl-((Time.now.to_f*1000).to_i - start_time)-drift 
    if n >= @quorum && validity_time > 0
      return {
        :validity => validity_time,
        :resource => resource,
        :val => val
      }
    else
      @servers.each{|s|
        unlock_instance(s,resource,val)
      }
    end
    # Wait a random delay before to retry
    sleep(rand(@retry_delay).to_f/1000)
  }
  return false
end

#lock_instance(redis, resource, val, ttl) ⇒ Object



41
42
43
44
45
46
47
# File 'lib/redlock.rb', line 41

def lock_instance(redis,resource,val,ttl)
  begin
    return redis.set(resource, val, nx: true, px: ttl)
  rescue
    return false
  end
end

#set_retry(count, delay) ⇒ Object



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

def set_retry(count,delay)
  @retry_count = count
  @retry_delay = delay
end

#testing=(mode) ⇒ Object



37
38
39
# File 'lib/redlock.rb', line 37

def testing=(mode)
  @testing_mode = mode
end

#unlock(lock) ⇒ Object



107
108
109
110
111
112
113
# File 'lib/redlock.rb', line 107

def unlock(lock)
  return if @testing_mode == :bypass

  @servers.each{|s|
    unlock_instance(s,lock[:resource],lock[:val])
  }
end

#unlock_instance(redis, resource, val) ⇒ Object



49
50
51
52
53
54
55
# File 'lib/redlock.rb', line 49

def unlock_instance(redis,resource,val)
  begin
    redis.evalsha(@unlock_sha, keys: [resource], argv: [val])
  rescue
    # Nothing to do, unlocking is just a best-effort attempt.
  end
end