Class: SimpleMutex::Mutex

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

Constant Summary collapse

DEFAULT_EXPIRES_IN =

1 hour

60 * 60
MAX_DEL_ATTEMPTS =
3
ERROR_MESSAGES =
{
  unlock: {
    unknown: lambda do |lock_key|
      "something when wrong when deleting lock key <#{lock_key}>."
    end,
    key_not_found: lambda do |lock_key|
      "lock not found for lock key <#{lock_key}>."
    end,
    signature_mismatch: lambda do |lock_key|
      "signature mismatch for lock key <#{lock_key}>."
    end,
    repeated_synchronization_anomaly: lambda do |lock_key|
      "repeated synchronization anomaly for <#{lock_key}>."
    end,
  }.freeze,
  lock: {
    basic: lambda do |lock_key|
      "failed to acquire lock <#{lock_key}>."
    end,
  }.freeze,
}.freeze
SynchronizationAnomalyError =
Class.new(::StandardError)
BaseError =
Class.new(::StandardError) do
  attr_reader :lock_key

  def initialize(msg, lock_key)
    @lock_key = lock_key
    super(msg)
  end
end
LockError =
Class.new(BaseError)
UnlockError =
Class.new(BaseError)

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(lock_key, expires_in: DEFAULT_EXPIRES_IN, signature: SecureRandom.uuid, payload: nil) ⇒ Mutex



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/simple_mutex/mutex.rb', line 142

def initialize(lock_key,
               expires_in: DEFAULT_EXPIRES_IN,
               signature:  SecureRandom.uuid,
               payload:    nil)
  ::SimpleMutex.redis_check!

  self.lock_key    = lock_key
  self.expires_in  = expires_in.to_i
  self.signature   = signature
  self.payload     = payload
end

Class Attribute Details

.redisObject

Returns the value of attribute redis.



48
49
50
# File 'lib/simple_mutex/mutex.rb', line 48

def redis
  @redis
end

Instance Attribute Details

#expires_inObject

Returns the value of attribute expires_in.



140
141
142
# File 'lib/simple_mutex/mutex.rb', line 140

def expires_in
  @expires_in
end

#lock_keyObject

Returns the value of attribute lock_key.



140
141
142
# File 'lib/simple_mutex/mutex.rb', line 140

def lock_key
  @lock_key
end

#payloadObject

Returns the value of attribute payload.



140
141
142
# File 'lib/simple_mutex/mutex.rb', line 140

def payload
  @payload
end

#signatureObject

Returns the value of attribute signature.



140
141
142
# File 'lib/simple_mutex/mutex.rb', line 140

def signature
  @signature
end

Class Method Details

.lock(lock_key, **options) ⇒ Object



50
51
52
# File 'lib/simple_mutex/mutex.rb', line 50

def lock(lock_key, **options)
  new(lock_key, **options).lock
end

.lock!(lock_key, **options) ⇒ Object



54
55
56
# File 'lib/simple_mutex/mutex.rb', line 54

def lock!(lock_key, **options)
  new(lock_key, **options).lock!
end

.raise_error(error_class, msg_template, lock_key) ⇒ Object

Raises:

  • (error_class)


124
125
126
127
128
129
# File 'lib/simple_mutex/mutex.rb', line 124

def raise_error(error_class, msg_template, lock_key)
  template_base = error_class.name.split("::").last.gsub("Error", "").downcase.to_sym
  error_msg     = ERROR_MESSAGES[template_base][msg_template].call(lock_key)

  raise(error_class.new(error_msg, lock_key))
end

.signature_valid?(raw_data, signature) ⇒ Boolean



131
132
133
134
135
136
137
# File 'lib/simple_mutex/mutex.rb', line 131

def signature_valid?(raw_data, signature)
  return false if raw_data.nil?

  JSON.parse(raw_data)["signature"] == signature
rescue JSON::ParserError, TypeError
  false
end

.unlock(lock_key, signature: nil, force: false) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/simple_mutex/mutex.rb', line 58

def unlock(lock_key, signature: nil, force: false)
  ::SimpleMutex.redis_check!

  redis = ::SimpleMutex.redis

  attempt = 0

  begin
    redis.watch(lock_key) do
      raw_data = redis.get(lock_key)
      raw_data = raw_data.value if raw_data.is_a?(Redis::Future)

      return false if raw_data.nil?
      return false unless force || signature_valid?(raw_data, signature)

      result = redis.multi { |multi| multi.del(lock_key) }.first

      raise SynchronizationAnomalyError, "Sync anomaly." unless result.is_a?(Integer)

      result.positive?
    ensure
      redis.unwatch
    end
  rescue SynchronizationAnomalyError
    retry if (attempt += 1) < MAX_DEL_ATTEMPTS
    raise_error(UnlockError, :repeated_synchronization_anomaly, lock_key)
  end
end

.unlock!(lock_key, signature: nil, force: false) ⇒ Object

rubocop:disable Metrics/MethodLength



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/simple_mutex/mutex.rb', line 88

def unlock!(lock_key, signature: nil, force: false)
  ::SimpleMutex.redis_check!

  redis = ::SimpleMutex.redis

  attempt = 0

  begin
    redis.watch(lock_key) do
      raw_data = redis.get(lock_key)
      raw_data = raw_data.value if raw_data.is_a?(Redis::Future)

      raise_error(UnlockError, :key_not_found, lock_key) if raw_data.nil?

      unless force || signature_valid?(raw_data, signature)
        raise_error(UnlockError, :signature_mismatch, lock_key)
      end

      result = redis.multi { |multi| multi.del(lock_key) }.first

      raise SynchronizationAnomalyError, "Sync anomaly." unless result.is_a?(Integer)
      raise_error(UnlockError, :unknown, lock_key) unless result.positive?
    ensure
      redis.unwatch
    end
  rescue SynchronizationAnomalyError
    retry if (attempt += 1) < MAX_DEL_ATTEMPTS
    raise_error(UnlockError, :repeated_synchronization_anomaly, lock_key)
  end
end

.with_lock(lock_key, **options, &block) ⇒ Object

rubocop:enable Metrics/MethodLength



120
121
122
# File 'lib/simple_mutex/mutex.rb', line 120

def with_lock(lock_key, **options, &block)
  new(lock_key, **options).with_lock(&block)
end

Instance Method Details

#lockObject



154
155
156
# File 'lib/simple_mutex/mutex.rb', line 154

def lock
  !!redis.set(lock_key, generate_data, nx: true, ex: expires_in)
end

#lock!Object



176
177
178
# File 'lib/simple_mutex/mutex.rb', line 176

def lock!
  lock or raise_error(LockError, :basic)
end

#lock_obtained?Boolean



172
173
174
# File 'lib/simple_mutex/mutex.rb', line 172

def lock_obtained?
  self.class.signature_valid?(redis.get(lock_key), signature)
end

#unlock(force: false) ⇒ Object



158
159
160
# File 'lib/simple_mutex/mutex.rb', line 158

def unlock(force: false)
  self.class.unlock(lock_key, signature: signature, force: force)
end

#unlock!(force: false) ⇒ Object



180
181
182
# File 'lib/simple_mutex/mutex.rb', line 180

def unlock!(force: false)
  self.class.unlock!(lock_key, signature: signature, force: force)
end

#with_lockObject



162
163
164
165
166
167
168
169
170
# File 'lib/simple_mutex/mutex.rb', line 162

def with_lock
  lock!

  begin
    yield
  ensure
    unlock
  end
end