with_advisory_lock 4.6.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +76 -0
- data/.github/workflows/release.yml +17 -0
- data/.gitignore +2 -0
- data/.release-please-manifest.json +1 -0
- data/.ruby-version +2 -0
- data/.tool-versions +1 -1
- data/CHANGELOG.md +89 -0
- data/Gemfile +22 -3
- data/LICENSE.txt +4 -4
- data/Makefile +10 -0
- data/README.md +22 -39
- 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 +19 -0
- data/lib/with_advisory_lock/concern.rb +37 -30
- data/lib/with_advisory_lock/core_advisory.rb +110 -0
- data/lib/with_advisory_lock/failed_to_acquire_lock.rb +9 -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 +71 -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 +3 -1
- data/lib/with_advisory_lock.rb +38 -11
- data/release-please-config.json +9 -0
- 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/dummy/db/schema.rb +15 -0
- 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 +33 -0
- data/test/with_advisory_lock/concern_test.rb +79 -0
- data/test/with_advisory_lock/lock_test.rb +197 -0
- data/test/with_advisory_lock/multi_adapter_test.rb +17 -0
- data/test/with_advisory_lock/mysql_release_lock_test.rb +119 -0
- data/test/with_advisory_lock/parallelism_test.rb +101 -0
- data/test/with_advisory_lock/postgresql_race_condition_test.rb +118 -0
- data/test/with_advisory_lock/shared_test.rb +129 -0
- data/test/with_advisory_lock/thread_test.rb +83 -0
- data/test/with_advisory_lock/transaction_test.rb +83 -0
- data/with_advisory_lock.gemspec +54 -28
- metadata +83 -69
- data/.travis.yml +0 -38
- data/Appraisals +0 -29
- data/gemfiles/activerecord_4.2.gemfile +0 -19
- data/gemfiles/activerecord_5.0.gemfile +0 -19
- data/gemfiles/activerecord_5.1.gemfile +0 -19
- data/gemfiles/activerecord_5.2.gemfile +0 -19
- data/gemfiles/activerecord_6.0.gemfile +0 -19
- data/lib/with_advisory_lock/base.rb +0 -104
- data/lib/with_advisory_lock/database_adapter_support.rb +0 -63
- data/lib/with_advisory_lock/flock.rb +0 -32
- data/lib/with_advisory_lock/mysql.rb +0 -27
- data/lib/with_advisory_lock/mysql_no_nesting.rb +0 -20
- data/lib/with_advisory_lock/nested_advisory_lock_error.rb +0 -14
- data/lib/with_advisory_lock/postgresql.rb +0 -41
- data/test/concern_test.rb +0 -20
- data/test/database.yml +0 -17
- data/test/lock_test.rb +0 -47
- data/test/minitest_helper.rb +0 -40
- data/test/nesting_test.rb +0 -93
- data/test/options_test.rb +0 -64
- data/test/parallelism_test.rb +0 -77
- data/test/shared_test.rb +0 -131
- data/test/test_models.rb +0 -24
- data/test/thread_test.rb +0 -60
- data/test/transaction_test.rb +0 -70
- data/tests.sh +0 -11
@@ -1,41 +0,0 @@
|
|
1
|
-
module WithAdvisoryLock
|
2
|
-
class PostgreSQL < Base
|
3
|
-
# See http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
|
4
|
-
def try_lock
|
5
|
-
pg_function = "pg_try_advisory#{transaction ? '_xact' : ''}_lock#{shared ? '_shared' : ''}"
|
6
|
-
execute_successful?(pg_function)
|
7
|
-
end
|
8
|
-
|
9
|
-
def release_lock
|
10
|
-
return if transaction
|
11
|
-
pg_function = "pg_advisory_unlock#{shared ? '_shared' : ''}"
|
12
|
-
execute_successful?(pg_function)
|
13
|
-
rescue ActiveRecord::StatementInvalid => e
|
14
|
-
raise unless e.message =~ / ERROR: +current transaction is aborted,/
|
15
|
-
begin
|
16
|
-
connection.rollback_db_transaction
|
17
|
-
execute_successful?(pg_function)
|
18
|
-
ensure
|
19
|
-
connection.begin_db_transaction
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def execute_successful?(pg_function)
|
24
|
-
comment = lock_name.gsub(/(\/\*)|(\*\/)/, '--')
|
25
|
-
sql = "SELECT #{pg_function}(#{lock_keys.join(',')}) AS #{unique_column_name} /* #{comment} */"
|
26
|
-
result = connection.select_value(sql)
|
27
|
-
# MRI returns 't', jruby returns true. YAY!
|
28
|
-
(result == 't' || result == true)
|
29
|
-
end
|
30
|
-
|
31
|
-
# PostgreSQL wants 2 32bit integers as the lock key.
|
32
|
-
def lock_keys
|
33
|
-
@lock_keys ||= begin
|
34
|
-
[stable_hashcode(lock_name), ENV['WITH_ADVISORY_LOCK_PREFIX']].map do |ea|
|
35
|
-
# pg advisory args must be 31 bit ints
|
36
|
-
ea.to_i & 0x7fffffff
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
data/test/concern_test.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
require 'minitest_helper'
|
2
|
-
|
3
|
-
describe "with_advisory_lock.concern" do
|
4
|
-
it "adds with_advisory_lock to ActiveRecord classes" do
|
5
|
-
assert Tag.respond_to?(:with_advisory_lock)
|
6
|
-
end
|
7
|
-
|
8
|
-
it "adds with_advisory_lock to ActiveRecord instances" do
|
9
|
-
assert Label.new.respond_to?(:with_advisory_lock)
|
10
|
-
end
|
11
|
-
|
12
|
-
it "adds advisory_lock_exists? to ActiveRecord classes" do
|
13
|
-
assert Tag.respond_to?(:advisory_lock_exists?)
|
14
|
-
end
|
15
|
-
|
16
|
-
it "adds advisory_lock_exists? to ActiveRecord classes" do
|
17
|
-
assert Label.new.respond_to?(:advisory_lock_exists?)
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|
data/test/database.yml
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
sqlite:
|
2
|
-
adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
|
3
|
-
database: test/sqlite.db
|
4
|
-
timeout: 500
|
5
|
-
pool: 50
|
6
|
-
postgresql:
|
7
|
-
adapter: postgresql
|
8
|
-
username: postgres
|
9
|
-
database: with_advisory_lock_test
|
10
|
-
min_messages: ERROR
|
11
|
-
pool: 50
|
12
|
-
mysql:
|
13
|
-
adapter: mysql2
|
14
|
-
host: localhost
|
15
|
-
username: root
|
16
|
-
database: with_advisory_lock_test
|
17
|
-
pool: 50
|
data/test/lock_test.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
require 'minitest_helper'
|
2
|
-
|
3
|
-
describe 'class methods' do
|
4
|
-
let(:lock_name) { 'test lock' }
|
5
|
-
|
6
|
-
describe '.current_advisory_lock' do
|
7
|
-
it 'returns nil outside an advisory lock request' do
|
8
|
-
Tag.current_advisory_lock.must_be_nil
|
9
|
-
end
|
10
|
-
|
11
|
-
it 'returns the name of the last lock acquired' do
|
12
|
-
Tag.with_advisory_lock(lock_name) do
|
13
|
-
# The lock name may have a prefix if WITH_ADVISORY_LOCK_PREFIX env is set
|
14
|
-
Tag.current_advisory_lock.must_match(/#{lock_name}/)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
it 'can obtain a lock with a name that attempts to disrupt a SQL comment' do
|
19
|
-
dangerous_lock_name = 'test */ lock /*'
|
20
|
-
Tag.with_advisory_lock(dangerous_lock_name) do
|
21
|
-
Tag.current_advisory_lock.must_match(/#{Regexp.escape(dangerous_lock_name)}/)
|
22
|
-
end
|
23
|
-
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
describe '.advisory_lock_exists?' do
|
28
|
-
it 'returns false for an unacquired lock' do
|
29
|
-
Tag.advisory_lock_exists?(lock_name).must_be_false
|
30
|
-
end
|
31
|
-
|
32
|
-
it 'returns the name of the last lock acquired' do
|
33
|
-
Tag.with_advisory_lock(lock_name) do
|
34
|
-
Tag.advisory_lock_exists?(lock_name).must_be_true
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
describe 'zero timeout_seconds' do
|
40
|
-
it 'attempts the lock exactly once with no timeout' do
|
41
|
-
expected = SecureRandom.base64
|
42
|
-
Tag.with_advisory_lock(lock_name, 0) do
|
43
|
-
expected
|
44
|
-
end.must_equal expected
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
data/test/minitest_helper.rb
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
require 'erb'
|
2
|
-
require 'active_record'
|
3
|
-
require 'with_advisory_lock'
|
4
|
-
require 'tmpdir'
|
5
|
-
require 'securerandom'
|
6
|
-
|
7
|
-
def env_db
|
8
|
-
(ENV['DB'] || :mysql).to_sym
|
9
|
-
end
|
10
|
-
|
11
|
-
db_config = File.expand_path('database.yml', File.dirname(__FILE__))
|
12
|
-
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(db_config)).result)
|
13
|
-
|
14
|
-
ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex
|
15
|
-
|
16
|
-
ActiveRecord::Base.establish_connection(env_db)
|
17
|
-
ActiveRecord::Migration.verbose = false
|
18
|
-
|
19
|
-
require 'test_models'
|
20
|
-
begin
|
21
|
-
require 'minitest'
|
22
|
-
rescue LoadError
|
23
|
-
puts 'Failed to load the minitest gem; built-in version will be used.'
|
24
|
-
end
|
25
|
-
require 'minitest/autorun'
|
26
|
-
require 'minitest/great_expectations'
|
27
|
-
require 'mocha/setup'
|
28
|
-
|
29
|
-
class MiniTest::Spec
|
30
|
-
before do
|
31
|
-
ENV['FLOCK_DIR'] = Dir.mktmpdir
|
32
|
-
Tag.delete_all
|
33
|
-
TagAudit.delete_all
|
34
|
-
Label.delete_all
|
35
|
-
end
|
36
|
-
after do
|
37
|
-
FileUtils.remove_entry_secure ENV['FLOCK_DIR']
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
data/test/nesting_test.rb
DELETED
@@ -1,93 +0,0 @@
|
|
1
|
-
require 'minitest_helper'
|
2
|
-
|
3
|
-
describe "lock nesting" do
|
4
|
-
# This simplifies what we expect from the lock name:
|
5
|
-
before :each do
|
6
|
-
@prior_prefix = ENV['WITH_ADVISORY_LOCK_PREFIX']
|
7
|
-
ENV['WITH_ADVISORY_LOCK_PREFIX'] = nil
|
8
|
-
end
|
9
|
-
|
10
|
-
after :each do
|
11
|
-
ENV['WITH_ADVISORY_LOCK_PREFIX'] = @prior_prefix
|
12
|
-
end
|
13
|
-
|
14
|
-
it "doesn't request the same lock twice" do
|
15
|
-
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
16
|
-
impl.lock_stack.must_be_empty
|
17
|
-
Tag.with_advisory_lock("first") do
|
18
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
19
|
-
# Even MySQL should be OK with this:
|
20
|
-
Tag.with_advisory_lock("first") do
|
21
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
22
|
-
end
|
23
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
24
|
-
end
|
25
|
-
impl.lock_stack.must_be_empty
|
26
|
-
end
|
27
|
-
|
28
|
-
it "raises errors with MySQL < 5.7.5 when acquiring nested lock" do
|
29
|
-
skip unless env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
30
|
-
exc = proc {
|
31
|
-
Tag.with_advisory_lock("first") do
|
32
|
-
Tag.with_advisory_lock("second") do
|
33
|
-
end
|
34
|
-
end
|
35
|
-
}.must_raise WithAdvisoryLock::NestedAdvisoryLockError
|
36
|
-
exc.lock_stack.map(&:name).must_equal %w(first)
|
37
|
-
end
|
38
|
-
|
39
|
-
it "does not raise errors with MySQL < 5.7.5 when acquiring nested error force enabled" do
|
40
|
-
skip unless env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
41
|
-
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
42
|
-
impl.lock_stack.must_be_empty
|
43
|
-
Tag.with_advisory_lock("first", force_nested_lock_support: true) do
|
44
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
45
|
-
Tag.with_advisory_lock("second", force_nested_lock_support: true) do
|
46
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
47
|
-
Tag.with_advisory_lock("first", force_nested_lock_support: true) do
|
48
|
-
# Shouldn't ask for another lock:
|
49
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
50
|
-
Tag.with_advisory_lock("second", force_nested_lock_support: true) do
|
51
|
-
# Shouldn't ask for another lock:
|
52
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
57
|
-
end
|
58
|
-
impl.lock_stack.must_be_empty
|
59
|
-
end
|
60
|
-
|
61
|
-
it "supports nested advisory locks with !MySQL 5.6" do
|
62
|
-
skip if env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
63
|
-
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
64
|
-
impl.lock_stack.must_be_empty
|
65
|
-
Tag.with_advisory_lock("first") do
|
66
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
67
|
-
Tag.with_advisory_lock("second") do
|
68
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
69
|
-
Tag.with_advisory_lock("first") do
|
70
|
-
# Shouldn't ask for another lock:
|
71
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
72
|
-
Tag.with_advisory_lock("second") do
|
73
|
-
# Shouldn't ask for another lock:
|
74
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
79
|
-
end
|
80
|
-
impl.lock_stack.must_be_empty
|
81
|
-
end
|
82
|
-
|
83
|
-
it "raises with !MySQL 5.6 and nested error force disabled" do
|
84
|
-
skip unless env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
85
|
-
exc = proc {
|
86
|
-
Tag.with_advisory_lock("first", force_nested_lock_support: false) do
|
87
|
-
Tag.with_advisory_lock("second", force_nested_lock_support: false) do
|
88
|
-
end
|
89
|
-
end
|
90
|
-
}.must_raise WithAdvisoryLock::NestedAdvisoryLockError
|
91
|
-
exc.lock_stack.map(&:name).must_equal %w(first)
|
92
|
-
end
|
93
|
-
end
|
data/test/options_test.rb
DELETED
@@ -1,64 +0,0 @@
|
|
1
|
-
require 'minitest_helper'
|
2
|
-
|
3
|
-
describe 'options parsing' do
|
4
|
-
def parse_options(options)
|
5
|
-
WithAdvisoryLock::Base.new(mock, mock, options)
|
6
|
-
end
|
7
|
-
|
8
|
-
specify 'defaults (empty hash)' do
|
9
|
-
impl = parse_options({})
|
10
|
-
impl.timeout_seconds.must_be_nil
|
11
|
-
impl.shared.must_equal false
|
12
|
-
impl.transaction.must_equal false
|
13
|
-
end
|
14
|
-
|
15
|
-
specify 'nil sets timeout to nil' do
|
16
|
-
impl = parse_options(nil)
|
17
|
-
impl.timeout_seconds.must_be_nil
|
18
|
-
impl.shared.must_equal false
|
19
|
-
impl.transaction.must_equal false
|
20
|
-
end
|
21
|
-
|
22
|
-
specify 'integer sets timeout to value' do
|
23
|
-
impl = parse_options(42)
|
24
|
-
impl.timeout_seconds.must_equal 42
|
25
|
-
impl.shared.must_equal false
|
26
|
-
impl.transaction.must_equal false
|
27
|
-
end
|
28
|
-
|
29
|
-
specify 'hash with invalid key errors' do
|
30
|
-
proc {
|
31
|
-
parse_options(foo: 42)
|
32
|
-
}.must_raise ArgumentError
|
33
|
-
end
|
34
|
-
|
35
|
-
specify 'hash with timeout_seconds sets timeout to value' do
|
36
|
-
impl = parse_options(timeout_seconds: 123)
|
37
|
-
impl.timeout_seconds.must_equal 123
|
38
|
-
impl.shared.must_equal false
|
39
|
-
impl.transaction.must_equal false
|
40
|
-
end
|
41
|
-
|
42
|
-
specify 'hash with shared option sets shared to true' do
|
43
|
-
impl = parse_options(shared: true)
|
44
|
-
impl.timeout_seconds.must_be_nil
|
45
|
-
impl.shared.must_equal true
|
46
|
-
impl.transaction.must_equal false
|
47
|
-
end
|
48
|
-
|
49
|
-
specify 'hash with transaction option set transaction to true' do
|
50
|
-
impl = parse_options(transaction: true)
|
51
|
-
impl.timeout_seconds.must_be_nil
|
52
|
-
impl.shared.must_equal false
|
53
|
-
impl.transaction.must_equal true
|
54
|
-
end
|
55
|
-
|
56
|
-
specify 'hash with multiple keys sets options' do
|
57
|
-
foo = mock
|
58
|
-
bar = mock
|
59
|
-
impl = parse_options(timeout_seconds: foo, shared: bar)
|
60
|
-
impl.timeout_seconds.must_equal foo
|
61
|
-
impl.shared.must_equal bar
|
62
|
-
impl.transaction.must_equal false
|
63
|
-
end
|
64
|
-
end
|
data/test/parallelism_test.rb
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
require 'minitest_helper'
|
2
|
-
require 'forwardable'
|
3
|
-
|
4
|
-
describe 'parallelism' do
|
5
|
-
class FindOrCreateWorker
|
6
|
-
extend Forwardable
|
7
|
-
def_delegators :@thread, :join, :wakeup, :status, :to_s
|
8
|
-
|
9
|
-
def initialize(name, use_advisory_lock)
|
10
|
-
@name = name
|
11
|
-
@use_advisory_lock = use_advisory_lock
|
12
|
-
@thread = Thread.new { work_later }
|
13
|
-
end
|
14
|
-
|
15
|
-
def work_later
|
16
|
-
sleep
|
17
|
-
ActiveRecord::Base.connection_pool.with_connection do
|
18
|
-
if @use_advisory_lock
|
19
|
-
Tag.with_advisory_lock(@name) { work }
|
20
|
-
else
|
21
|
-
work
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def work
|
27
|
-
Tag.transaction do
|
28
|
-
Tag.where(name: @name).first_or_create
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def run_workers
|
34
|
-
@names = @iterations.times.map { |iter| "iteration ##{iter}" }
|
35
|
-
@names.each do |name|
|
36
|
-
workers = @workers.times.map do
|
37
|
-
FindOrCreateWorker.new(name, @use_advisory_lock)
|
38
|
-
end
|
39
|
-
# Wait for all the threads to get ready:
|
40
|
-
until workers.all? { |ea| ea.status == 'sleep' }
|
41
|
-
sleep(0.1)
|
42
|
-
end
|
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
|
-
before :each do
|
53
|
-
ActiveRecord::Base.connection.reconnect!
|
54
|
-
@workers = 10
|
55
|
-
end
|
56
|
-
|
57
|
-
# < SQLite, understandably, throws "The database file is locked (database is locked)"
|
58
|
-
|
59
|
-
it 'creates multiple duplicate rows without advisory locks' do
|
60
|
-
skip if env_db == :sqlite
|
61
|
-
@use_advisory_lock = false
|
62
|
-
@iterations = 1
|
63
|
-
run_workers
|
64
|
-
Tag.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
|
65
|
-
TagAudit.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
|
66
|
-
Label.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
|
67
|
-
end
|
68
|
-
|
69
|
-
it "doesn't create multiple duplicate rows with advisory locks" do
|
70
|
-
@use_advisory_lock = true
|
71
|
-
@iterations = 10
|
72
|
-
run_workers
|
73
|
-
Tag.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
74
|
-
TagAudit.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
75
|
-
Label.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
76
|
-
end
|
77
|
-
end
|
data/test/shared_test.rb
DELETED
@@ -1,131 +0,0 @@
|
|
1
|
-
require 'minitest_helper'
|
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
|
11
|
-
|
12
|
-
@locked = nil
|
13
|
-
@cleanup = false
|
14
|
-
@thread = Thread.new { work }
|
15
|
-
end
|
16
|
-
|
17
|
-
def locked?
|
18
|
-
sleep 0.01 while @locked.nil? && @thread.alive?
|
19
|
-
@locked
|
20
|
-
end
|
21
|
-
|
22
|
-
def cleanup!
|
23
|
-
@cleanup = true
|
24
|
-
@thread.join
|
25
|
-
raise if @thread.status.nil?
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
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
|
37
|
-
sleep 0.01 until @cleanup
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
it 'does not allow two exclusive locks' do
|
43
|
-
one = SharedTestWorker.new(false)
|
44
|
-
one.locked?.must_equal true
|
45
|
-
|
46
|
-
two = SharedTestWorker.new(false)
|
47
|
-
two.locked?.must_equal false
|
48
|
-
|
49
|
-
one.cleanup!
|
50
|
-
two.cleanup!
|
51
|
-
end
|
52
|
-
|
53
|
-
describe 'not supported' do
|
54
|
-
before do
|
55
|
-
skip if supported?
|
56
|
-
end
|
57
|
-
|
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'
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
describe 'supported' do
|
69
|
-
before do
|
70
|
-
skip unless supported?
|
71
|
-
end
|
72
|
-
|
73
|
-
it 'does allow two shared locks' do
|
74
|
-
one = SharedTestWorker.new(true)
|
75
|
-
one.locked?.must_equal true
|
76
|
-
|
77
|
-
two = SharedTestWorker.new(true)
|
78
|
-
two.locked?.must_equal true
|
79
|
-
|
80
|
-
one.cleanup!
|
81
|
-
two.cleanup!
|
82
|
-
end
|
83
|
-
|
84
|
-
it 'does not allow exclusive lock with shared lock' do
|
85
|
-
one = SharedTestWorker.new(true)
|
86
|
-
one.locked?.must_equal true
|
87
|
-
|
88
|
-
two = SharedTestWorker.new(false)
|
89
|
-
two.locked?.must_equal false
|
90
|
-
|
91
|
-
three = SharedTestWorker.new(true)
|
92
|
-
three.locked?.must_equal true
|
93
|
-
|
94
|
-
one.cleanup!
|
95
|
-
two.cleanup!
|
96
|
-
three.cleanup!
|
97
|
-
end
|
98
|
-
|
99
|
-
it 'does not allow shared lock with exclusive lock' do
|
100
|
-
one = SharedTestWorker.new(false)
|
101
|
-
one.locked?.must_equal true
|
102
|
-
|
103
|
-
two = SharedTestWorker.new(true)
|
104
|
-
two.locked?.must_equal false
|
105
|
-
|
106
|
-
one.cleanup!
|
107
|
-
two.cleanup!
|
108
|
-
end
|
109
|
-
|
110
|
-
describe 'PostgreSQL' do
|
111
|
-
before do
|
112
|
-
skip unless env_db == :postgresql
|
113
|
-
end
|
114
|
-
|
115
|
-
def pg_lock_modes
|
116
|
-
ActiveRecord::Base.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';")
|
117
|
-
end
|
118
|
-
|
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
|
126
|
-
end
|
127
|
-
pg_lock_modes.must_equal %w[]
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
data/test/test_models.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
ActiveRecord::Schema.define(:version => 0) do
|
2
|
-
create_table "tags", :force => true do |t|
|
3
|
-
t.string "name"
|
4
|
-
end
|
5
|
-
create_table "tag_audits", :id => false, :force => true do |t|
|
6
|
-
t.string "tag_name"
|
7
|
-
end
|
8
|
-
create_table "labels", :id => false, :force => true do |t|
|
9
|
-
t.string "name"
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
class Tag < ActiveRecord::Base
|
14
|
-
after_save do
|
15
|
-
TagAudit.create(tag_name: name)
|
16
|
-
Label.create(name: name)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
class TagAudit < ActiveRecord::Base
|
21
|
-
end
|
22
|
-
|
23
|
-
class Label < ActiveRecord::Base
|
24
|
-
end
|
data/test/thread_test.rb
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
require 'minitest_helper'
|
2
|
-
|
3
|
-
describe 'separate thread tests' do
|
4
|
-
let(:lock_name) { 'testing 1,2,3' } # OMG COMMAS
|
5
|
-
|
6
|
-
before do
|
7
|
-
@mutex = Mutex.new
|
8
|
-
@t1_acquired_lock = false
|
9
|
-
@t1_return_value = nil
|
10
|
-
|
11
|
-
@t1 = Thread.new do
|
12
|
-
ActiveRecord::Base.connection_pool.with_connection do
|
13
|
-
@t1_return_value = Label.with_advisory_lock(lock_name) do
|
14
|
-
@mutex.synchronize { @t1_acquired_lock = true }
|
15
|
-
sleep
|
16
|
-
't1 finished'
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
# Wait for the thread to acquire the lock:
|
22
|
-
until @mutex.synchronize { @t1_acquired_lock } do
|
23
|
-
sleep(0.1)
|
24
|
-
end
|
25
|
-
ActiveRecord::Base.connection.reconnect!
|
26
|
-
end
|
27
|
-
|
28
|
-
after do
|
29
|
-
@t1.wakeup if @t1.status == 'sleep'
|
30
|
-
@t1.join
|
31
|
-
end
|
32
|
-
|
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'
|
36
|
-
end
|
37
|
-
response.must_be_false
|
38
|
-
end
|
39
|
-
|
40
|
-
it '#with_advisory_lock yields to the provided block' do
|
41
|
-
@t1_acquired_lock.must_be_true
|
42
|
-
end
|
43
|
-
|
44
|
-
it '#advisory_lock_exists? returns true when another thread has the lock' do
|
45
|
-
Tag.advisory_lock_exists?(lock_name).must_be_true
|
46
|
-
end
|
47
|
-
|
48
|
-
it 'can re-establish the lock after the other thread releases it' do
|
49
|
-
@t1.wakeup
|
50
|
-
@t1.join
|
51
|
-
@t1_return_value.must_equal 't1 finished'
|
52
|
-
|
53
|
-
# We should now be able to acquire the lock immediately:
|
54
|
-
reacquired = false
|
55
|
-
Label.with_advisory_lock(lock_name, 0) do
|
56
|
-
reacquired = true
|
57
|
-
end.must_be_true
|
58
|
-
reacquired.must_be_true
|
59
|
-
end
|
60
|
-
end
|
data/test/transaction_test.rb
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
require 'minitest_helper'
|
2
|
-
|
3
|
-
describe 'transaction scoping' do
|
4
|
-
def supported?
|
5
|
-
env_db == :postgresql
|
6
|
-
end
|
7
|
-
|
8
|
-
describe 'not supported' do
|
9
|
-
before do
|
10
|
-
skip if supported?
|
11
|
-
end
|
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'
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
describe 'supported' do
|
26
|
-
before do
|
27
|
-
skip unless env_db == :postgresql
|
28
|
-
end
|
29
|
-
|
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
|
35
|
-
Tag.transaction do
|
36
|
-
pg_lock_count.must_equal 0
|
37
|
-
Tag.with_advisory_lock 'test' do
|
38
|
-
pg_lock_count.must_equal 1
|
39
|
-
end
|
40
|
-
pg_lock_count.must_equal 0
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
specify 'session locks release when transaction fails inside block' do
|
45
|
-
Tag.transaction do
|
46
|
-
pg_lock_count.must_equal 0
|
47
|
-
|
48
|
-
exception = proc {
|
49
|
-
Tag.with_advisory_lock 'test' do
|
50
|
-
Tag.connection.execute 'SELECT 1/0;'
|
51
|
-
end
|
52
|
-
}.must_raise ActiveRecord::StatementInvalid
|
53
|
-
exception.message.must_include 'division by zero'
|
54
|
-
|
55
|
-
pg_lock_count.must_equal 0
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
specify 'transaction level locks hold until the transaction completes' do
|
60
|
-
Tag.transaction do
|
61
|
-
pg_lock_count.must_equal 0
|
62
|
-
Tag.with_advisory_lock 'test', transaction: true do
|
63
|
-
pg_lock_count.must_equal 1
|
64
|
-
end
|
65
|
-
pg_lock_count.must_equal 1
|
66
|
-
end
|
67
|
-
pg_lock_count.must_equal 0
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|