active_record_mutex 3.1.0 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 337ab2cfb637dc992dda05b67bc12098b0d8d7266591dc741d4918dbb3238aee
4
- data.tar.gz: e812c787de8cb82696027803eafa426793af334d4c1d847c9956473a7d1faf20
3
+ metadata.gz: 36b8a40a6140ecdfcd0e3892d2e008d502acdce96826ef3aecc797cadb65fa89
4
+ data.tar.gz: 38bf80d5ed330479f5a8399633048c606f7d03b71aaccacf512509c9cdcbb58e
5
5
  SHA512:
6
- metadata.gz: f8cb37ec99b0a52267f964b032b8f061801bef3000a11d355af5cddef3d5f075c471bd51e3fa79310136cbc53b3cd0376c293a501363b4f48f979fc774f03efc
7
- data.tar.gz: 1418b58e067e151bb33e92cdd7beb215cb3fdce4ea9f7439e34ae6315b452c80d9e0cafa5f368f399ec2abb7d48c0479a1a233b1fc01f4ff364043042647faa1
6
+ metadata.gz: a580eda2be88864af2b8aec078b09b5757992063f9078e64afb195c167bfc6b858f1d0674f2c348e66659c9f759e2cb47bb147a9b6118e9817510cdf6b77cb05
7
+ data.tar.gz: 51e00870718236b49c3271a0164d68fe6708e5b2451daad87b9f211090b3d7979a06546665d25082dadaca028fbb773c544ac33f7932c3a8d04150c2e31ff369
data/CHANGES.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changes
2
2
 
3
+ ## 2024-10-16 v3.2.1
4
+
5
+ * Refactor DatabaseMutex implementation:
6
+ * Change `@name` and `mutex.name` to downcase and freeze values
7
+ * Update tests to reflect new behavior
8
+
9
+ ## 2024-10-16 v3.2.0
10
+
11
+ * DatabaseMutex improvements:
12
+ * Improved lock name generation in `lock_name` method.
13
+ * Modified SQL queries to use `lock_name` instead of `internal_name`.
14
+ * Added `lock_exists?` and `test_all_mutexes` methods for testing mutex
15
+ creation and querying.
16
+
3
17
  ## 2024-10-16 v3.1.0
4
18
 
5
19
  * Changes for **3.1.0**:
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: active_record_mutex 3.1.0 ruby lib
2
+ # stub: active_record_mutex 3.2.1 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "active_record_mutex".freeze
6
- s.version = "3.1.0".freeze
6
+ s.version = "3.2.1".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
@@ -20,8 +20,10 @@ module ActiveRecord
20
20
  #
21
21
  # @raise [ ArgumentError ] if no **name** option is provided in the options hash.
22
22
  def initialize(opts = {})
23
- @name = opts[:name] or raise ArgumentError, "mutex requires a :name argument"
24
- internal_name # create/check internal_name
23
+ @name = opts[:name].to_s.downcase
24
+ @name.size != 0 or raise ArgumentError, "mutex requires a nonempty :name argument"
25
+ @name.freeze
26
+ lock_name # create/check internal_name and lock_name
25
27
  end
26
28
 
27
29
  # Returns the name of this mutex as given via the constructor argument.
@@ -34,7 +36,8 @@ module ActiveRecord
34
36
  def internal_name
35
37
  @internal_name and return @internal_name
