with_advisory_lock 4.6.0 → 5.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 +80 -0
- data/.gitignore +2 -0
- data/.tool-versions +1 -1
- data/Appraisals +34 -18
- data/CHANGELOG.md +9 -0
- data/Gemfile +0 -12
- data/README.md +17 -6
- data/gemfiles/{activerecord_6.0.gemfile → activerecord_6.1.gemfile} +4 -2
- data/gemfiles/{activerecord_5.2.gemfile → activerecord_7.0.gemfile} +4 -2
- data/gemfiles/activerecord_7.1.gemfile +14 -0
- data/lib/with_advisory_lock/base.rb +16 -2
- data/lib/with_advisory_lock/concern.rb +16 -16
- data/lib/with_advisory_lock/database_adapter_support.rb +4 -41
- data/lib/with_advisory_lock/failed_to_acquire_lock.rb +7 -0
- data/lib/with_advisory_lock/flock.rb +4 -3
- data/lib/with_advisory_lock/mysql.rb +5 -5
- data/lib/with_advisory_lock/postgresql.rb +9 -7
- data/lib/with_advisory_lock/version.rb +3 -1
- data/lib/with_advisory_lock.rb +1 -2
- data/test/concern_test.rb +23 -10
- data/test/lock_test.rb +61 -28
- data/test/nesting_test.rb +14 -79
- data/test/options_test.rb +35 -33
- data/test/parallelism_test.rb +35 -37
- data/test/shared_test.rb +93 -90
- data/test/test_helper.rb +52 -0
- data/test/test_models.rb +9 -7
- data/test/thread_test.rb +23 -22
- data/test/transaction_test.rb +34 -36
- data/with_advisory_lock.gemspec +24 -23
- metadata +25 -41
- data/.travis.yml +0 -38
- 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/lib/with_advisory_lock/mysql_no_nesting.rb +0 -20
- data/lib/with_advisory_lock/nested_advisory_lock_error.rb +0 -14
- data/test/database.yml +0 -17
- data/test/minitest_helper.rb +0 -40
- data/tests.sh +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 784812e261b5a57a5d3da8c878e536949245b29372b40b2baf15a52d7c65b854
|
4
|
+
data.tar.gz: ba2f6fc8bf78c89d56635c4f56500117f79a443404d3574d8b54b2a45d1811de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb7dc11c1f206639943daa363a4d917bfe93792e7db764c7860ef68151e99defaa03f58640fae0480b566e3d4a0697695f5b60ae8af09838bb176979c328d8ae
|
7
|
+
data.tar.gz: 10132a0c47ed8070a0661a7b6ae0409be76ef14693be65c12a8e8cdd9c5a506ba25c60bda264f7c60bd92952e4cd4c5513a5e566a3e0d757f0ab6fc4311ffcd5
|
@@ -0,0 +1,80 @@
|
|
1
|
+
---
|
2
|
+
name: CI
|
3
|
+
|
4
|
+
on:
|
5
|
+
pull_request:
|
6
|
+
branches:
|
7
|
+
- master
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
minitest:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
services:
|
13
|
+
mysql:
|
14
|
+
image: mysql/mysql-server:5.7
|
15
|
+
ports:
|
16
|
+
- "3306:3306"
|
17
|
+
env:
|
18
|
+
MYSQL_ROOT_PASSWORD: root
|
19
|
+
MYSQL_DATABASE: with_advisory_lock_test
|
20
|
+
MYSQL_ROOT_HOST: '%'
|
21
|
+
postgres:
|
22
|
+
image: 'postgres:14-alpine'
|
23
|
+
ports: ['5432:5432']
|
24
|
+
env:
|
25
|
+
POSTGRES_USER: closure_tree
|
26
|
+
POSTGRES_PASSWORD: closure_tree
|
27
|
+
POSTGRES_DB: with_advisory_lock_test
|
28
|
+
options: >-
|
29
|
+
--health-cmd pg_isready
|
30
|
+
--health-interval 10s
|
31
|
+
--health-timeout 5s
|
32
|
+
--health-retries 5
|
33
|
+
|
34
|
+
strategy:
|
35
|
+
fail-fast: false
|
36
|
+
matrix:
|
37
|
+
ruby:
|
38
|
+
- '3.2'
|
39
|
+
- '3.1'
|
40
|
+
- '3.0'
|
41
|
+
- '2.7'
|
42
|
+
- 'truffleruby'
|
43
|
+
rails:
|
44
|
+
- activerecord_7.1
|
45
|
+
- activerecord_7.0
|
46
|
+
- activerecord_6.1
|
47
|
+
adapter:
|
48
|
+
- sqlite3:///tmp/test.sqlite3
|
49
|
+
- mysql2://root:root@0/with_advisory_lock_test
|
50
|
+
- trilogy://root:root@0/with_advisory_lock_test
|
51
|
+
- postgres://closure_tree:closure_tree@0/with_advisory_lock_test
|
52
|
+
include:
|
53
|
+
- ruby: jruby
|
54
|
+
rails: activerecord_6.1
|
55
|
+
adapter: jdbcmysql://root:root@0/with_advisory_lock_test
|
56
|
+
- ruby: jruby
|
57
|
+
rails: activerecord_6.1
|
58
|
+
adapter: jdbcsqlite3:///tmp/test.sqlite3
|
59
|
+
- ruby: jruby
|
60
|
+
rails: activerecord_6.1
|
61
|
+
adapter: jdbcpostgresql://closure_tree:closure_tree@0/with_advisory_lock_test
|
62
|
+
steps:
|
63
|
+
- name: Checkout
|
64
|
+
uses: actions/checkout@v4
|
65
|
+
|
66
|
+
- name: Setup Ruby
|
67
|
+
uses: ruby/setup-ruby@v1
|
68
|
+
with:
|
69
|
+
ruby-version: ${{ matrix.ruby }}
|
70
|
+
bundler-cache: true
|
71
|
+
rubygems: latest
|
72
|
+
env:
|
73
|
+
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
|
74
|
+
|
75
|
+
- name: Test
|
76
|
+
env:
|
77
|
+
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
|
78
|
+
DATABASE_URL: ${{ matrix.adapter }}
|
79
|
+
WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }}
|
80
|
+
run: bundle exec rake
|
data/.gitignore
CHANGED
data/.tool-versions
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby
|
1
|
+
ruby 3.0.5
|
data/Appraisals
CHANGED
@@ -1,29 +1,45 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
appraise 'activerecord-7.1' do
|
4
|
+
gem 'activerecord', '~> 7.1.0'
|
3
5
|
platforms :ruby do
|
4
|
-
|
5
|
-
|
6
|
-
|
6
|
+
gem 'sqlite3'
|
7
|
+
gem 'mysql2'
|
8
|
+
gem 'trilogy'
|
9
|
+
gem 'pg'
|
7
10
|
end
|
8
11
|
end
|
9
12
|
|
10
|
-
appraise
|
11
|
-
gem
|
13
|
+
appraise 'activerecord-7.0' do
|
14
|
+
gem 'activerecord', '~> 7.0.0'
|
12
15
|
platforms :ruby do
|
13
|
-
gem
|
16
|
+
gem 'sqlite3'
|
17
|
+
gem 'mysql2'
|
18
|
+
gem 'trilogy'
|
19
|
+
gem "activerecord-trilogy-adapter"
|
20
|
+
gem 'pg'
|
21
|
+
end
|
22
|
+
platforms :jruby do
|
23
|
+
gem "activerecord-jdbcmysql-adapter"
|
24
|
+
gem "activerecord-jdbcpostgresql-adapter"
|
25
|
+
gem "activerecord-jdbcsqlite3-adapter"
|
14
26
|
end
|
15
27
|
end
|
16
28
|
|
17
|
-
appraise
|
18
|
-
gem
|
19
|
-
gem "sqlite3", "~> 1.3.6"
|
20
|
-
end
|
29
|
+
appraise 'activerecord-6.1' do
|
30
|
+
gem 'activerecord', '~> 6.1.0'
|
21
31
|
|
22
|
-
|
23
|
-
|
24
|
-
|
32
|
+
platforms :ruby do
|
33
|
+
gem 'sqlite3'
|
34
|
+
gem 'mysql2'
|
35
|
+
gem 'trilogy'
|
36
|
+
gem "activerecord-trilogy-adapter"
|
37
|
+
gem 'pg'
|
38
|
+
end
|
39
|
+
platforms :jruby do
|
40
|
+
gem "activerecord-jdbcmysql-adapter"
|
41
|
+
gem "activerecord-jdbcpostgresql-adapter"
|
42
|
+
gem "activerecord-jdbcsqlite3-adapter"
|
43
|
+
end
|
25
44
|
end
|
26
45
|
|
27
|
-
appraise "activerecord-6.0" do
|
28
|
-
gem "activerecord", "~> 6.0.0"
|
29
|
-
end
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
## Changelog
|
2
2
|
|
3
|
+
### 5.0.0
|
4
|
+
- Drop support for EOL rubies and activerecord (ruby below 2.7 and activerecord below 6.1).
|
5
|
+
- Allow lock name to be integer
|
6
|
+
- Jruby support
|
7
|
+
- Truffleruby support
|
8
|
+
- Add `with_advisory_lock!`, which raises an error if the lock acquisition fails
|
9
|
+
- Add `disable_query_cache` option to `with_advisory_lock`
|
10
|
+
- Drop support for mysql < 5.7.5
|
11
|
+
|
3
12
|
### 4.6.0
|
4
13
|
|
5
14
|
- Support for ActiveRecord 6
|
data/Gemfile
CHANGED
@@ -1,15 +1,3 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
3
|
gemspec
|
4
|
-
|
5
|
-
platforms :ruby do
|
6
|
-
gem 'mysql2'
|
7
|
-
gem 'pg'
|
8
|
-
gem 'sqlite3'
|
9
|
-
end
|
10
|
-
|
11
|
-
platforms :jruby do
|
12
|
-
gem 'activerecord-jdbcmysql-adapter'
|
13
|
-
gem 'activerecord-jdbcpostgresql-adapter'
|
14
|
-
gem 'activerecord-jdbcsqlite3-adapter'
|
15
|
-
end
|
data/README.md
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
# with_advisory_lock
|
2
2
|
|
3
|
-
Adds advisory locking (mutexes) to ActiveRecord
|
4
|
-
2.4, 2.5 and 2.6, when used with
|
3
|
+
Adds advisory locking (mutexes) to ActiveRecord 6.0+, with ruby 2.7+, jruby or truffleruby, when used with
|
5
4
|
[MySQL](https://dev.mysql.com/doc/refman/8.0/en/miscellaneous-functions.html#function_get-lock)
|
6
5
|
or
|
7
6
|
[PostgreSQL](https://www.postgresql.org/docs/current/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS).
|
8
7
|
SQLite resorts to file locking.
|
9
8
|
|
10
|
-
[](https://travis-ci.org/ClosureTree/with_advisory_lock)
|
11
9
|
[](https://badge.fury.io/rb/with_advisory_lock)
|
10
|
+
[](https://github.com/ClosureTree/with_advisory_lock/actions/workflows/ci.yml)
|
12
11
|
|
13
12
|
## What's an "Advisory Lock"?
|
14
13
|
|
@@ -45,8 +44,10 @@ A value of zero will try the lock only once. If the lock is acquired, the block
|
|
45
44
|
will be yielded to. If the lock is currently being held, the block will not be
|
46
45
|
called.
|
47
46
|
|
48
|
-
Note
|
49
|
-
|
47
|
+
> **Note**
|
48
|
+
>
|
49
|
+
> If a non-nil value is provided for `timeout_seconds`, the block will
|
50
|
+
*not* be invoked if the lock cannot be acquired within that time-frame. In this case, `with_advisory_lock` will return `false`, while `with_advisory_lock!` will raise a `WithAdvisoryLock::FailedToAcquireLock` error.
|
50
51
|
|
51
52
|
For backwards compatability, the timeout value can be specified directly as the
|
52
53
|
second parameter.
|
@@ -80,6 +81,8 @@ block, if the lock was able to be acquired and the block yielded, or `false`, if
|
|
80
81
|
you provided a timeout_seconds value and the lock was not able to be acquired in
|
81
82
|
time.
|
82
83
|
|
84
|
+
`with_advisory_lock!` is similar to `with_advisory_lock`, but raises a `WithAdvisoryLock::FailedToAcquireLock` error if the lock was not able to be acquired in time.
|
85
|
+
|
83
86
|
### Testing for the current lock status
|
84
87
|
|
85
88
|
If you needed to check if the advisory lock is currently being held, you can
|
@@ -91,6 +94,14 @@ If you want to see if the current Thread is holding a lock, you can call
|
|
91
94
|
`Tag.current_advisory_lock` which will return the name of the current lock. If
|
92
95
|
no lock is currently held, `.current_advisory_lock` returns `nil`.
|
93
96
|
|
97
|
+
### ActiveRecord Query Cache
|
98
|
+
|
99
|
+
You can optionally pass `disable_query_cache: true` to the options hash of
|
100
|
+
`with_advisory_lock` in order to disable ActiveRecord's query cache. This can
|
101
|
+
prevent problems when you query the database from within the lock and it returns
|
102
|
+
stale results. More info on why this can be a problem can be
|
103
|
+
[found here](https://github.com/ClosureTree/with_advisory_lock/issues/52)
|
104
|
+
|
94
105
|
## Installation
|
95
106
|
|
96
107
|
Add this line to your application's Gemfile:
|
@@ -123,7 +134,7 @@ row-level locks prevent concurrent modification to a given model.
|
|
123
134
|
|
124
135
|
**If you're building a
|
125
136
|
[CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete)
|
126
|
-
application, this will be your most commonly used lock.**
|
137
|
+
application, this will be 2.4, 2.5 and your most commonly used lock.**
|
127
138
|
|
128
139
|
### Table-level locks
|
129
140
|
|
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "activerecord", "~> 6.
|
5
|
+
gem "activerecord", "~> 6.1.0"
|
6
6
|
|
7
7
|
platforms :ruby do
|
8
|
+
gem "sqlite3"
|
8
9
|
gem "mysql2"
|
10
|
+
gem "trilogy"
|
11
|
+
gem "activerecord-trilogy-adapter"
|
9
12
|
gem "pg"
|
10
|
-
gem "sqlite3"
|
11
13
|
end
|
12
14
|
|
13
15
|
platforms :jruby do
|
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "activerecord", "~>
|
5
|
+
gem "activerecord", "~> 7.0.0"
|
6
6
|
|
7
7
|
platforms :ruby do
|
8
|
+
gem "sqlite3"
|
8
9
|
gem "mysql2"
|
10
|
+
gem "trilogy"
|
11
|
+
gem "activerecord-trilogy-adapter"
|
9
12
|
gem "pg"
|
10
|
-
gem "sqlite3", "~> 1.3.6"
|
11
13
|
end
|
12
14
|
|
13
15
|
platforms :jruby do
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'zlib'
|
2
4
|
|
3
5
|
module WithAdvisoryLock
|
@@ -19,17 +21,18 @@ module WithAdvisoryLock
|
|
19
21
|
LockStackItem = Struct.new(:name, :shared)
|
20
22
|
|
21
23
|
class Base
|
22
|
-
attr_reader :connection, :lock_name, :timeout_seconds, :shared, :transaction
|
24
|
+
attr_reader :connection, :lock_name, :timeout_seconds, :shared, :transaction, :disable_query_cache
|
23
25
|
|
24
26
|
def initialize(connection, lock_name, options)
|
25
27
|
options = { timeout_seconds: options } unless options.respond_to?(:fetch)
|
26
|
-
options.assert_valid_keys :timeout_seconds, :shared, :transaction
|
28
|
+
options.assert_valid_keys :timeout_seconds, :shared, :transaction, :disable_query_cache
|
27
29
|
|
28
30
|
@connection = connection
|
29
31
|
@lock_name = lock_name
|
30
32
|
@timeout_seconds = options.fetch(:timeout_seconds, nil)
|
31
33
|
@shared = options.fetch(:shared, false)
|
32
34
|
@transaction = options.fetch(:transaction, false)
|
35
|
+
@disable_query_cache = options.fetch(:disable_query_cache, false)
|
33
36
|
end
|
34
37
|
|
35
38
|
def lock_str
|
@@ -51,6 +54,16 @@ module WithAdvisoryLock
|
|
51
54
|
end
|
52
55
|
|
53
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)
|
54
67
|
if already_locked?
|
55
68
|
Result.new(true, yield)
|
56
69
|
elsif timeout_seconds == 0
|
@@ -75,6 +88,7 @@ module WithAdvisoryLock
|
|
75
88
|
while @timeout_seconds.nil? || Time.now < give_up_at
|
76
89
|
r = yield_with_lock(&block)
|
77
90
|
return r if r.lock_was_acquired?
|
91
|
+
|
78
92
|
# Randomizing sleep time may help reduce contention.
|
79
93
|
sleep(rand(0.05..0.15))
|
80
94
|
end
|
@@ -1,19 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/concern'
|
2
4
|
|
3
5
|
module WithAdvisoryLock
|
4
6
|
module Concern
|
5
7
|
extend ActiveSupport::Concern
|
6
|
-
delegate :with_advisory_lock, :advisory_lock_exists?, to: 'self.class'
|
8
|
+
delegate :with_advisory_lock, :with_advisory_lock!, :advisory_lock_exists?, to: 'self.class'
|
7
9
|
|
8
|
-
|
10
|
+
class_methods do
|
9
11
|
def with_advisory_lock(lock_name, options = {}, &block)
|
10
12
|
result = with_advisory_lock_result(lock_name, options, &block)
|
11
13
|
result.lock_was_acquired? ? result.result : false
|
12
14
|
end
|
13
15
|
|
16
|
+
def with_advisory_lock!(lock_name, options = {}, &block)
|
17
|
+
result = with_advisory_lock_result(lock_name, options, &block)
|
18
|
+
unless result.lock_was_acquired?
|
19
|
+
raise WithAdvisoryLock::FailedToAcquireLock, lock_name
|
20
|
+
end
|
21
|
+
|
22
|
+
result.result
|
23
|
+
end
|
24
|
+
|
14
25
|
def with_advisory_lock_result(lock_name, options = {}, &block)
|
15
|
-
|
16
|
-
impl = impl_class(class_options).new(connection, lock_name, options)
|
26
|
+
impl = impl_class.new(connection, lock_name, options)
|
17
27
|
impl.with_advisory_lock_if_needed(&block)
|
18
28
|
end
|
19
29
|
|
@@ -29,22 +39,12 @@ module WithAdvisoryLock
|
|
29
39
|
|
30
40
|
private
|
31
41
|
|
32
|
-
def impl_class
|
42
|
+
def impl_class
|
33
43
|
adapter = WithAdvisoryLock::DatabaseAdapterSupport.new(connection)
|
34
44
|
if adapter.postgresql?
|
35
45
|
WithAdvisoryLock::PostgreSQL
|
36
46
|
elsif adapter.mysql?
|
37
|
-
|
38
|
-
options.fetch(:force_nested_lock_support)
|
39
|
-
else
|
40
|
-
adapter.mysql_nested_lock_support?
|
41
|
-
end
|
42
|
-
|
43
|
-
if nested_lock
|
44
|
-
WithAdvisoryLock::MySQL
|
45
|
-
else
|
46
|
-
WithAdvisoryLock::MySQLNoNesting
|
47
|
-
end
|
47
|
+
WithAdvisoryLock::MySQL
|
48
48
|
else
|
49
49
|
WithAdvisoryLock::Flock
|
50
50
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WithAdvisoryLock
|
2
4
|
class DatabaseAdapterSupport
|
3
5
|
# Caches nested lock support by MySQL reported version
|
@@ -10,46 +12,7 @@ module WithAdvisoryLock
|
|
10
12
|
end
|
11
13
|
|
12
14
|
def mysql?
|
13
|
-
%i[
|
14
|
-
end
|
15
|
-
|
16
|
-
# Nested lock support for MySQL was introduced in 5.7.5
|
17
|
-
# Checking by version number is complicated by MySQL compatible DBs (like MariaDB) having their own versioning schemes
|
18
|
-
# Therefore, we check for nested lock support by simply trying a nested lock, then testing and caching the outcome
|
19
|
-
def mysql_nested_lock_support?
|
20
|
-
return false unless mysql?
|
21
|
-
|
22
|
-
# We select the MySQL version this way and cache on it, as MySQL will report versions like "5.7.5", and MariaDB will
|
23
|
-
# report versions like "10.3.8-MariaDB", which allow us to cache on features without introducing problems.
|
24
|
-
version = @connection.select_value("SELECT version()")
|
25
|
-
|
26
|
-
@@mysql_nl_cache_mutex.synchronize do
|
27
|
-
return @@mysql_nl_cache[version] if @@mysql_nl_cache.keys.include?(version)
|
28
|
-
|
29
|
-
lock_1 = "\"nested-test-1-#{SecureRandom.hex}\""
|
30
|
-
lock_2 = "\"nested-test-2-#{SecureRandom.hex}\""
|
31
|
-
|
32
|
-
get_1 = @connection.select_value("SELECT GET_LOCK(#{lock_1}, 0) AS t#{SecureRandom.hex}")
|
33
|
-
get_2 = @connection.select_value("SELECT GET_LOCK(#{lock_2}, 0) AS t#{SecureRandom.hex}")
|
34
|
-
|
35
|
-
# Both locks should succeed in old and new MySQL versions with "1"
|
36
|
-
raise RuntimeError, "Unexpected nested lock acquire result #{get_1}, #{get_2}" unless [get_1, get_2] == [1, 1]
|
37
|
-
|
38
|
-
release_1 = @connection.select_value("SELECT RELEASE_LOCK(#{lock_1}) AS t#{SecureRandom.hex}")
|
39
|
-
release_2 = @connection.select_value("SELECT RELEASE_LOCK(#{lock_2}) AS t#{SecureRandom.hex}")
|
40
|
-
|
41
|
-
# In MySQL < 5.7.5 release_1 will return nil (not currently locked) and release_2 will return 1 (successfully unlocked)
|
42
|
-
# In MySQL >= 5.7.5 release_1 and release_2 will return 1 (both successfully unlocked)
|
43
|
-
# See https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock for more
|
44
|
-
@@mysql_nl_cache[version] = case [release_1, release_2]
|
45
|
-
when [1, 1]
|
46
|
-
true
|
47
|
-
when [nil, 1]
|
48
|
-
false
|
49
|
-
else
|
50
|
-
raise RuntimeError, "Unexpected nested lock release result #{release_1}, #{release_2}"
|
51
|
-
end
|
52
|
-
end
|
15
|
+
%i[mysql2 trilogy].include? @sym_name
|
53
16
|
end
|
54
17
|
|
55
18
|
def postgresql?
|
@@ -57,7 +20,7 @@ module WithAdvisoryLock
|
|
57
20
|
end
|
58
21
|
|
59
22
|
def sqlite?
|
60
|
-
|
23
|
+
@sym_name == :sqlite3
|
61
24
|
end
|
62
25
|
end
|
63
26
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'fileutils'
|
2
4
|
|
3
5
|
module WithAdvisoryLock
|
@@ -19,9 +21,8 @@ module WithAdvisoryLock
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def try_lock
|
22
|
-
if transaction
|
23
|
-
|
24
|
-
end
|
24
|
+
raise ArgumentError, 'transaction level locks are not supported on SQLite' if transaction
|
25
|
+
|
25
26
|
0 == file_io.flock((shared ? File::LOCK_SH : File::LOCK_EX) | File::LOCK_NB)
|
26
27
|
end
|
27
28
|
|
@@ -1,12 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WithAdvisoryLock
|
2
|
-
# MySQL > 5.7.5 supports nested locks
|
3
4
|
class MySQL < Base
|
4
5
|
# See https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
|
5
6
|
def try_lock
|
6
7
|
raise ArgumentError, 'shared locks are not supported on MySQL' if shared
|
7
|
-
if transaction
|
8
|
-
|
9
|
-
end
|
8
|
+
raise ArgumentError, 'transaction level locks are not supported on MySQL' if transaction
|
9
|
+
|
10
10
|
execute_successful?("GET_LOCK(#{quoted_lock_str}, 0)")
|
11
11
|
end
|
12
12
|
|
@@ -16,7 +16,7 @@ module WithAdvisoryLock
|
|
16
16
|
|
17
17
|
def execute_successful?(mysql_function)
|
18
18
|
sql = "SELECT #{mysql_function} AS #{unique_column_name}"
|
19
|
-
connection.select_value(sql).to_i
|
19
|
+
connection.select_value(sql).to_i.positive?
|
20
20
|
end
|
21
21
|
|
22
22
|
# MySQL wants a string as the lock key.
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WithAdvisoryLock
|
2
4
|
class PostgreSQL < Base
|
3
5
|
# See http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
|
@@ -8,10 +10,12 @@ module WithAdvisoryLock
|
|
8
10
|
|
9
11
|
def release_lock
|
10
12
|
return if transaction
|
13
|
+
|
11
14
|
pg_function = "pg_advisory_unlock#{shared ? '_shared' : ''}"
|
12
15
|
execute_successful?(pg_function)
|
13
16
|
rescue ActiveRecord::StatementInvalid => e
|
14
17
|
raise unless e.message =~ / ERROR: +current transaction is aborted,/
|
18
|
+
|
15
19
|
begin
|
16
20
|
connection.rollback_db_transaction
|
17
21
|
execute_successful?(pg_function)
|
@@ -21,20 +25,18 @@ module WithAdvisoryLock
|
|
21
25
|
end
|
22
26
|
|
23
27
|
def execute_successful?(pg_function)
|
24
|
-
comment = lock_name.gsub(
|
28
|
+
comment = lock_name.to_s.gsub(%r{(/\*)|(\*/)}, '--')
|
25
29
|
sql = "SELECT #{pg_function}(#{lock_keys.join(',')}) AS #{unique_column_name} /* #{comment} */"
|
26
30
|
result = connection.select_value(sql)
|
27
31
|
# MRI returns 't', jruby returns true. YAY!
|
28
|
-
|
32
|
+
['t', true].include?(result)
|
29
33
|
end
|
30
34
|
|
31
35
|
# PostgreSQL wants 2 32bit integers as the lock key.
|
32
36
|
def lock_keys
|
33
|
-
@lock_keys ||=
|
34
|
-
|
35
|
-
|
36
|
-
ea.to_i & 0x7fffffff
|
37
|
-
end
|
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
|
38
40
|
end
|
39
41
|
end
|
40
42
|
end
|
data/lib/with_advisory_lock.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'with_advisory_lock/version'
|
2
2
|
require 'active_support'
|
3
|
+
require_relative 'with_advisory_lock/failed_to_acquire_lock'
|
3
4
|
|
4
5
|
module WithAdvisoryLock
|
5
6
|
extend ActiveSupport::Autoload
|
@@ -9,8 +10,6 @@ module WithAdvisoryLock
|
|
9
10
|
autoload :DatabaseAdapterSupport
|
10
11
|
autoload :Flock
|
11
12
|
autoload :MySQL, 'with_advisory_lock/mysql'
|
12
|
-
autoload :MySQLNoNesting, 'with_advisory_lock/mysql_no_nesting'
|
13
|
-
autoload :NestedAdvisoryLockError
|
14
13
|
autoload :PostgreSQL, 'with_advisory_lock/postgresql'
|
15
14
|
end
|
16
15
|
|
data/test/concern_test.rb
CHANGED
@@ -1,20 +1,33 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
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)
|
6
8
|
end
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
test 'adds with_advisory_lock to ActiveRecord instances' do
|
11
|
+
assert_respond_to(Label.new, :with_advisory_lock)
|
10
12
|
end
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
+
test 'adds advisory_lock_exists? to ActiveRecord classes' do
|
15
|
+
assert_respond_to(Tag, :advisory_lock_exists?)
|
14
16
|
end
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
+
test 'adds advisory_lock_exists? to ActiveRecord instances' do
|
19
|
+
assert_respond_to(Label.new, :advisory_lock_exists?)
|
18
20
|
end
|
21
|
+
end
|
19
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
|
20
33
|
end
|