with_advisory_lock 4.6.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://api.travis-ci.org/ClosureTree/with_advisory_lock.svg?branch=master)](https://travis-ci.org/ClosureTree/with_advisory_lock)
|
11
9
|
[![Gem Version](https://badge.fury.io/rb/with_advisory_lock.svg)](https://badge.fury.io/rb/with_advisory_lock)
|
10
|
+
[![CI](https://github.com/ClosureTree/with_advisory_lock/actions/workflows/ci.yml/badge.svg)](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
|