36
38
  encoded_name = ?$ + Digest::MD5.base64digest([ self.class.name, name ] * ?#).
37
- delete('^A-Za-z0-9+/').gsub(/[+\/]/, ?+ => ?_, ?/ => ?.)
39
+ delete('^A-Za-z0-9+/').gsub(/[+\/]/, ?+ => ?_, ?/ => ?.).
40
+ downcase.freeze
38
41
  if encoded_name.size <= 64
39
42
  @internal_name = encoded_name
40
43
  else
@@ -43,38 +46,51 @@ module ActiveRecord
43
46
  end
44
47
  end
45
48
 
46
- # The synchronize method attempts to acquire a mutex lock for the given name
47
- # and executes the block passed to it. If the lock is already held by another
48
- # database connection, this method will return nil instead of raising an
49
- # exception and not execute the block. #
50
- #
51
- # This method provides a convenient way to ensure that critical sections of code
52
- # are executed while holding the mutex lock. It attempts to acquire the lock using
53
- # the underlying locking mechanisms (such as {lock} and {unlock}) and executes
54
- # the block passed to it.
55
- #
56
- # The **block** and **timeout** options are passed to the {lock} method
57
- # and configure the way the lock is acquired.
58
- #
59
- # The **force** option is passed to the {unlock} method, which will force the
60
- # lock to open if true.
61
- #
62
- # @example
63
- # foo.mutex.synchronize { do_something_with foo } # wait forever and never give up
64
- #
65
- # @example
66
- # foo.mutex.synchronize(timeout: 5) { do_something_with foo } # wait 5s and give up
67
- #
68
- # @example
69
- # unless foo.mutex.synchronize(block: false) { do_something_with foo }
70
- # # try again later
71
- # end
72
- #
73
- # @param opts [ Hash ] Options hash containing the **block**, **timeout**, or **force** keys
74
- #
75
- # @yield [ Result ] The block to be executed while holding the mutex lock
76
- #
77
- # @return [ Nil or result of yielded block ] depending on whether the lock was acquired
49
+ # The lock_name method generates the name for the mutex's internal lock
50
+ # variable based on its class and {name} attributes, prefixing it with a
51
+ # truncated version of the name that only includes printable characters.
52
+ #
53
+ # @return [ String ] the generated lock name
54
+ def lock_name
55
+ @lock_name and return @lock_name
56
+ prefix_name = name.gsub(/[^[:print:]]/, '')[0, 32]
57
+ @lock_name = prefix_name + ?= + internal_name
58
+ @lock_name.downcase!
59
+ @lock_name.freeze
60
+ end
61
+
62
+ # The synchronize method attempts to acquire a mutex lock for the given name
63
+ # and executes the block passed to it. If the lock is already held by another
64
+ # database connection, this method will return nil instead of raising an
65
+ # exception and not execute the block. #
66
+ #
67
+ # This method provides a convenient way to ensure that critical sections of code
68
+ # are executed while holding the mutex lock. It attempts to acquire the lock using
69
+ # the underlying locking mechanisms (such as {lock} and {unlock}) and executes
70
+ # the block passed to it.
71
+ #
72
+ # The **block** and **timeout** options are passed to the {lock} method
73
+ # and configure the way the lock is acquired.
74
+ #
75
+ # The **force** option is passed to the {unlock} method, which will force the
76
+ # lock to open if true.
77
+ #
78
+ # @example
79
+ # foo.mutex.synchronize { do_something_with foo } # wait forever and never give up
80
+ #
81
+ # @example
82
+ # foo.mutex.synchronize(timeout: 5) { do_something_with foo } # wait 5s and give up
83
+ #
84
+ # @example
85
+ # unless foo.mutex.synchronize(block: false) { do_something_with foo }
86
+ # # try again later
87
+ # end
88
+ #
89
+ # @param opts [ Hash ] Options hash containing the **block**, **timeout**, or **force** keys
90
+ #
91
+ # @yield [ Result ] The block to be executed while holding the mutex lock
92
+ #
93
+ # @return [ Nil or result of yielded block ] depending on whether the lock was acquired
78
94
  def synchronize(opts = {})
79
95
  locked = lock(opts.slice(:block, :timeout)) or return
80
96
  yield
@@ -147,7 +163,7 @@ module ActiveRecord
147
163
  decrement_counter
148
164
  end
149
165
  if counter_zero?
150
- case query("SELECT RELEASE_LOCK(#{quote(internal_name)})")
166
+ case query("SELECT RELEASE_LOCK(#{quote(lock_name)})")
151
167
  when 1
152
168
  true
153
169
  when 0, nil
@@ -181,7 +197,7 @@ module ActiveRecord
181
197
  #
182
198
  # @return [ true, false ] true if the mutex is unlocked, false otherwise
183
199
  def unlocked?
184
- query("SELECT IS_FREE_LOCK(#{quote(internal_name)})") == 1
200
+ query("SELECT IS_FREE_LOCK(#{quote(lock_name)})") == 1
185
201
  end
186
202
 
187
203
  # The locked? method returns true if this mutex is currently locked by
@@ -194,7 +210,7 @@ module ActiveRecord
194
210
 
195
211
  # Returns true if the mutex is was acquired on this database connection.
196
212
  def owned?
197
- query("SELECT CONNECTION_ID() = IS_USED_LOCK(#{quote(internal_name)})") == 1
213
+ query("SELECT CONNECTION_ID() = IS_USED_LOCK(#{quote(lock_name)})") == 1
198
214
  end
199
215
 
200
216
  # Returns true if this mutex was not acquired on this database connection,
@@ -225,13 +241,15 @@ module ActiveRecord
225
241
  ActiveRecord::Base.connection.quote(value)
226
242
  end
227
243
 
244
+ alias counter_name internal_name
245
+
228
246
  # The counter method generates a unique name for the mutex's internal
229
247
  # counter variable. This name is used as part of the SQL query to set and
230
248
  # retrieve the counter value.
231
249
  #
232
250
  # @return [String] the unique name for the mutex's internal counter variable.
233
251
  def counter
234
- "@#{internal_name}"
252
+ "@#{counter_name}"
235
253
  end
236
254
 
237
255
  # The increment_counter method increments the internal counter value for
@@ -290,7 +308,7 @@ module ActiveRecord
290
308
  increment_counter
291
309
  true
292
310
  else
293
- case query("SELECT GET_LOCK(#{quote(internal_name)}, #{timeout})")
311
+ case query("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})")
294
312
  when 1
295
313
  increment_counter
296
314
  true
@@ -1,6 +1,6 @@
1
1
  module ActiveRecord::DatabaseMutex
2
2
  # ActiveRecord::DatabaseMutex version
3
- VERSION = '3.1.0'
3
+ VERSION = '3.2.1'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
@@ -76,7 +76,7 @@ module ActiveRecord
76
76
  connection.select_all(<<~EOT).map { MutexInfo.new(_1) }
77
77
  SELECT * FROM performance_schema.metadata_locks
78
78
  WHERE OBJECT_TYPE = 'USER LEVEL LOCK'
79
- AND OBJECT_NAME LIKE "$%"
79
+ AND OBJECT_NAME LIKE "%=$%"
80
80
  EOT
81
81
  end
82
82
  end
@@ -25,7 +25,7 @@ class DatabaseMutexTest < Test::Unit::TestCase
25
25
  old, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], nil
