Class: ActiveRecord::DatabaseMutex::Implementation

Inherits:
Object
  • Object
show all
Defined in:
lib/active_record/database_mutex/implementation.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Implementation

The initialize method initializes an instance of the DatabaseMutex class by setting its name and internal_name attributes.

Parameters:

  • opts (Hash) (defaults to: {})

    options hash containing the name key

Options Hash (opts):

  • name (String)

    name for the mutex, required.

Raises:

  • (ArgumentError)

    if no name option is provided in the options hash.



22
23
24
25
26
27
# File 'lib/active_record/database_mutex/implementation.rb', line 22

def initialize(opts = {})
  @name = opts[:name].to_s.downcase
  @name.size != 0 or raise ArgumentError, "mutex requires a nonempty :name argument"
  @name.freeze
  lock_name # create/check internal_name and lock_name
end

Instance Attribute Details

#nameObject (readonly)

Returns the name of this mutex as given via the constructor argument.



30
31
32
# File 'lib/active_record/database_mutex/implementation.rb', line 30

def name
  @name
end

Class Method Details

.dbObject

The db method returns an instance of ActiveRecord::Base.connection



9
10
11
# File 'lib/active_record/database_mutex/implementation.rb', line 9

def db
  ActiveRecord::Base.connection
end

Instance Method Details

#internal_nameString Also known as: counter_name

The internal_name method generates an encoded name for this mutex instance based on its class and #name attributes and memoizes it.

Returns:

  • (String)

    the encoded name of length <= 64 characters



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/active_record/database_mutex/implementation.rb', line 36

