with_advisory_lock 5.1.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +36 -40
  3. data/.github/workflows/release.yml +1 -4
  4. data/.gitignore +2 -2
  5. data/.release-please-manifest.json +1 -1
  6. data/.ruby-version +2 -0
  7. data/.tool-versions +1 -1
  8. data/CHANGELOG.md +51 -0
  9. data/Gemfile +31 -0
  10. data/LICENSE.txt +4 -4
  11. data/Makefile +10 -0
  12. data/README.md +7 -35
  13. data/Rakefile +5 -2
  14. data/bin/console +11 -0
  15. data/bin/rails +15 -0
  16. data/bin/sanity +20 -0
  17. data/bin/sanity_check +86 -0
  18. data/bin/setup +8 -0
  19. data/bin/setup_test_db +59 -0
  20. data/bin/test_connections +22 -0
  21. data/docker-compose.yml +19 -0
  22. data/lib/with_advisory_lock/concern.rb +27 -16
  23. data/lib/with_advisory_lock/core_advisory.rb +110 -0
  24. data/lib/with_advisory_lock/jruby_adapter.rb +29 -0
  25. data/lib/with_advisory_lock/lock_stack_item.rb +6 -0
  26. data/lib/with_advisory_lock/mysql_advisory.rb +62 -0
  27. data/lib/with_advisory_lock/postgresql_advisory.rb +112 -0
  28. data/lib/with_advisory_lock/result.rb +14 -0
  29. data/lib/with_advisory_lock/version.rb +1 -1
  30. data/lib/with_advisory_lock.rb +38 -9
  31. data/test/dummy/Rakefile +8 -0
  32. data/test/dummy/app/controllers/application_controller.rb +7 -0
  33. data/test/dummy/app/models/application_record.rb +6 -0
  34. data/test/dummy/app/models/label.rb +4 -0
  35. data/test/dummy/app/models/mysql_label.rb +5 -0
  36. data/test/dummy/app/models/mysql_record.rb +6 -0
  37. data/test/dummy/app/models/mysql_tag.rb +10 -0
  38. data/test/dummy/app/models/mysql_tag_audit.rb +5 -0
  39. data/test/dummy/app/models/tag.rb +8 -0
  40. data/test/dummy/app/models/tag_audit.rb +4 -0
  41. data/test/dummy/config/application.rb +31 -0
  42. data/test/dummy/config/boot.rb +3 -0
  43. data/test/dummy/config/database.yml +13 -0
  44. data/test/dummy/config/environment.rb +7 -0
  45. data/test/dummy/config/routes.rb +4 -0
  46. data/test/dummy/config.ru +6 -0
  47. data/test/{test_models.rb → dummy/db/schema.rb} +3 -14
  48. data/test/dummy/db/secondary_schema.rb +15 -0
  49. data/test/dummy/lib/tasks/db.rake +40 -0
  50. data/test/sanity_check_test.rb +63 -0
  51. data/test/test_helper.rb +18 -37
  52. data/test/with_advisory_lock/concern_test.rb +79 -0
  53. data/test/with_advisory_lock/lock_test.rb +197 -0
  54. data/test/with_advisory_lock/multi_adapter_test.rb +17 -0
  55. data/test/with_advisory_lock/parallelism_test.rb +101 -0
  56. data/test/with_advisory_lock/postgresql_race_condition_test.rb +118 -0
  57. data/test/with_advisory_lock/shared_test.rb +129 -0
  58. data/test/with_advisory_lock/thread_test.rb +83 -0
  59. data/test/with_advisory_lock/transaction_test.rb +83 -0
  60. data/with_advisory_lock.gemspec +26 -6
  61. metadata +64 -55
  62. data/Appraisals +0 -45
  63. data/gemfiles/activerecord_6.1.gemfile +0 -21
  64. data/gemfiles/activerecord_7.0.gemfile +0 -21
  65. data/gemfiles/activerecord_7.1.gemfile +0 -14
  66. data/lib/with_advisory_lock/base.rb +0 -118
  67. data/lib/with_advisory_lock/database_adapter_support.rb +0 -26
  68. data/lib/with_advisory_lock/flock.rb +0 -33
  69. data/lib/with_advisory_lock/mysql.rb +0 -27
  70. data/lib/with_advisory_lock/postgresql.rb +0 -43
  71. data/test/concern_test.rb +0 -33
  72. data/test/lock_test.rb +0 -80
  73. data/test/nesting_test.rb +0 -28
  74. data/test/options_test.rb +0 -66
  75. data/test/parallelism_test.rb +0 -75
  76. data/test/shared_test.rb +0 -134
  77. data/test/thread_test.rb +0 -61
  78. data/test/transaction_test.rb +0 -68
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'zlib'
4
-
5
- module WithAdvisoryLock
6
- class Result
7
- attr_reader :result
8
-
9
- def initialize(lock_was_acquired, result = false)
10
- @lock_was_acquired = lock_was_acquired
11
- @result = result
12
- end
13
-
14
- def lock_was_acquired?
15
- @lock_was_acquired
16
- end
17
- end
18
-
19
- FAILED_TO_LOCK = Result.new(false)
20
-
21
- LockStackItem = Struct.new(:name, :shared)
22
-
23
- class Base
24
- attr_reader :connection, :lock_name, :timeout_seconds, :shared, :transaction, :disable_query_cache
25
-
26
- def initialize(connection, lock_name, options)
27
- options = { timeout_seconds: options } unless options.respond_to?(:fetch)
28
- options.assert_valid_keys :timeout_seconds, :shared, :transaction, :disable_query_cache
29
-
30
- @connection = connection
31
- @lock_name = lock_name
32
- @timeout_seconds = options.fetch(:timeout_seconds, nil)
33
- @shared = options.fetch(:shared, false)
34
- @transaction = options.fetch(:transaction, false)
35
- @disable_query_cache = options.fetch(:disable_query_cache, false)
36
- end
37
-
38
- def lock_str
39
- @lock_str ||= "#{ENV['WITH_ADVISORY_LOCK_PREFIX']}#{lock_name}"
40
- end
41
-
42
- def lock_stack_item
43
- @lock_stack_item ||= LockStackItem.new(lock_str, shared)
44
- end
45
-
46
- def self.lock_stack
47
- # access doesn't need to be synchronized as it is only accessed by the current thread.
48
- Thread.current[:with_advisory_lock_stack] ||= []
49
- end
50
- delegate :lock_stack, to: 'self.class'
51
-
52
- def already_locked?
53
- lock_stack.include? lock_stack_item
54
- end
55
-
56
- def with_advisory_lock_if_needed(&block)
57
- if disable_query_cache
58
- return lock_and_yield do
59
- ActiveRecord::Base.uncached(&block)
60
- end
61
- end
62
-
63
- lock_and_yield(&block)
64
- end
65
-
66
- def lock_and_yield(&block)
67
- if already_locked?
68
- Result.new(true, yield)
69
- elsif timeout_seconds == 0
70
- yield_with_lock(&block)
71
- else
72
- yield_with_lock_and_timeout(&block)
73
- end
74
- end
75
-
76
- def stable_hashcode(input)
77
- if input.is_a? Numeric
78
- input.to_i
79
- else
80
- # Ruby MRI's String#hash is randomly seeded as of Ruby 1.9 so
81
- # make sure we use a deterministic hash.
82
- Zlib.crc32(input.to_s, 0)
83
- end
84
- end
85
-
86
- def yield_with_lock_and_timeout(&block)
87
- give_up_at = Time.now + @timeout_seconds if @timeout_seconds
88
- while @timeout_seconds.nil? || Time.now < give_up_at
89
- r = yield_with_lock(&block)
90
- return r if r.lock_was_acquired?
91
-
92
- # Randomizing sleep time may help reduce contention.
93
- sleep(rand(0.05..0.15))
94
- end
95
- FAILED_TO_LOCK
96
- end
97
-
98
- def yield_with_lock
99
- if try_lock
100
- begin
101
- lock_stack.push(lock_stack_item)
102
- result = block_given? ? yield : nil
103
- Result.new(true, result)
104
- ensure
105
- lock_stack.pop
106
- release_lock
107
- end
108
- else
109
- FAILED_TO_LOCK
110
- end
111
- end
112
-
113
- # Prevent AR from caching results improperly
114
- def unique_column_name
115
- "t#{SecureRandom.hex}"
116
- end
117
- end
118
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WithAdvisoryLock
4
- class DatabaseAdapterSupport
5
- # Caches nested lock support by MySQL reported version
6
- @@mysql_nl_cache = {}
7
- @@mysql_nl_cache_mutex = Mutex.new
8
-
9
- def initialize(connection)
10
- @connection = connection
11
- @sym_name = connection.adapter_name.downcase.to_sym
12
- end
13
-
14
- def mysql?
15
- %i[mysql2 trilogy].include? @sym_name
16
- end
17
-
18
- def postgresql?
19
- %i[postgresql empostgresql postgis].include? @sym_name
20
- end
21
-
22
- def sqlite?
23
- @sym_name == :sqlite3
24
- end
25
- end
26
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
-
5
- module WithAdvisoryLock
6
- class Flock < Base
7
- def filename
8
- @filename ||= begin
9
- safe = lock_str.to_s.gsub(/[^a-z0-9]/i, '')
10
- fn = ".lock-#{safe}-#{stable_hashcode(lock_str)}"
11
- # Let the user specify a directory besides CWD.
12
- ENV['FLOCK_DIR'] ? File.expand_path(fn, ENV['FLOCK_DIR']) : fn
13
- end
14
- end
15
-
16
- def file_io
17
- @file_io ||= begin
18
- FileUtils.touch(filename)
19
- File.open(filename, 'r+')
20
- end
21
- end
22
-
23
- def try_lock
24
- raise ArgumentError, 'transaction level locks are not supported on SQLite' if transaction
25
-
26
- 0 == file_io.flock((shared ? File::LOCK_SH : File::LOCK_EX) | File::LOCK_NB)
27
- end
28
-
29
- def release_lock
30
- 0 == file_io.flock(File::LOCK_UN)
31
- end
32
- end
33
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WithAdvisoryLock
4
- class MySQL < Base
5
- # See https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
6
- def try_lock
7
- raise ArgumentError, 'shared locks are not supported on MySQL' if shared
8
- raise ArgumentError, 'transaction level locks are not supported on MySQL' if transaction
9
-
10
- execute_successful?("GET_LOCK(#{quoted_lock_str}, 0)")
11
- end
12
-
13
- def release_lock
14
- execute_successful?("RELEASE_LOCK(#{quoted_lock_str})")
15
- end
16
-
17
- def execute_successful?(mysql_function)
18
- sql = "SELECT #{mysql_function} AS #{unique_column_name}"
19
- connection.select_value(sql).to_i.positive?
20
- end
21
-
22
- # MySQL wants a string as the lock key.
23
- def quoted_lock_str
24
- connection.quote(lock_str)
25
- end
26
- end
27
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WithAdvisoryLock
4
- class PostgreSQL < Base
5
- # See http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
6
- def try_lock
7
- pg_function = "pg_try_advisory#{transaction ? '_xact' : ''}_lock#{shared ? '_shared' : ''}"
8
- execute_successful?(pg_function)
9
- end
10
-
11
- def release_lock
12
- return if transaction
13
-
14
- pg_function = "pg_advisory_unlock#{shared ? '_shared' : ''}"
15
- execute_successful?(pg_function)
16
- rescue ActiveRecord::StatementInvalid => e
17
- raise unless e.message =~ / ERROR: +current transaction is aborted,/
18
-
19
- begin
20
- connection.rollback_db_transaction
21
- execute_successful?(pg_function)
22
- ensure
23
- connection.begin_db_transaction
24
- end
25
- end
26
-
27
- def execute_successful?(pg_function)
28
- comment = lock_name.to_s.gsub(%r{(/\*)|(\*/)}, '--')
29
- sql = "SELECT #{pg_function}(#{lock_keys.join(',')}) AS #{unique_column_name} /* #{comment} */"
30
- result = connection.select_value(sql)
31
- # MRI returns 't', jruby returns true. YAY!
32
- ['t', true].include?(result)
33
- end
34
-
35
- # PostgreSQL wants 2 32bit integers as the lock key.
36
- def lock_keys
37
- @lock_keys ||= [stable_hashcode(lock_name), ENV['WITH_ADVISORY_LOCK_PREFIX']].map do |ea|
38
- # pg advisory args must be 31 bit ints
39
- ea.to_i & 0x7fffffff
40
- end
41
- end
42
- end
43
- end
data/test/concern_test.rb DELETED
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class WithAdvisoryLockConcernTest < GemTestCase
6
- test 'adds with_advisory_lock to ActiveRecord classes' do
7
- assert_respond_to(Tag, :with_advisory_lock)
8
- end
9
-
10
- test 'adds with_advisory_lock to ActiveRecord instances' do
11
- assert_respond_to(Label.new, :with_advisory_lock)
12
- end
13
-
14
- test 'adds advisory_lock_exists? to ActiveRecord classes' do
15
- assert_respond_to(Tag, :advisory_lock_exists?)
16
- end
17
-
18
- test 'adds advisory_lock_exists? to ActiveRecord instances' do
19
- assert_respond_to(Label.new, :advisory_lock_exists?)
20
- end
21
- end
22
-
23
- class ActiveRecordQueryCacheTest < GemTestCase
24
- test 'does not disable quary cache by default' do
25
- ActiveRecord::Base.expects(:uncached).never
26
- Tag.with_advisory_lock('lock') { Tag.first }
27
- end
28
-
29
- test 'can disable ActiveRecord query cache' do
30
- ActiveRecord::Base.expects(:uncached).once
31
- Tag.with_advisory_lock('a-lock', disable_query_cache: true) { Tag.first }
32
- end
33
- end
data/test/lock_test.rb DELETED
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class LockTest < GemTestCase
6
- setup do
7
- @lock_name = 'test lock'
8
- @return_val = 1900
9
- end
10
-
11
- test 'returns nil outside an advisory lock request' do
12
- assert_nil(Tag.current_advisory_lock)
13
- end
14
-
15
- test 'returns the name of the last lock acquired' do
16
- Tag.with_advisory_lock(@lock_name) do
17
- assert_match(/#{@lock_name}/, Tag.current_advisory_lock)
18
- end
19
- end
20
-
21
- test 'can obtain a lock with a name that attempts to disrupt a SQL comment' do
22
- dangerous_lock_name = 'test */ lock /*'
23
- Tag.with_advisory_lock(dangerous_lock_name) do
24
- assert_match(/#{Regexp.escape(dangerous_lock_name)}/, Tag.current_advisory_lock)
25
- end
26
- end
27
-
28
- test 'returns false for an unacquired lock' do
29
- refute(Tag.advisory_lock_exists?(@lock_name))
30
- end
31
-
32
- test 'returns true for an acquired lock' do
33
- Tag.with_advisory_lock(@lock_name) do
34
- assert(Tag.advisory_lock_exists?(@lock_name))
35
- end
36
- end
37
-
38
- test 'returns block return value if lock successful' do
39
- assert_equal(@return_val, Tag.with_advisory_lock!(@lock_name) { @return_val })
40
- end
41
-
42
- test 'returns false on lock acquisition failure' do
43
- thread_with_lock = Thread.new do
44
- Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) do
45
- @locked_elsewhere = true
46
- loop { sleep 0.01 }
47
- end
48
- end
49
-
50
- sleep 0.01 until @locked_elsewhere
51
- assert_not(Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) { @return_val })
52
-
53
- thread_with_lock.kill
54
- end
55
-
56
- test 'raises an error on lock acquisition failure' do
57
- thread_with_lock = Thread.new do
58
- Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) do
59
- @locked_elsewhere = true
60
- loop { sleep 0.01 }
61
- end
62
- end
63
-
64
- sleep 0.01 until @locked_elsewhere
65
- assert_raises(WithAdvisoryLock::FailedToAcquireLock) do
66
- Tag.with_advisory_lock!(@lock_name, timeout_seconds: 0) { @return_val }
67
- end
68
-
69
- thread_with_lock.kill
70
- end
71
-
72
- test 'attempts the lock exactly once with no timeout' do
73
- expected = SecureRandom.base64
74
- actual = Tag.with_advisory_lock(@lock_name, 0) do
75
- expected
76
- end
77
-
78
- assert_equal(expected, actual)
79
- end
80
- end
data/test/nesting_test.rb DELETED
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class LockNestingTest < GemTestCase
6
- setup do
7
- @prior_prefix = ENV['WITH_ADVISORY_LOCK_PREFIX']
8
- ENV['WITH_ADVISORY_LOCK_PREFIX'] = nil
9
- end
10
-
11
- teardown do
12
- ENV['WITH_ADVISORY_LOCK_PREFIX'] = @prior_prefix
13
- end
14
-
15
- test "doesn't request the same lock twice" do
16
- impl = WithAdvisoryLock::Base.new(nil, nil, nil)
17
- assert_empty(impl.lock_stack)
18
- Tag.with_advisory_lock('first') do
19
- assert_equal(%w[first], impl.lock_stack.map(&:name))
20
- # Even MySQL should be OK with this:
21
- Tag.with_advisory_lock('first') do
22
- assert_equal(%w[first], impl.lock_stack.map(&:name))
23
- end
24
- assert_equal(%w[first], impl.lock_stack.map(&:name))
25
- end
26
- assert_empty(impl.lock_stack)
27
- end
28
- end
data/test/options_test.rb DELETED
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class OptionsParsingTest < GemTestCase
6
- def parse_options(options)
7
- WithAdvisoryLock::Base.new(mock, mock, options)
8
- end
9
-
10
- test 'defaults (empty hash)' do
11
- impl = parse_options({})
12
- assert_nil(impl.timeout_seconds)
13
- assert_not(impl.shared)
14
- assert_not(impl.transaction)
15
- end
16
-
17
- test 'nil sets timeout to nil' do
18
- impl = parse_options(nil)
19
- assert_nil(impl.timeout_seconds)
20
- assert_not(impl.shared)
21
- assert_not(impl.transaction)
22
- end
23
-
24
- test 'integer sets timeout to value' do
25
- impl = parse_options(42)
26
- assert_equal(42, impl.timeout_seconds)
27
- assert_not(impl.shared)
28
- assert_not(impl.transaction)
29
- end
30
-
31
- test 'hash with invalid key errors' do
32
- assert_raises(ArgumentError) do
33
- parse_options(foo: 42)
34
- end
35
- end
36
-
37
- test 'hash with timeout_seconds sets timeout to value' do
38
- impl = parse_options(timeout_seconds: 123)
39
- assert_equal(123, impl.timeout_seconds)
40
- assert_not(impl.shared)
41
- assert_not(impl.transaction)
42
- end
43
-
44
- test 'hash with shared option sets shared to true' do
45
- impl = parse_options(shared: true)
46
- assert_nil(impl.timeout_seconds)
47
- assert(impl.shared)
48
- assert_not(impl.transaction)
49
- end
50
-
51
- test 'hash with transaction option set transaction to true' do
52
- impl = parse_options(transaction: true)
53
- assert_nil(impl.timeout_seconds)
54
- assert_not(impl.shared)
55
- assert(impl.transaction)
56
- end
57
-
58
- test 'hash with multiple keys sets options' do
59
- foo = mock
60
- bar = mock
61
- impl = parse_options(timeout_seconds: foo, shared: bar)
62
- assert_equal(foo, impl.timeout_seconds)
63
- assert_equal(bar, impl.shared)
64
- assert_not(impl.transaction)
65
- end
66
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
- require 'forwardable'
5
-
6
- class FindOrCreateWorker
7
- extend Forwardable
8
- def_delegators :@thread, :join, :wakeup, :status, :to_s
9
-
10
- def initialize(name, use_advisory_lock)
11
- @name = name
12
- @use_advisory_lock = use_advisory_lock
13
- @thread = Thread.new { work_later }
14
- end
15
-
16
- def work_later
17
- sleep
18
- ActiveRecord::Base.connection_pool.with_connection do
19
- if @use_advisory_lock
20
- Tag.with_advisory_lock(@name) { work }
21
- else
22
- work
23
- end
24
- end
25
- end
26
-
27
- def work
28
- Tag.transaction do
29
- Tag.where(name: @name).first_or_create
30
- end
31
- end
32
- end
33
-
34
- class ParallelismTest < GemTestCase
35
- def run_workers
36
- @names = @iterations.times.map { |iter| "iteration ##{iter}" }
37
- @names.each do |name|
38
- workers = @workers.times.map do
39
- FindOrCreateWorker.new(name, @use_advisory_lock)
40
- end
41
- # Wait for all the threads to get ready:
42
- sleep(0.1) until workers.all? { |ea| ea.status == 'sleep' }
43
- # OK, GO!
44
- workers.each(&:wakeup)
45
- # Then wait for them to finish:
46
- workers.each(&:join)
47
- end
48
- # Ensure we're still connected:
49
- ActiveRecord::Base.connection_pool.connection
50
- end
51
-
52
- setup do
53
- ActiveRecord::Base.connection.reconnect!
54
- @workers = 10
55
- end
56
-
57
- test 'creates multiple duplicate rows without advisory locks' do
58
- skip if %i[sqlite3 jdbcsqlite3].include?(env_db)
59
- @use_advisory_lock = false
60
- @iterations = 1
61
- run_workers
62
- assert_operator(Tag.all.size, :>, @iterations) # <- any duplicated rows will make me happy.
63
- assert_operator(TagAudit.all.size, :>, @iterations) # <- any duplicated rows will make me happy.
64
- assert_operator(Label.all.size, :>, @iterations) # <- any duplicated rows will make me happy.
65
- end
66
-
67
- test "doesn't create multiple duplicate rows with advisory locks" do
68
- @use_advisory_lock = true
69
- @iterations = 10
70
- run_workers
71
- assert_equal(@iterations, Tag.all.size) # <- any duplicated rows will NOT make me happy.
72
- assert_equal(@iterations, TagAudit.all.size) # <- any duplicated rows will NOT make me happy.
73
- assert_equal(@iterations, Label.all.size) # <- any duplicated rows will NOT make me happy.
74
- end
75
- end
data/test/shared_test.rb DELETED
@@ -1,134 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
- class SharedTestWorker
5
- def initialize(shared)
6
- @shared = shared
7
-
8
- @locked = nil
9
- @cleanup = false
10
- @thread = Thread.new { work }
11
- end
12
-
13
- def locked?
14
- sleep 0.01 while @locked.nil? && @thread.alive?
15
- @locked
16
- end
17
-
18
- def cleanup!
19
- @cleanup = true
20
- @thread.join
21
- raise if @thread.status.nil?
22
- end
23
-
24
- private
25
-
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
30
- sleep 0.01 until @cleanup
31
- end
32
- @locked = false
33
- sleep 0.01 until @cleanup
34
- end
35
- end
36
- end
37
-
38
- class SharedLocksTest < GemTestCase
39
- def supported?
40
- %i[trilogy mysql2 jdbcmysql].exclude?(env_db)
41
- end
42
-
43
- test 'does not allow two exclusive locks' do
44
- one = SharedTestWorker.new(false)
45
- assert_predicate(one, :locked?)
46
-
47
- two = SharedTestWorker.new(false)
48
- refute(two.locked?)
49
-
50
- one.cleanup!
51
- two.cleanup!
52
- end
53
- end
54
-
55
- class NotSupportedEnvironmentTest < SharedLocksTest
56
- setup do
57
- skip if supported?
58
- end
59
-
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!
66
- end
67
-
68
- assert_match(/#{Regexp.escape('not supported')}/, exception.message)
69
- end
70
- end
71
-
72
- class SupportedEnvironmentTest < SharedLocksTest
73
- setup do
74
- skip unless supported?
75
- end
76
-
77
- test 'does allow two shared locks' do
78
- one = SharedTestWorker.new(true)
79
- assert_predicate(one, :locked?)
80
-
81
- two = SharedTestWorker.new(true)
82
- assert_predicate(two, :locked?)
83
-
84
- one.cleanup!
85
- two.cleanup!
86
- end
87
-
88
- test 'does not allow exclusive lock with shared lock' do
89
- one = SharedTestWorker.new(true)
90
- assert_predicate(one, :locked?)
91
-
92
- two = SharedTestWorker.new(false)
93
- refute(two.locked?)
94
-
95
- three = SharedTestWorker.new(true)
96
- assert_predicate(three, :locked?)
97
-
98
- one.cleanup!
99
- two.cleanup!
100
- three.cleanup!
101
- end
102
-
103
- test 'does not allow shared lock with exclusive lock' do
104
- one = SharedTestWorker.new(false)
105
- assert_predicate(one, :locked?)
106
-
107
- two = SharedTestWorker.new(true)
108
- refute(two.locked?)
109
-
110
- one.cleanup!
111
- two.cleanup!
112
- end
113
-
114
- class PostgreSQLTest < SupportedEnvironmentTest
115
- setup do
116
- skip unless env_db == :postgresql
117
- end
118
-
119
- def pg_lock_modes
120
- ActiveRecord::Base.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';")
121
- end
122
-
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)
129
- end
130
- end
131
- assert_empty(pg_lock_modes)
132
- end
133
- end
134
- end