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

def initialize(opts = {})
  @name = opts[:name].to_s
  @name.size != 0 or raise ArgumentError, "mutex requires a nonempty :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.



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

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



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

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



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/active_record/database_mutex/implementation.rb', line 120

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



52
53
54
55
# File 'lib/active_record/database_mutex/implementation.rb', line 52

def lock_name
  prefix_name = name.gsub(/[^[:print:]]/, '')[0, 32]
  prefix_name + ?= + internal_name
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



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

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)


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

def not_owned?
  not owned?
end

#owned?Boolean

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

Returns:

  • (Boolean)


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

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



89
90
91
92
93
94
95
96
# File 'lib/active_record/database_mutex/implementation.rb', line 89

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



221
222
223
# File 'lib/active_record/database_mutex/implementation.rb', line 221

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:



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/active_record/database_mutex/implementation.rb', line 152

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



185
186
187
188
# File 'lib/active_record/database_mutex/implementation.rb', line 185

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



194
195
196
# File 'lib/active_record/database_mutex/implementation.rb', line 194

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