def internal_name
  @internal_name and return @internal_name
  encoded_name = ?$ + Digest::MD5.base64digest([ self.class.name, name ] * ?#).
    delete('^A-Za-z0-9+/').gsub(/[+\/]/, ?+ => ?_, ?/ => ?.).
    downcase.freeze
  if encoded_name.size <= 64
    @internal_name = encoded_name
  else
    # This should never happen:
    raise MutexInvalidState, "internal_name #{encoded_name} too long: >64 characters"
  end
end

#lock(opts = {}) ⇒ true, false

The lock method attempts to acquire the mutex lock for the configured name and returns true if successful, that means ##locked? and ##owned? will be true. Note that you can lock the mutex n-times, but it has to be unlocked n-times to be released as well.

If the block option was given as false, it returns false instead of raising MutexLocked exception when unable to acquire lock without blocking.

If a timeout option with the (nonnegative) timeout in seconds was given, a MutexLocked exception is raised after this time, otherwise the method blocks forever.

If the raise option is given as false, no MutexLocked exception is raised, but false is returned.

Parameters:

  • opts (Hash) (defaults to: {})

    the options hash

Options Hash (opts):

  • block, (true, false)

    defaults to true

  • raise, (true, false)

    defaults to true

  • timeout, (Integer, nil)

    defaults to nil, which means wait forever

Returns:

  • (true, false)

    depending on whether lock was acquired



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/active_record/database_mutex/implementation.rb', line 125

def lock(opts = {})
  opts = { block: true, raise: true }.merge(opts)
  if opts[:block]
    timeout = opts[:timeout] || -1
    lock_with_timeout timeout:
  else
    begin
      lock_with_timeout timeout: 0
    rescue MutexLocked
      false # If non-blocking and unable to acquire lock, return false.
    end
  end
rescue MutexLocked
  if opts[:raise]
    raise
  else
    return false
  end
end

#lock_nameString

The lock_name method generates the name for the mutex’s internal lock variable based on its class and #name attributes, prefixing it with a truncated version of the name that only includes printable characters.

Returns:

  • (String)

    the generated lock name



54
55
56
57
58
59
60
# File 'lib/active_record/database_mutex/implementation.rb', line 54

def lock_name
  @lock_name and return @lock_name
  prefix_name = name.gsub(/[^[:print:]]/, '')[0, 32]
  @lock_name = prefix_name + ?= + internal_name
  @lock_name.downcase!
  @lock_name.freeze
end

#locked?true, false

The locked? method returns true if this mutex is currently locked by any database connection, the opposite of #unlocked?.

Returns:

  • (true, false)

    true if the mutex is locked, false otherwise



207
208
209
# File 'lib/active_record/database_mutex/implementation.rb', line 207

def locked?
  not unlocked?
end

#not_owned?Boolean

Returns true if this mutex was not acquired on this database connection, the opposite of #owned?.

Returns:

  • (Boolean)


218
219
220
# File 'lib/active_record/database_mutex/implementation.rb', line 218

def not_owned?
  not owned?
end

#owned?Boolean

Returns true if the mutex is was acquired on this database connection.

Returns:

  • (Boolean)


212
213
214
# File 'lib/active_record/database_mutex/implementation.rb', line 212

def owned?
  query("SELECT CONNECTION_ID() = IS_USED_LOCK(#{quote(lock_name)})") == 1
end

#synchronize(opts = {}) {|Result| ... } ⇒ Nil or result of yielded block

The synchronize method attempts to acquire a mutex lock for the given name and executes the block passed to it. If the lock is already held by another database connection, this method will return nil instead of raising an exception and not execute the block. #

This method provides a convenient way to ensure that critical sections of code are executed while holding the mutex lock. It attempts to acquire the lock using the underlying locking mechanisms (such as #lock and #unlock) and executes the block passed to it.

The block and timeout options are passed to the #lock method and configure the way the lock is acquired.

The force option is passed to the #unlock method, which will force the lock to open if true.

Examples:

foo.mutex.synchronize { do_something_with foo } # wait forever and never give up
foo.mutex.synchronize(timeout: 5) { do_something_with foo } # wait 5s and give up
unless foo.mutex.synchronize(block: false) { do_something_with foo }
  # try again later
end

Parameters:

  • opts (Hash) (defaults to: {})

    Options hash containing the block, timeout, or force keys

Yields:

  • (Result)

    The block to be executed while holding the mutex lock

Returns:

  • (Nil or result of yielded block)

    depending on whether the lock was acquired



94
95
96
97
98
99
100
101
# File 'lib/active_record/database_mutex/implementation.rb', line 94

def synchronize(opts = {})
  locked = lock(opts.slice(:block, :timeout)) or return
  yield
rescue ActiveRecord::DatabaseMutex::MutexLocked
  return nil
ensure
  locked and unlock opts.slice(:force)
end

#to_sString Also known as: inspect

The to_s method returns a string representation of this DatabaseMutex instance.

Returns:

  • (String)

    the string representation of this DatabaseMutex instance



226
227
228
# File 'lib/active_record/database_mutex/implementation.rb', line 226

def to_s
  "#<#{self.class} #{name}>"
end

#unlock(opts = {}) ⇒ true, false

The unlock method releases the mutex lock for the given name and returns true if successful. If the lock doesn’t belong to this connection raises a MutexUnlockFailed exception.

Parameters:

  • opts (Hash) (defaults to: {})

    the options hash

Options Hash (opts):

  • raise (true, false)

    if false won’t raise MutexUnlockFailed, defaults to true

  • force (true, false)

    if true will force the lock to open, defaults to false

Returns:

  • (true, false)

    true if unlocking was successful, false otherwise

Raises:



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/active_record/database_mutex/implementation.rb', line 157

def unlock(opts = {})
  opts = { raise: true, force: false }.merge(opts)
  if owned?
    if opts[:force]
      reset_counter
    else
      decrement_counter
    end
    if counter_zero?
      case query("SELECT RELEASE_LOCK(#{quote(lock_name)})")
      when 1
        true
      when 0, nil
        raise MutexUnlockFailed, "unlocking of mutex '#{name}' failed"
      end
    else
      false
    end
  else
    raise MutexUnlockFailed, "unlocking of mutex '#{name}' failed"
  end
rescue MutexUnlockFailed
  if opts[:raise]
    raise
  else
    return false
  end
end

#unlock?(opts = {}) ⇒ self?

The unlock? method returns self if the mutex could successfully unlocked, otherwise it returns nil.

Returns:

  • (self, nil)

    self if the mutex was unlocked, nil otherwise



190
191
192
193
# File 'lib/active_record/database_mutex/implementation.rb', line 190

def unlock?(opts = {})
  opts = { raise: false }.merge(opts)
  self if unlock(opts)
end

#unlocked?true, false

The unlocked? method checks whether the mutex is currently free and not locked by any database connection.

Returns:

  • (true, false)

    true if the mutex is unlocked, false otherwise



199
200
201
# File 'lib/active_record/database_mutex/implementation.rb', line 199

def unlocked?
  query("SELECT IS_FREE_LOCK(#{quote(lock_name)})") == 1
end