26
26
  mutex = Foo.mutex
27
27
  assert_kind_of ActiveRecord::DatabaseMutex::Implementation, mutex
28
- assert_equal Foo.name, mutex.name
28
+ assert_equal Foo.name.downcase, mutex.name
29
29
  ensure
30
30
  ENV['RAILS_ENV'] = old
31
31
  end
@@ -34,7 +34,7 @@ class DatabaseMutexTest < Test::Unit::TestCase
34
34
  old, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test'
35
35
  mutex = Foo.mutex
36
36
  assert_kind_of ActiveRecord::DatabaseMutex::Implementation, mutex
37
- assert_equal "#{Foo.name}@test", mutex.name
37
+ assert_equal "#{Foo.name}@test".downcase, mutex.name
38
38
  ensure
39
39
  ENV['RAILS_ENV'] = old
40
40
  end
@@ -47,7 +47,7 @@ class DatabaseMutexTest < Test::Unit::TestCase
47
47
  assert_equal true, instance.save
48
48
  mutex = instance.mutex
49
49
  assert_kind_of ActiveRecord::DatabaseMutex::Implementation, mutex
50
- assert_equal "#{instance.id}@#{Foo.name}", mutex.name
50
+ assert_equal "#{instance.id}@#{Foo.name}".downcase, mutex.name
51
51
  end
52
52
 
53
53
  def test_factory_method_for
@@ -55,9 +55,26 @@ class DatabaseMutexTest < Test::Unit::TestCase
55
55
  assert_kind_of ActiveRecord::DatabaseMutex::Implementation, mutex
56
56
  end
57
57
 
58
+ private def lock_exists?(string)
59
+ string = string.downcase
60
+ ActiveRecord::Base.all_mutexes.map(&:OBJECT_NAME).any? {
61
+ _1.include?(string)
62
+ }
63
+ end
64
+
65
+ def test_all_mutexes
66
+ string = SecureRandom.hex(16)
67
+ mutex = ActiveRecord::DatabaseMutex.for(string)
68
+ assert_false lock_exists?(string)
69
+ mutex.lock
70
+ assert lock_exists?(string)
71
+ ensure
72
+ mutex.unlock force: true
73
+ end
74
+
58
75
  def test_create
59
76
  mutex = Implementation.new(:name => 'Create')
60
- assert_equal 'Create', mutex.name
77
+ assert_equal 'create', mutex.name
61
78
  end
62
79
 
63
80
  def test_lock
@@ -268,11 +285,11 @@ class DatabaseMutexTest < Test::Unit::TestCase
268
285
 
269
286
  def test_internal_name
270
287
  mutex = Implementation.new(:name => (250..255).map(&:chr) * '')
271
- assert_equal '$3i3xQvUrNPGyH6kaIOkiPw', mutex.send(:internal_name)
288
+ assert_equal '$3i3xqvurnpgyh6kaiokipw', mutex.send(:internal_name)
272
289
  end
273
290
 
274
291
  def test_counter_name
275
292
  mutex = Implementation.new(:name => (250..255).map(&:chr) * '')
276
- assert_equal '@$3i3xQvUrNPGyH6kaIOkiPw', mutex.send(:counter)
293
+ assert_equal '@$3i3xqvurnpgyh6kaiokipw', mutex.send(:counter)
277
294
  end
278
295
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_mutex
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank