with_advisory_lock 5.3.0 → 7.0.1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +76 -0
  3. data/.gitignore +2 -2
  4. data/.release-please-manifest.json +1 -1
  5. data/CHANGELOG.md +39 -0
  6. data/Gemfile +31 -0
  7. data/Makefile +8 -12
  8. data/README.md +7 -35
  9. data/Rakefile +5 -2
  10. data/bin/console +11 -0
  11. data/bin/rails +15 -0
  12. data/bin/sanity +20 -0
  13. data/bin/sanity_check +86 -0
  14. data/bin/setup +8 -0
  15. data/bin/setup_test_db +59 -0
  16. data/bin/test_connections +22 -0
  17. data/docker-compose.yml +3 -4
  18. data/lib/with_advisory_lock/concern.rb +26 -19
  19. data/lib/with_advisory_lock/core_advisory.rb +110 -0
  20. data/lib/with_advisory_lock/jruby_adapter.rb +29 -0
  21. data/lib/with_advisory_lock/lock_stack_item.rb +6 -0
  22. data/lib/with_advisory_lock/mysql_advisory.rb +71 -0
  23. data/lib/with_advisory_lock/postgresql_advisory.rb +112 -0
  24. data/lib/with_advisory_lock/result.rb +14 -0
  25. data/lib/with_advisory_lock/version.rb +1 -1
  26. data/lib/with_advisory_lock.rb +38 -10
  27. data/test/dummy/Rakefile +8 -0
  28. data/test/dummy/app/controllers/application_controller.rb +7 -0
  29. data/test/dummy/app/models/application_record.rb +6 -0
  30. data/test/dummy/app/models/label.rb +4 -0
  31. data/test/dummy/app/models/mysql_label.rb +5 -0
  32. data/test/dummy/app/models/mysql_record.rb +6 -0
  33. data/test/dummy/app/models/mysql_tag.rb +10 -0
  34. data/test/dummy/app/models/mysql_tag_audit.rb +5 -0
  35. data/test/dummy/app/models/tag.rb +8 -0
  36. data/test/dummy/app/models/tag_audit.rb +4 -0
  37. data/test/dummy/config/application.rb +31 -0
  38. data/test/dummy/config/boot.rb +3 -0
  39. data/test/dummy/config/database.yml +13 -0
  40. data/test/dummy/config/environment.rb +7 -0
  41. data/test/dummy/config/routes.rb +4 -0
  42. data/test/dummy/config.ru +6 -0
  43. data/test/{test_models.rb → dummy/db/schema.rb} +2 -17
  44. data/test/dummy/db/secondary_schema.rb +15 -0
  45. data/test/dummy/lib/tasks/db.rake +40 -0
  46. data/test/sanity_check_test.rb +63 -0
  47. data/test/test_helper.rb +14 -47
  48. data/test/with_advisory_lock/concern_test.rb +58 -12
  49. data/test/with_advisory_lock/lock_test.rb +159 -73
  50. data/test/with_advisory_lock/multi_adapter_test.rb +17 -0
  51. data/test/with_advisory_lock/mysql_release_lock_test.rb +119 -0
  52. data/test/with_advisory_lock/parallelism_test.rb +63 -37
  53. data/test/with_advisory_lock/postgresql_race_condition_test.rb +118 -0
  54. data/test/with_advisory_lock/shared_test.rb +52 -57
  55. data/test/with_advisory_lock/thread_test.rb +64 -42
  56. data/test/with_advisory_lock/transaction_test.rb +55 -40
  57. data/with_advisory_lock.gemspec +25 -5
  58. metadata +55 -50
  59. data/.github/workflows/ci-mysql5.yml +0 -61
  60. data/.github/workflows/ci-mysql8.yml +0 -62
  61. data/.github/workflows/ci-postgresql.yml +0 -64
  62. data/.github/workflows/ci-sqlite3.yml +0 -54
  63. data/Appraisals +0 -45
  64. data/gemfiles/activerecord_6.1.gemfile +0 -21
  65. data/gemfiles/activerecord_7.0.gemfile +0 -21
  66. data/gemfiles/activerecord_7.1.gemfile +0 -14
  67. data/lib/with_advisory_lock/base.rb +0 -118
  68. data/lib/with_advisory_lock/database_adapter_support.rb +0 -23
  69. data/lib/with_advisory_lock/flock.rb +0 -33
  70. data/lib/with_advisory_lock/mysql.rb +0 -32
  71. data/lib/with_advisory_lock/postgresql.rb +0 -66
  72. data/test/with_advisory_lock/base_test.rb +0 -9
  73. data/test/with_advisory_lock/nesting_test.rb +0 -28
  74. data/test/with_advisory_lock/options_test.rb +0 -66
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WithAdvisoryLock
4
- class DatabaseAdapterSupport
5
- attr_reader :adapter_name
6
- def initialize(connection)
7
- @connection = connection
8
- @adapter_name = connection.adapter_name.downcase.to_sym
9
- end
10
-
11
- def mysql?
12
- %i[mysql2 trilogy].include? adapter_name
13
- end
14
-
15
- def postgresql?
16
- %i[postgresql empostgresql postgis].include? adapter_name
17
- end
18
-
19
- def sqlite?
20
- [:sqlite3, :sqlite].include? adapter_name
21
- end
22
- end
23
- 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,32 +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/locking-functions.html
6
- # See https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html
7
- def try_lock
8
- raise ArgumentError, 'shared locks are not supported on MySQL' if shared
9
- raise ArgumentError, 'transaction level locks are not supported on MySQL' if transaction
10
-
11
- execute_successful?("GET_LOCK(#{quoted_lock_str}, 0)")
12
- end
13
-
14
- def release_lock
15
- execute_successful?("RELEASE_LOCK(#{quoted_lock_str})")
16
- end
17
-
18
- def execute_successful?(mysql_function)
19
- execute_query(mysql_function) == 1
20
- end
21
-
22
- def execute_query(mysql_function)
23
- sql = "SELECT #{mysql_function}"
24
- connection.query_value(sql)
25
- end
26
-
27
- # MySQL wants a string as the lock key.
28
- def quoted_lock_str
29
- connection.quote(lock_str)
30
- end
31
- end
32
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WithAdvisoryLock
4
- class PostgreSQL < Base
5
- # See https://www.postgresql.org/docs/16/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
6
-
7
- # MRI returns 't', jruby returns true. YAY!
8
- LOCK_RESULT_VALUES = ['t', true].freeze
9
- PG_ADVISORY_UNLOCK = 'pg_advisory_unlock'
10
- PG_TRY_ADVISORY = 'pg_try_advisory'
11
- ERROR_MESSAGE_REGEX = / ERROR: +current transaction is aborted,/
12
-
13
- def try_lock
14
- execute_successful?(advisory_try_lock_function(transaction))
15
- end
16
-
17
- def release_lock
18
- return if transaction
19
-
20
- execute_successful?(advisory_unlock_function)
21
- rescue ActiveRecord::StatementInvalid => e
22
- raise unless e.message =~ ERROR_MESSAGE_REGEX
23
-
24
- begin
25
- connection.rollback_db_transaction
26
- execute_successful?(advisory_unlock_function)
27
- ensure
28
- connection.begin_db_transaction
29
- end
30
- end
31
-
32
- def advisory_try_lock_function(transaction_scope)
33
- [
34
- 'pg_try_advisory',
35
- transaction_scope ? '_xact' : nil,
36
- '_lock',
37
- shared ? '_shared' : nil
38
- ].compact.join
39
- end
40
-
41
- def advisory_unlock_function
42
- [
43
- 'pg_advisory_unlock',
44
- shared ? '_shared' : nil
45
- ].compact.join
46
- end
47
-
48
- def execute_successful?(pg_function)
49
- result = connection.select_value(prepare_sql(pg_function))
50
- LOCK_RESULT_VALUES.include?(result)
51
- end
52
-
53
- def prepare_sql(pg_function)
54
- comment = lock_name.to_s.gsub(%r{(/\*)|(\*/)}, '--')
55
- "SELECT #{pg_function}(#{lock_keys.join(',')}) AS #{unique_column_name} /* #{comment} */"
56
- end
57
-
58
- # PostgreSQL wants 2 32bit integers as the lock key.
59
- def lock_keys
60
- @lock_keys ||= [
61
- stable_hashcode(lock_name),
62
- ENV[LOCK_PREFIX_ENV]
63
- ].map { |ea| ea.to_i & 0x7fffffff }
64
- end
65
- end
66
- end
@@ -1,9 +0,0 @@
1
- require 'test_helper'
2
-
3
- class WithAdvisoryLockBaseTest < GemTestCase
4
- test 'should support advisory_locks_enabled' do
5
- skip if is_sqlite3_adapter?
6
-
7
- assert Tag.connection.advisory_locks_enabled?
8
- end
9
- end
@@ -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
@@ -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