with_advisory_lock 4.6.0 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +80 -0
  3. data/.github/workflows/release.yml +20 -0
  4. data/.gitignore +2 -0
  5. data/.release-please-manifest.json +1 -0
  6. data/.tool-versions +1 -1
  7. data/Appraisals +34 -18
  8. data/CHANGELOG.md +31 -0
  9. data/Gemfile +0 -12
  10. data/README.md +17 -6
  11. data/gemfiles/{activerecord_6.0.gemfile → activerecord_6.1.gemfile} +4 -2
  12. data/gemfiles/{activerecord_5.2.gemfile → activerecord_7.0.gemfile} +4 -2
  13. data/gemfiles/activerecord_7.1.gemfile +14 -0
  14. data/lib/with_advisory_lock/base.rb +17 -3
  15. data/lib/with_advisory_lock/concern.rb +13 -17
  16. data/lib/with_advisory_lock/database_adapter_support.rb +4 -41
  17. data/lib/with_advisory_lock/failed_to_acquire_lock.rb +9 -0
  18. data/lib/with_advisory_lock/flock.rb +4 -3
  19. data/lib/with_advisory_lock/mysql.rb +5 -5
  20. data/lib/with_advisory_lock/postgresql.rb +9 -7
  21. data/lib/with_advisory_lock/version.rb +3 -1
  22. data/lib/with_advisory_lock.rb +8 -10
  23. data/release-please-config.json +9 -0
  24. data/test/concern_test.rb +23 -10
  25. data/test/lock_test.rb +61 -28
  26. data/test/nesting_test.rb +14 -79
  27. data/test/options_test.rb +35 -33
  28. data/test/parallelism_test.rb +35 -37
  29. data/test/shared_test.rb +93 -90
  30. data/test/test_helper.rb +52 -0
  31. data/test/test_models.rb +9 -7
  32. data/test/thread_test.rb +23 -22
  33. data/test/transaction_test.rb +34 -36
  34. data/with_advisory_lock.gemspec +29 -23
  35. metadata +32 -28
  36. data/.travis.yml +0 -38
  37. data/gemfiles/activerecord_4.2.gemfile +0 -19
  38. data/gemfiles/activerecord_5.0.gemfile +0 -19
  39. data/gemfiles/activerecord_5.1.gemfile +0 -19
  40. data/lib/with_advisory_lock/mysql_no_nesting.rb +0 -20
  41. data/lib/with_advisory_lock/nested_advisory_lock_error.rb +0 -14
  42. data/test/database.yml +0 -17
  43. data/test/minitest_helper.rb +0 -40
  44. data/tests.sh +0 -11
data/test/shared_test.rb CHANGED
@@ -1,131 +1,134 @@
1
- require 'minitest_helper'
1
+ # frozen_string_literal: true
2
2
 
3
- describe 'shared locks' do
4
- def supported?
5
- env_db != :mysql
6
- end
7
-
8
- class SharedTestWorker
9
- def initialize(shared)
10
- @shared = shared
3
+ require 'test_helper'
4
+ class SharedTestWorker
5
+ def initialize(shared)
6
+ @shared = shared
11
7
 
12
- @locked = nil
13
- @cleanup = false
14
- @thread = Thread.new { work }
15
- end
8
+ @locked = nil
9
+ @cleanup = false
10
+ @thread = Thread.new { work }
11
+ end
16
12
 
17
- def locked?
18
- sleep 0.01 while @locked.nil? && @thread.alive?
19
- @locked
20
- end
13
+ def locked?
14
+ sleep 0.01 while @locked.nil? && @thread.alive?
15
+ @locked
16
+ end
21
17
 
22
- def cleanup!
23
- @cleanup = true
24
- @thread.join
25
- raise if @thread.status.nil?
26
- end
18
+ def cleanup!
19
+ @cleanup = true
20
+ @thread.join
21
+ raise if @thread.status.nil?
22
+ end
27
23
 
