with_advisory_lock 5.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +76 -0
- data/.gitignore +2 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +32 -0
- data/Gemfile +31 -0
- data/Makefile +8 -12
- data/README.md +7 -35
- data/Rakefile +5 -2
- data/bin/console +11 -0
- data/bin/rails +15 -0
- data/bin/sanity +20 -0
- data/bin/sanity_check +86 -0
- data/bin/setup +8 -0
- data/bin/setup_test_db +59 -0
- data/bin/test_connections +22 -0
- data/docker-compose.yml +3 -4
- data/lib/with_advisory_lock/concern.rb +26 -19
- data/lib/with_advisory_lock/core_advisory.rb +110 -0
- data/lib/with_advisory_lock/jruby_adapter.rb +29 -0
- data/lib/with_advisory_lock/lock_stack_item.rb +6 -0
- data/lib/with_advisory_lock/mysql_advisory.rb +62 -0
- data/lib/with_advisory_lock/postgresql_advisory.rb +112 -0
- data/lib/with_advisory_lock/result.rb +14 -0
- data/lib/with_advisory_lock/version.rb +1 -1
- data/lib/with_advisory_lock.rb +38 -10
- data/test/dummy/Rakefile +8 -0
- data/test/dummy/app/controllers/application_controller.rb +7 -0
- data/test/dummy/app/models/application_record.rb +6 -0
- data/test/dummy/app/models/label.rb +4 -0
- data/test/dummy/app/models/mysql_label.rb +5 -0
- data/test/dummy/app/models/mysql_record.rb +6 -0
- data/test/dummy/app/models/mysql_tag.rb +10 -0
- data/test/dummy/app/models/mysql_tag_audit.rb +5 -0
- data/test/dummy/app/models/tag.rb +8 -0
- data/test/dummy/app/models/tag_audit.rb +4 -0
- data/test/dummy/config/application.rb +31 -0
- data/test/dummy/config/boot.rb +3 -0
- data/test/dummy/config/database.yml +13 -0
- data/test/dummy/config/environment.rb +7 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config.ru +6 -0
- data/test/{test_models.rb → dummy/db/schema.rb} +2 -17
- data/test/dummy/db/secondary_schema.rb +15 -0
- data/test/dummy/lib/tasks/db.rake +40 -0
- data/test/sanity_check_test.rb +63 -0
- data/test/test_helper.rb +14 -47
- data/test/with_advisory_lock/concern_test.rb +58 -12
- data/test/with_advisory_lock/lock_test.rb +159 -73
- data/test/with_advisory_lock/multi_adapter_test.rb +17 -0
- data/test/with_advisory_lock/parallelism_test.rb +63 -37
- data/test/with_advisory_lock/postgresql_race_condition_test.rb +118 -0
- data/test/with_advisory_lock/shared_test.rb +52 -57
- data/test/with_advisory_lock/thread_test.rb +64 -42
- data/test/with_advisory_lock/transaction_test.rb +55 -40
- data/with_advisory_lock.gemspec +25 -5
- metadata +54 -50
- data/.github/workflows/ci-mysql5.yml +0 -61
- data/.github/workflows/ci-mysql8.yml +0 -62
- data/.github/workflows/ci-postgresql.yml +0 -64
- data/.github/workflows/ci-sqlite3.yml +0 -54
- data/Appraisals +0 -45
- data/gemfiles/activerecord_6.1.gemfile +0 -21
- data/gemfiles/activerecord_7.0.gemfile +0 -21
- data/gemfiles/activerecord_7.1.gemfile +0 -14
- data/lib/with_advisory_lock/base.rb +0 -118
- data/lib/with_advisory_lock/database_adapter_support.rb +0 -23
- data/lib/with_advisory_lock/flock.rb +0 -33
- data/lib/with_advisory_lock/mysql.rb +0 -32
- data/lib/with_advisory_lock/postgresql.rb +0 -66
- data/test/with_advisory_lock/base_test.rb +0 -9
- data/test/with_advisory_lock/nesting_test.rb +0 -28
- data/test/with_advisory_lock/options_test.rb +0 -66
@@ -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,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
|