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
# File 'lib/active_record/database_mutex/implementation.rb', line 22

def initialize(opts = {})
  @name = opts[:name] or raise ArgumentError, "mutex requires a :name argument"
  internal_name # create/check internal_name
end

Instance Attribute Details

#nameObject (readonly)

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



28
29
30
# File 'lib/active_record/database_mutex/implementation.rb', line 28

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

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



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

def internal_name
  @internal_name and return @internal_name
  encoded_name = ?$ + Digest::MD5.base64digest([ self.class.name, name ] * ?#).
    delete('^A-Za-z0-9+/').gsub(/[+\/]/, ?+ => ?_, ?/ => ?.)
  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



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/active_record/database_mutex/implementation.rb', line 109

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

#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



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

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)


202
203
204
# File 'lib/active_record/database_mutex/implementation.rb', line 202

def not_owned?
  not owned?
end

#owned?Boolean

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

Returns:

  • (Boolean)


196
197
198
# File 'lib/active_record/database_mutex/implementation.rb', line 196

def owned?
  query("SELECT CONNECTION_ID() = IS_USED_LOCK(#{quote(internal_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



78
79
80
81
82
83
84
85
# File 'lib/active_record/database_mutex/implementation.rb', line 78

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



210
211
212
# File 'lib/active_record/database_mutex/implementation.rb', line 210

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:



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/active_record/database_mutex/implementation.rb', line 141

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(internal_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



174
175
176
177
# File 'lib/active_record/database_mutex/implementation.rb', line 174

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



183
184
185
# File 'lib/active_record/database_mutex/implementation.rb', line 183

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