28
- private
24
+ private
29
25
 
30
- def work
31
- ActiveRecord::Base.connection_pool.with_connection do
32
- Tag.with_advisory_lock('test', timeout_seconds: 0, shared: @shared) do
33
- @locked = true
34
- sleep 0.01 until @cleanup
35
- end
36
- @locked = false
26
+ def work
27
+ ActiveRecord::Base.connection_pool.with_connection do
28
+ Tag.with_advisory_lock('test', timeout_seconds: 0, shared: @shared) do
29
+ @locked = true
37
30
  sleep 0.01 until @cleanup
38
31
  end
32
+ @locked = false
33
+ sleep 0.01 until @cleanup
39
34
  end
40
35
  end
36
+ end
37
+
38
+ class SharedLocksTest < GemTestCase
39
+ def supported?
40
+ %i[trilogy mysql2 jdbcmysql].exclude?(env_db)
41
+ end
41
42
 
42
- it 'does not allow two exclusive locks' do
43
+ test 'does not allow two exclusive locks' do
43
44
  one = SharedTestWorker.new(false)
44
- one.locked?.must_equal true
45
+ assert_predicate(one, :locked?)
45
46
 
46
47
  two = SharedTestWorker.new(false)
47
- two.locked?.must_equal false
48
+ refute(two.locked?)
48
49
 
49
50
  one.cleanup!
50
51
  two.cleanup!
51
52
  end
53
+ end
52
54
 
53
- describe 'not supported' do
54
- before do
55
- skip if supported?
56
- end
55
+ class NotSupportedEnvironmentTest < SharedLocksTest
56
+ setup do
57
+ skip if supported?
58
+ end
57
59
 
58
- it 'raises an error when attempting to use a shared lock' do
59
- one = SharedTestWorker.new(true)
60
- one.locked?.must_be_nil
61
- exception = proc {
62
- one.cleanup!
63
- }.must_raise ArgumentError
64
- exception.message.must_include 'not supported'
60
+ test 'raises an error when attempting to use a shared lock' do
61
+ one = SharedTestWorker.new(true)
62
+ assert_nil(one.locked?)
63
+
64
+ exception = assert_raises(ArgumentError) do
65
+ one.cleanup!
65
66
  end
