with_advisory_lock 4.6.0 → 5.1.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/.github/workflows/release.yml +20 -0
- data/.gitignore +2 -0
- data/.release-please-manifest.json +1 -0
- data/.tool-versions +1 -1
- data/Appraisals +34 -18
- data/CHANGELOG.md +31 -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 +17 -3
- data/lib/with_advisory_lock/concern.rb +13 -17
- data/lib/with_advisory_lock/database_adapter_support.rb +4 -41
- data/lib/with_advisory_lock/failed_to_acquire_lock.rb +9 -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 +8 -10
- data/release-please-config.json +9 -0
- 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 +29 -23
- metadata +32 -28
- 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: 6918df100f32e829701a38ad6b7dd30db20d7a83635b82cbd8ccc7f3022a88d5
|
4
|
+
data.tar.gz: f231c4fa8b5c403de000054839603e2297215929a64cccc63c21c577bc19ecd7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21a2201433600aa26b0f7b0176b3b4e87606a00459b70d9dfb10a385c00a53fa5f0365cd426c2b5793d1b0022fa37e663567b0daf185e4be93d5a59e10695e2e
|
7
|
+
data.tar.gz: b1e707a2115e83cd1667bb6b1842c7150b28da3603bd3ec668fc9918de01553fc10d44e65d89d94a3f7a183489da49b0348d1cc59650487d48482648041445df
|
@@ -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
|
@@ -0,0 +1,20 @@
|
|
1
|
+
name: release-please
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- master
|
7
|
+
workflow_dispatch:
|
8
|
+
|
9
|
+
permissions:
|
10
|
+
contents: write
|
11
|
+
pull-requests: write
|
12
|
+
|
13
|
+
jobs:
|
14
|
+
release-please:
|
15
|
+
runs-on: ubuntu-latest
|
16
|
+
steps:
|
17
|
+
- uses: google-github-actions/release-please-action@v4
|
18
|
+
id: release
|
19
|
+
with:
|
20
|
+
command: manifest
|
data/.gitignore
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
{".":"5.1.0"}
|
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,36 @@
|
|
1
1
|
## Changelog
|
2
2
|
|
3
|
+
## [5.1.0](https://github.com/ClosureTree/with_advisory_lock/compare/with_advisory_lock/v5.0.1...with_advisory_lock/v5.1.0) (2024-01-21)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* use zeitwerk loader instead of ActiveSupport::Autoload ([b5082fd](https://github.com/ClosureTree/with_advisory_lock/commit/b5082fddacacacff48139f5bf509601a37945a0e))
|
9
|
+
|
10
|
+
## 5.0.1 (2024-01-21)
|
11
|
+
|
12
|
+
|
13
|
+
### Features
|
14
|
+
|
15
|
+
* add release workflow ([5d32520](https://github.com/ClosureTree/with_advisory_lock/commit/5d325201c82974991381a9fbc4d1714c9739dc4f))
|
16
|
+
* add ruby 3.1 test/support ([#60](https://github.com/ClosureTree/with_advisory_lock/issues/60)) ([514f042](https://github.com/ClosureTree/with_advisory_lock/commit/514f0420d957ef30911a00d54685385bec5867c3))
|
17
|
+
* Add testing for activerecord 7.1 and support for trilogy adapter ([#77](https://github.com/ClosureTree/with_advisory_lock/issues/77)) ([69c23fe](https://github.com/ClosureTree/with_advisory_lock/commit/69c23fe09887fc5d97ac7b0194825c21efe244a5))
|
18
|
+
* add truffleruby support ([#62](https://github.com/ClosureTree/with_advisory_lock/issues/62)) ([ec34bd4](https://github.com/ClosureTree/with_advisory_lock/commit/ec34bd448e3505e5df631daaf47bb83f2f5316dc))
|
19
|
+
|
20
|
+
|
21
|
+
### Bug Fixes
|
22
|
+
|
23
|
+
* User may sometimes pass in non-strings, such as integers ([#55](https://github.com/ClosureTree/with_advisory_lock/issues/55)) ([9885597](https://github.com/ClosureTree/with_advisory_lock/commit/988559747363ef00958fcf782317e76c40ffa2a3))
|
24
|
+
|
25
|
+
### 5.0.0
|
26
|
+
- Drop support for EOL rubies and activerecord (ruby below 2.7 and activerecord below 6.1).
|
27
|
+
- Allow lock name to be integer
|
28
|
+
- Jruby support
|
29
|
+
- Truffleruby support
|
30
|
+
- Add `with_advisory_lock!`, which raises an error if the lock acquisition fails
|
31
|
+
- Add `disable_query_cache` option to `with_advisory_lock`
|
32
|
+
- Drop support for mysql < 5.7.5
|
33
|
+
|
3
34
|
### 4.6.0
|
4
35
|
|
5
36
|
- 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
|
@@ -66,7 +79,7 @@ module WithAdvisoryLock
|
|
66
79
|
else
|
67
80
|
# Ruby MRI's String#hash is randomly seeded as of Ruby 1.9 so
|
68
81
|
# make sure we use a deterministic hash.
|
69
|
-
Zlib.crc32(input.to_s)
|
82
|
+
Zlib.crc32(input.to_s, 0)
|
70
83
|
end
|
71
84
|
end
|
72
85
|
|
@@ -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,25 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module WithAdvisoryLock
|
4
4
|
module Concern
|
5
5
|
extend ActiveSupport::Concern
|
6
|
-
delegate :with_advisory_lock, :advisory_lock_exists?, to: 'self.class'
|
6
|
+
delegate :with_advisory_lock, :with_advisory_lock!, :advisory_lock_exists?, to: 'self.class'
|
7
7
|
|
8
|
-
|
8
|
+
class_methods do
|
9
9
|
def with_advisory_lock(lock_name, options = {}, &block)
|
10
10
|
result = with_advisory_lock_result(lock_name, options, &block)
|
11
11
|
result.lock_was_acquired? ? result.result : false
|
12
12
|
end
|
13
13
|
|
14
|
+
def with_advisory_lock!(lock_name, options = {}, &block)
|
15
|
+
result = with_advisory_lock_result(lock_name, options, &block)
|
16
|
+
raise WithAdvisoryLock::FailedToAcquireLock, lock_name unless result.lock_was_acquired?
|
17
|
+
|
18
|
+
result.result
|
19
|
+
end
|
20
|
+
|
14
21
|
def with_advisory_lock_result(lock_name, options = {}, &block)
|
15
|
-
|
16
|
-
impl = impl_class(class_options).new(connection, lock_name, options)
|
22
|
+
impl = impl_class.new(connection, lock_name, options)
|
17
23
|
impl.with_advisory_lock_if_needed(&block)
|
18
24
|
end
|
19
25
|
|
@@ -29,22 +35,12 @@ module WithAdvisoryLock
|
|
29
35
|
|
30
36
|
private
|
31
37
|
|
32
|
-
def impl_class
|
38
|
+
def impl_class
|
33
39
|
adapter = WithAdvisoryLock::DatabaseAdapterSupport.new(connection)
|
34
40
|
if adapter.postgresql?
|
35
41
|
WithAdvisoryLock::PostgreSQL
|
36
42
|
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
|
43
|
+
WithAdvisoryLock::MySQL
|
48
44
|
else
|
49
45
|
WithAdvisoryLock::Flock
|
50
46
|
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
|