67
+
68
+ assert_match(/#{Regexp.escape('not supported')}/, exception.message)
66
69
  end
70
+ end
67
71
 
68
- describe 'supported' do
69
- before do
70
- skip unless supported?
71
- end
72
+ class SupportedEnvironmentTest < SharedLocksTest
73
+ setup do
74
+ skip unless supported?
75
+ end
72
76
 
73
- it 'does allow two shared locks' do
74
- one = SharedTestWorker.new(true)
75
- one.locked?.must_equal true
77
+ test 'does allow two shared locks' do
78
+ one = SharedTestWorker.new(true)
79
+ assert_predicate(one, :locked?)
76
80
 
77
- two = SharedTestWorker.new(true)
78
- two.locked?.must_equal true
81
+ two = SharedTestWorker.new(true)
82
+ assert_predicate(two, :locked?)
79
83
 
80
- one.cleanup!
81
- two.cleanup!
82
- end
84
+ one.cleanup!
85
+ two.cleanup!
86
+ end
83
87
 
84
- it 'does not allow exclusive lock with shared lock' do
85
- one = SharedTestWorker.new(true)
86
- one.locked?.must_equal true
88
+ test 'does not allow exclusive lock with shared lock' do
89
+ one = SharedTestWorker.new(true)
90
+ assert_predicate(one, :locked?)
87
91
 
88
- two = SharedTestWorker.new(false)
89
- two.locked?.must_equal false
92
+ two = SharedTestWorker.new(false)
93
+ refute(two.locked?)
90
94
 
91
- three = SharedTestWorker.new(true)
92
- three.locked?.must_equal true
95
+ three = SharedTestWorker.new(true)
96
+ assert_predicate(three, :locked?)
93
97
 
94
- one.cleanup!
95
- two.cleanup!
96
- three.cleanup!
97
- end
98
+ one.cleanup!
99
+ two.cleanup!
100
+ three.cleanup!
101
+ end
98
102
 
99
- it 'does not allow shared lock with exclusive lock' do
100
- one = SharedTestWorker.new(false)
101
- one.locked?.must_equal true
103
+ test 'does not allow shared lock with exclusive lock' do
104
+ one = SharedTestWorker.new(false)
105
+ assert_predicate(one, :locked?)
102
106
 
103
- two = SharedTestWorker.new(true)
104
- two.locked?.must_equal false
107
+ two = SharedTestWorker.new(true)
108
+ refute(two.locked?)
105
109
 
106
- one.cleanup!
107
- two.cleanup!
108
- end
110
+ one.cleanup!
111
+ two.cleanup!
112
+ end
109
113
 
110
- describe 'PostgreSQL' do
111
- before do
112
- skip unless env_db == :postgresql
113
- end
114
+ class PostgreSQLTest < SupportedEnvironmentTest
115
+ setup do
116
+ skip unless env_db == :postgresql
117
+ end
114
118
 
115
- def pg_lock_modes
116
- ActiveRecord::Base.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';")
117
- end
119
+ def pg_lock_modes
120
+ ActiveRecord::Base.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';")
121
+ end
118
122
 
119
- it 'allows shared lock to be upgraded to an exclusive lock' do
120
- pg_lock_modes.must_equal %w[]
121
- Tag.with_advisory_lock 'test', shared: true do
122
- pg_lock_modes.must_equal %w[ShareLock]
123
- Tag.with_advisory_lock 'test', shared: false do
124
- pg_lock_modes.must_equal %w[ShareLock ExclusiveLock]
125
- end
123
+ test 'allows shared lock to be upgraded to an exclusive lock' do
124
+ assert_empty(pg_lock_modes)
125
+ Tag.with_advisory_lock 'test', shared: true do
126
+ assert_equal(%w[ShareLock], pg_lock_modes)
127
+ Tag.with_advisory_lock 'test', shared: false do
128
+ assert_equal(%w[ShareLock ExclusiveLock], pg_lock_modes)
126
129
  end
127
- pg_lock_modes.must_equal %w[]
128
130
  end
131
+ assert_empty(pg_lock_modes)
129
132
  end
130
133
  end
131
134
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'active_record'
5
+ require 'with_advisory_lock'
6
+ require 'tmpdir'
7
+ require 'securerandom'
8
+ begin
9
+ require 'activerecord-trilogy-adapter'
10
+ ActiveSupport.on_load(:active_record) do
11
+ require "trilogy_adapter/connection"
12
+ ActiveRecord::Base.public_send :extend, TrilogyAdapter::Connection
13
+ end
14
+ rescue LoadError
15
+ # do nothing
16
+ end
17
+
18
+ ActiveRecord::Base.configurations = {
19
+ default_env: {
20
+ url: ENV.fetch('DATABASE_URL', "sqlite3://#{Dir.tmpdir}/#{SecureRandom.hex}.sqlite3"),
21
+ properties: { allowPublicKeyRetrieval: true } # for JRuby madness
22
+ }
23
+ }
24
+
25
+ ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex
26
+
27
+ ActiveRecord::Base.establish_connection
28
+
29
+ def env_db
30
+ @env_db ||= ActiveRecord::Base.connection_db_config.adapter.to_sym
31
+ end
32
+
33
+ ActiveRecord::Migration.verbose = false
34
+
35
+ require 'test_models'
36
+ require 'minitest'
37
+ require 'maxitest/autorun'
38
+ require 'mocha/minitest'
39
+
40
+ class GemTestCase < ActiveSupport::TestCase
41
+ setup do
42
+ ENV['FLOCK_DIR'] = Dir.mktmpdir
43
+ Tag.delete_all
44
+ TagAudit.delete_all
45
+ Label.delete_all
46
+ end
47
+ teardown do
48
+ FileUtils.remove_entry_secure ENV['FLOCK_DIR']
49
+ end
50
+ end
51
+
52
+ puts "Testing with #{env_db} database, ActiveRecord #{ActiveRecord.gem_version} and #{RUBY_ENGINE} #{RUBY_ENGINE_VERSION} as #{RUBY_VERSION}"
data/test/test_models.rb CHANGED
@@ -1,12 +1,14 @@
1
- ActiveRecord::Schema.define(:version => 0) do
2
- create_table "tags", :force => true do |t|
3
- t.string "name"
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::Schema.define(version: 0) do
4
+ create_table 'tags', force: true do |t|
5
+ t.string 'name'
4
6
  end
5
- create_table "tag_audits", :id => false, :force => true do |t|
6
- t.string "tag_name"
7
+ create_table 'tag_audits', id: false, force: true do |t|
8
+ t.string 'tag_name'
7
9
  end
8
- create_table "labels", :id => false, :force => true do |t|
9
- t.string "name"
10
+ create_table 'labels', id: false, force: true do |t|
11
+ t.string 'name'
10
12
  end
11
13
  end
12
14
 
data/test/thread_test.rb CHANGED
@@ -1,16 +1,17 @@
1
- require 'minitest_helper'
1
+ # frozen_string_literal: true
2
2
 
3
- describe 'separate thread tests' do
4
- let(:lock_name) { 'testing 1,2,3' } # OMG COMMAS
3
+ require 'test_helper'
5
4
 
6
- before do
5
+ class SeparateThreadTest < GemTestCase
6
+ setup do
7
+ @lock_name = 'testing 1,2,3' # OMG COMMAS
7
8
  @mutex = Mutex.new
8
9
  @t1_acquired_lock = false
9
10
  @t1_return_value = nil
10
11
 
11
12
  @t1 = Thread.new do
12
13
  ActiveRecord::Base.connection_pool.with_connection do
13
- @t1_return_value = Label.with_advisory_lock(lock_name) do
14
+ @t1_return_value = Label.with_advisory_lock(@lock_name) do
14
15
  @mutex.synchronize { @t1_acquired_lock = true }
15
16
  sleep
16
17
  't1 finished'
@@ -19,42 +20,42 @@ describe 'separate thread tests' do
19
20
  end
20
21
 
21
22
  # Wait for the thread to acquire the lock:
22
- until @mutex.synchronize { @t1_acquired_lock } do
23
- sleep(0.1)
24
- end
23
+ sleep(0.1) until @mutex.synchronize { @t1_acquired_lock }
25
24
  ActiveRecord::Base.connection.reconnect!
26
25
  end
27
26
 
28
- after do
27
+ teardown do
29
28
  @t1.wakeup if @t1.status == 'sleep'
30
29
  @t1.join
31
30
  end
32
31
 
33
- it '#with_advisory_lock with a 0 timeout returns false immediately' do
34
- response = Label.with_advisory_lock(lock_name, 0) do
35
- fail 'should not be yielded to'
32
+ test '#with_advisory_lock with a 0 timeout returns false immediately' do
33
+ response = Label.with_advisory_lock(@lock_name, 0) do
34
+ raise 'should not be yielded to'
36
35
  end
37
- response.must_be_false
36
+ assert_not(response)
38
37
  end
39
38
 
40
- it '#with_advisory_lock yields to the provided block' do
41
- @t1_acquired_lock.must_be_true
39
+ test '#with_advisory_lock yields to the provided block' do
40
+ assert(@t1_acquired_lock)
42
41
  end
43
42
 
44
- it '#advisory_lock_exists? returns true when another thread has the lock' do
45
- Tag.advisory_lock_exists?(lock_name).must_be_true
43
+ test '#advisory_lock_exists? returns true when another thread has the lock' do
44
+ assert(Tag.advisory_lock_exists?(@lock_name))
46
45
  end
47
46
 
48
- it 'can re-establish the lock after the other thread releases it' do
47
+ test 'can re-establish the lock after the other thread releases it' do
49
48
  @t1.wakeup
50
49
  @t1.join
51
- @t1_return_value.must_equal 't1 finished'
50
+ assert_equal('t1 finished', @t1_return_value)
52
51
 
53
52
  # We should now be able to acquire the lock immediately:
54
53
  reacquired = false
55
- Label.with_advisory_lock(lock_name, 0) do
54
+ lock_result = Label.with_advisory_lock(@lock_name, 0) do
56
55
  reacquired = true
57
- end.must_be_true
58
- reacquired.must_be_true
56
+ end
57
+
58
+ assert(lock_result)
59
+ assert(reacquired)
59
60
  end
60
61
  end
@@ -1,70 +1,68 @@
1
- require 'minitest_helper'
1
+ # frozen_string_literal: true
2
2
 
3
- describe 'transaction scoping' do
3
+ require 'test_helper'
4
+
5
+ class TransactionScopingTest < GemTestCase
4
6
  def supported?
5
- env_db == :postgresql
7
+ %i[postgresql jdbcpostgresql].include?(env_db)
6
8
  end
7
9
 
8
- describe 'not supported' do
9
- before do
10
- skip if supported?
11
- end
10
+ test 'raises an error when attempting to use transaction level locks if not supported' do
11
+ skip if supported?
12
12
 
13
- it 'raises an error when attempting to use transaction level locks' do
14
- Tag.transaction do
15
- exception = proc {
16
- Tag.with_advisory_lock 'test', transaction: true do
17
- raise 'should not get here'
18
- end
19
- }.must_raise ArgumentError
20
- exception.message.must_include 'not supported'
13
+ Tag.transaction do
14
+ exception = assert_raises(ArgumentError) do
15
+ Tag.with_advisory_lock 'test', transaction: true do
16
+ raise 'should not get here'
17
+ end
21
18
  end
19
+
20
+ assert_match(/#{Regexp.escape('not supported')}/, exception.message)
22
21
  end
23
22
  end
24
23
 
25
- describe 'supported' do
26
- before do
24
+ class PostgresqlTest < TransactionScopingTest
25
+ setup do
27
26
  skip unless env_db == :postgresql
27
+ @pg_lock_count = lambda do
28
+ ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM pg_locks WHERE locktype = 'advisory';").to_i
29
+ end
28
30
  end
29
31
 
30
- def pg_lock_count
31
- ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM pg_locks WHERE locktype = 'advisory';").to_i
32
- end
33
-
34
- specify 'session locks release after the block executes' do
32
+ test 'session locks release after the block executes' do
35
33
  Tag.transaction do
36
- pg_lock_count.must_equal 0
34
+ assert_equal(0, @pg_lock_count.call)
37
35
  Tag.with_advisory_lock 'test' do
38
- pg_lock_count.must_equal 1
36
+ assert_equal(1, @pg_lock_count.call)
39
37
  end
40
- pg_lock_count.must_equal 0
38
+ assert_equal(0, @pg_lock_count.call)
41
39
  end
42
40
  end
43
41
 
44
- specify 'session locks release when transaction fails inside block' do
42
+ test 'session locks release when transaction fails inside block' do
45
43
  Tag.transaction do
46
- pg_lock_count.must_equal 0
44
+ assert_equal(0, @pg_lock_count.call)
47
45
 
48
- exception = proc {
46
+ exception = assert_raises(ActiveRecord::StatementInvalid) do
49
47
  Tag.with_advisory_lock 'test' do
50
48
  Tag.connection.execute 'SELECT 1/0;'
51
49
  end
52
- }.must_raise ActiveRecord::StatementInvalid
53
- exception.message.must_include 'division by zero'
50
+ end
54
51
 
55
- pg_lock_count.must_equal 0
52
+ assert_match(/#{Regexp.escape('division by zero')}/, exception.message)
53
+ assert_equal(0, @pg_lock_count.call)
56
54
  end
57
55
  end
58
56
 
59
- specify 'transaction level locks hold until the transaction completes' do
57
+ test 'transaction level locks hold until the transaction completes' do
60
58
  Tag.transaction do
61
- pg_lock_count.must_equal 0
59
+ assert_equal(0, @pg_lock_count.call)
62
60
  Tag.with_advisory_lock 'test', transaction: true do
63
- pg_lock_count.must_equal 1
61
+ assert_equal(1, @pg_lock_count.call)
64
62
  end
65
- pg_lock_count.must_equal 1
63
+ assert_equal(1, @pg_lock_count.call)
66
64
  end
67
- pg_lock_count.must_equal 0
65
+ assert_equal(0, @pg_lock_count.call)
68
66
  end
69
67
  end
70
68
  end
@@ -1,29 +1,35 @@
1
- lib = File.expand_path('../lib', __FILE__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'with_advisory_lock/version'
1
+ # frozen_string_literal: true
4
2
 
5
- Gem::Specification.new do |gem|
6
- gem.name = "with_advisory_lock"
7
- gem.version = WithAdvisoryLock::VERSION
8
- gem.authors = ['Matthew McEachen']
9
- gem.email = %w(matthew+github@mceachen.org)
10
- gem.homepage = 'https://github.com/mceachen/with_advisory_lock'
11
- gem.summary = %q{Advisory locking for ActiveRecord}
12
- gem.description = %q{Advisory locking for ActiveRecord}
13
- gem.license = 'MIT'
3
+ require 'English'
4
+ require_relative 'lib/with_advisory_lock/version'
14
5
 
15
- gem.files = `git ls-files`.split($/)
16
- gem.test_files = gem.files.grep(%r{^test/})
17
- gem.require_paths = %w(lib)
18
- gem.required_ruby_version = '>= 2.2.10'
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'with_advisory_lock'
8
+ spec.version = WithAdvisoryLock::VERSION
9
+ spec.authors = ['Matthew McEachen', 'Abdelkader Boudih']
10
+ spec.email = %w[matthew+github@mceachen.org terminale@gmail.com]
11
+ spec.homepage = 'https://github.com/ClosureTree/with_advisory_lock'
12
+ spec.summary = 'Advisory locking for ActiveRecord'
13
+ spec.description = 'Advisory locking for ActiveRecord'
14
+ spec.license = 'MIT'
19
15
 
20
- gem.add_runtime_dependency 'activerecord', '>= 4.2'
16
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
+ spec.test_files = spec.files.grep(%r{^test/})
18
+ spec.require_paths = %w[lib]
19
+ spec.metadata = { 'rubyspecs_mfa_required' => 'true' }
20
+ spec.required_ruby_version = '>= 2.7.0'
21
+ spec.metadata['yard.run'] = 'yri'
21
22
 
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = 'https://github.com/ClosureTree/with_advisory_lock'
25
+ spec.metadata['changelog_uri'] = 'https://github.com/ClosureTree/with_advisory_lock/blob/master/CHANGELOG.md'
22
26
 
23
- gem.add_development_dependency 'yard'
24
- gem.add_development_dependency 'minitest'
25
- gem.add_development_dependency 'minitest-great_expectations'
26
- gem.add_development_dependency 'minitest-reporters'
27
- gem.add_development_dependency 'mocha'
28
- gem.add_development_dependency 'appraisal'
27
+ spec.add_runtime_dependency 'activerecord', '>= 6.1'
28
+ spec.add_runtime_dependency 'zeitwerk', '>= 2.6'
29
+
30
+ spec.add_development_dependency 'appraisal'
31
+ spec.add_development_dependency 'maxitest'
32
+ spec.add_development_dependency 'minitest-reporters'
33
+ spec.add_development_dependency 'mocha'
34
+ spec.add_development_dependency 'yard'
29
35
  end