db_lock 0.8.3 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +14 -5
- data/lib/db_lock/adapter/base.rb +28 -1
- data/lib/db_lock/adapter/mysql.rb +4 -6
- data/lib/db_lock/adapter/postgres.rb +66 -0
- data/lib/db_lock/adapter.rb +3 -0
- data/lib/db_lock/lock.rb +3 -39
- data/lib/db_lock/locking.rb +47 -0
- data/lib/db_lock.rb +12 -4
- metadata +10 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50db7061b198b0f203663eecb1c432320d5977a9887e9468a2742d718d16e399
|
4
|
+
data.tar.gz: 18368c4c79889d0eed565a5090f4b00b548d52b29b7cfbf93f65e7f05cbc05fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4d25535f5a4b4e32dd4c4ec8b3c9bd97a107df91c881ddef25ba2f3e47ff7e0cbfec398a8ca45a29e251daaf0a3a5aa8d17a776eba3b2d8bd6a8bef5ff44b26
|
7
|
+
data.tar.gz: cb7cc7240ba171a0e049d638043b3b220a987ee254255674de053dfae413adb65fa165292184f1ddeef9f1eb4ed93b52dbb64b18f711f3166ca9d09a5c84fc32
|
data/README.md
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# DBLock
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/db_lock.svg)](https://badge.fury.io/rb/db_lock)
|
4
|
-
[![Tests](https://github.com/mkon/db_lock/actions/workflows/
|
4
|
+
[![Tests](https://github.com/mkon/db_lock/actions/workflows/main.yml/badge.svg)](https://github.com/mkon/db_lock/actions/workflows/main.yml)
|
5
5
|
|
6
6
|
Gem to obtain and release manual db locks. This can be utilized for example to make sure that certain rake tasks do not run in parallel on the same database (for example when cron jobs run for too long or are accidentally started multiple times). Currently only supports:
|
7
7
|
|
8
8
|
- MySQL
|
9
9
|
- Microsoft SQL Server
|
10
|
+
- Postgres
|
10
11
|
|
11
12
|
## Installation
|
12
13
|
|
@@ -19,16 +20,24 @@ then run `bundle`
|
|
19
20
|
## Usage
|
20
21
|
|
21
22
|
```ruby
|
22
|
-
DBLock
|
23
|
+
DBLock.with_lock('name_of_lock', 5) do
|
23
24
|
# code here
|
24
25
|
end
|
25
26
|
```
|
26
27
|
|
27
|
-
Before the code block is executed, it will attempt to acquire a
|
28
|
+
Before the code block is executed, it will attempt to acquire a db lock for X seconds (5 in this example). If this fails it will raise an `DBLock::AlreadyLocked` error. The lock is released after the block is executed, even if the block raised an error itself.
|
28
29
|
|
29
|
-
The
|
30
|
+
The locking will already fail with an error if the current thread already holds a lock via this gem.
|
30
31
|
|
31
|
-
|
32
|
+
Locks are achieved on the database via:
|
33
|
+
|
34
|
+
| Database | Locking method |
|
35
|
+
|-----------|--------------------|
|
36
|
+
| MySQL | `GET_LOCK` |
|
37
|
+
| Postgres | `pg_advisory_lock` |
|
38
|
+
| SQLServer | `sp_getapplock` |
|
39
|
+
|
40
|
+
## Dynamic lock name
|
32
41
|
|
33
42
|
If you prefix the lock with a `.` in a Rails application, `.` will be automatically replaced with `YourAppName.environment` (production/development/etc).
|
34
43
|
If the lock name exceeds 64 characters, it will be replaced with a lock name of 64 characters, that consists of a pre- and suffix from the original lock name and a middle MD5 checksum.
|
data/lib/db_lock/adapter/base.rb
CHANGED
@@ -3,14 +3,41 @@ module DBLock
|
|
3
3
|
class Base
|
4
4
|
include Singleton
|
5
5
|
|
6
|
+
def execute(*args)
|
7
|
+
run_sanitized :execute, args
|
8
|
+
end
|
9
|
+
|
10
|
+
def select_one(*args)
|
11
|
+
run_sanitized :select_one, args
|
12
|
+
end
|
13
|
+
|
14
|
+
def select_value(*args)
|
15
|
+
run_sanitized :select_value, args
|
16
|
+
end
|
17
|
+
|
6
18
|
private
|
7
19
|
|
8
20
|
def connection
|
9
21
|
DBLock.db_handler.connection
|
10
22
|
end
|
11
23
|
|
24
|
+
def pool
|
25
|
+
DBLock.db_handler.connection_pool
|
26
|
+
end
|
27
|
+
|
28
|
+
def logger
|
29
|
+
DBLock.db_handler.logger
|
30
|
+
end
|
31
|
+
|
12
32
|
def sanitize_sql_array(*args)
|
13
|
-
DBLock.db_handler.
|
33
|
+
DBLock.db_handler.sanitize_sql_array args
|
34
|
+
end
|
35
|
+
|
36
|
+
def run_sanitized(command, args)
|
37
|
+
options = args.extract_options!
|
38
|
+
con = options[:connection] || connection
|
39
|
+
sql = sanitize_sql_array(*args)
|
40
|
+
con.public_send(command, sql)
|
14
41
|
end
|
15
42
|
end
|
16
43
|
end
|
@@ -2,15 +2,13 @@ module DBLock
|
|
2
2
|
module Adapter
|
3
3
|
class MYSQL < Base
|
4
4
|
def lock(name, timeout = 0)
|
5
|
-
|
6
|
-
res
|
7
|
-
(res && res.values.first == 1)
|
5
|
+
res = select_value 'SELECT GET_LOCK(?, ?)', name, timeout
|
6
|
+
res == 1
|
8
7
|
end
|
9
8
|
|
10
9
|
def release(name)
|
11
|
-
|
12
|
-
res
|
13
|
-
(res && res.values.first == 1)
|
10
|
+
res = select_value 'SELECT RELEASE_LOCK(?)', name
|
11
|
+
res == 1
|
14
12
|
end
|
15
13
|
end
|
16
14
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module DBLock
|
4
|
+
module Adapter
|
5
|
+
LockTimeout = Class.new(Timeout::Error)
|
6
|
+
|
7
|
+
class Postgres < Base
|
8
|
+
def lock(name, timeout = 0)
|
9
|
+
pid = connection_pid(connection)
|
10
|
+
Timeout.timeout(timeout, LockTimeout) do
|
11
|
+
execute lock_query(name)
|
12
|
+
# Sadly this returns void in postgres
|
13
|
+
true
|
14
|
+
end
|
15
|
+
rescue LockTimeout
|
16
|
+
logger&.info 'DBLock: Recovering from expired lock query'
|
17
|
+
recover_from_timeout pid, name
|
18
|
+
end
|
19
|
+
|
20
|
+
def release(name)
|
21
|
+
res = select_value 'SELECT pg_advisory_unlock(hashtext(?))', name
|
22
|
+
res == true
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def lock_query(name)
|
28
|
+
sanitize_sql_array 'SELECT pg_advisory_lock(hashtext(?))', name
|
29
|
+
end
|
30
|
+
|
31
|
+
def connection_pid(con)
|
32
|
+
select_value 'SELECT pg_backend_pid()', connection: con
|
33
|
+
end
|
34
|
+
|
35
|
+
# We have to manually kill the lock query.
|
36
|
+
# Connection pool keeps it alive blocking one connection.
|
37
|
+
# Also it would eventually acquire the lock.
|
38
|
+
# returns true if lock was acquired
|
39
|
+
def recover_from_timeout(pid, name)
|
40
|
+
with_dedicated_connection do |con|
|
41
|
+
lock = select_one(<<~SQL, pid, name, connection: con)
|
42
|
+
SELECT locktype, objid, pid, granted FROM pg_locks \
|
43
|
+
WHERE pid = ? AND locktype = 'advisory' AND objid = hashtext(?)
|
44
|
+
SQL
|
45
|
+
return false unless lock
|
46
|
+
|
47
|
+
if lock['granted']
|
48
|
+
logger&.info 'DBLock: Lock was acquired after all'
|
49
|
+
true
|
50
|
+
else
|
51
|
+
res = select_value 'SELECT pg_cancel_backend(?)', pid, connection: con
|
52
|
+
logger&.warn 'DBLock: Failed to cancel ungranted lock query' unless res == true
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def with_dedicated_connection
|
59
|
+
con = pool.checkout
|
60
|
+
yield con
|
61
|
+
ensure
|
62
|
+
pool.checkin con
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/db_lock/adapter.rb
CHANGED
@@ -4,6 +4,7 @@ module DBLock
|
|
4
4
|
|
5
5
|
autoload :Base, 'db_lock/adapter/base'
|
6
6
|
autoload :MYSQL, 'db_lock/adapter/mysql'
|
7
|
+
autoload :Postgres, 'db_lock/adapter/postgres'
|
7
8
|
autoload :Sqlserver, 'db_lock/adapter/sqlserver'
|
8
9
|
|
9
10
|
delegate :lock, :release, to: :implementation
|
@@ -12,6 +13,8 @@ module DBLock
|
|
12
13
|
case DBLock.db_handler.connection.adapter_name.downcase
|
13
14
|
when 'mysql2'
|
14
15
|
MYSQL.instance
|
16
|
+
when 'postgresql'
|
17
|
+
Postgres.instance
|
15
18
|
when 'sqlserver'
|
16
19
|
Sqlserver.instance
|
17
20
|
else
|
data/lib/db_lock/lock.rb
CHANGED
@@ -4,48 +4,12 @@ module DBLock
|
|
4
4
|
module Lock
|
5
5
|
extend self
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
timeout = timeout.to_f # catches nil
|
10
|
-
timeout = 0 if timeout.negative?
|
11
|
-
|
12
|
-
raise "Invalid lock name: #{name.inspect}" if name.empty?
|
13
|
-
raise AlreadyLocked, 'Already lock in progress' if locked?
|
14
|
-
|
15
|
-
name = generate_lock_name(name)
|
16
|
-
|
17
|
-
if Adapter.lock(name, timeout)
|
18
|
-
@locked = true
|
19
|
-
yield
|
20
|
-
else
|
21
|
-
raise AlreadyLocked, "Unable to obtain lock '#{name}' within #{timeout} seconds" unless locked?
|
22
|
-
end
|
23
|
-
ensure
|
24
|
-
Adapter.release(name) if locked?
|
25
|
-
@locked = false
|
7
|
+
def get(name, timeout = 0, &block)
|
8
|
+
DBLock.with_lock(name, timeout, &block)
|
26
9
|
end
|
27
|
-
# rubocop:enable Metrics/AbcSize
|
28
10
|
|
29
11
|
def locked?
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def generate_lock_name(name)
|
36
|
-
name = "#{rails_app_name}.#{Rails.env}#{name}" if name[0] == '.' && defined? Rails
|
37
|
-
# reduce lock names of > 64 chars in size
|
38
|
-
# MySQL 5.7 only supports 64 chars max, there might be similar limitations elsewhere
|
39
|
-
name = "#{name.chars.first(15).join}-#{Digest::MD5.hexdigest(name)}-#{name.chars.last(15).join}" if name.length > 64
|
40
|
-
name
|
41
|
-
end
|
42
|
-
|
43
|
-
def rails_app_name
|
44
|
-
if Gem::Version.new(Rails.version) >= Gem::Version.new('6.0.0')
|
45
|
-
Rails.application.class.module_parent_name
|
46
|
-
else
|
47
|
-
Rails.application.class.parent_name
|
48
|
-
end
|
12
|
+
DBLock.send(:locked?)
|
49
13
|
end
|
50
14
|
end
|
51
15
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module DBLock
|
4
|
+
module Locking
|
5
|
+
def with_lock(name, timeout = 0)
|
6
|
+
timeout = timeout.to_f # catches nil
|
7
|
+
timeout = 0 if timeout.negative?
|
8
|
+
|
9
|
+
raise ArgumentError, "Invalid lock name: #{name.inspect}" if name.empty?
|
10
|
+
raise AlreadyLocked, 'Already lock in progress' if locked?
|
11
|
+
|
12
|
+
name = generate_lock_name(name)
|
13
|
+
|
14
|
+
if Adapter.lock(name, timeout)
|
15
|
+
@locked = true
|
16
|
+
yield
|
17
|
+
else
|
18
|
+
raise AlreadyLocked, "Unable to obtain lock '#{name}' within #{timeout} seconds" unless locked?
|
19
|
+
end
|
20
|
+
ensure
|
21
|
+
Adapter.release(name) if locked?
|
22
|
+
@locked = false
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def locked?
|
28
|
+
@locked ||= false
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate_lock_name(name)
|
32
|
+
name = "#{rails_app_name}.#{Rails.env}#{name}" if name[0] == '.' && defined? Rails
|
33
|
+
# reduce lock names of > 64 chars in size
|
34
|
+
# MySQL 5.7 only supports 64 chars max, there might be similar limitations elsewhere
|
35
|
+
name = "#{name.chars.first(15).join}-#{Digest::MD5.hexdigest(name)}-#{name.chars.last(15).join}" if name.length > 64
|
36
|
+
name
|
37
|
+
end
|
38
|
+
|
39
|
+
def rails_app_name
|
40
|
+
if Gem::Version.new(Rails.version) >= Gem::Version.new('6.0.0')
|
41
|
+
Rails.application.class.module_parent_name
|
42
|
+
else
|
43
|
+
Rails.application.class.parent_name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/db_lock.rb
CHANGED
@@ -1,17 +1,25 @@
|
|
1
|
+
require 'active_support'
|
1
2
|
require 'digest/md5'
|
2
3
|
|
3
4
|
module DBLock
|
4
|
-
extend self
|
5
|
-
|
6
5
|
autoload :Adapter, 'db_lock/adapter'
|
7
6
|
autoload :Lock, 'db_lock/lock'
|
7
|
+
autoload :Locking, 'db_lock/locking'
|
8
|
+
|
9
|
+
extend Locking
|
8
10
|
|
9
11
|
class AlreadyLocked < StandardError; end
|
10
12
|
|
11
|
-
|
13
|
+
attr_writer :db_handler
|
12
14
|
|
13
|
-
def db_handler
|
15
|
+
def self.db_handler
|
14
16
|
# this must be an active record base object or subclass
|
15
17
|
@db_handler || ActiveRecord::Base
|
16
18
|
end
|
19
|
+
|
20
|
+
custom_deprecator = ActiveSupport::Deprecation.new('1.0.0', 'DBLock')
|
21
|
+
ActiveSupport::Deprecation.deprecate_methods(DBLock::Lock, get: 'use DBLock.with_lock instead',
|
22
|
+
deprecator: custom_deprecator)
|
23
|
+
ActiveSupport::Deprecation.deprecate_methods(DBLock::Lock, locked?: 'will be removed without replacement',
|
24
|
+
deprecator: custom_deprecator)
|
17
25
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: db_lock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mkon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -50,28 +50,28 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - '='
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: 1.
|
53
|
+
version: 1.37.1
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - '='
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version: 1.
|
60
|
+
version: 1.37.1
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
62
|
name: rubocop-rspec
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
65
|
- - '='
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: 2.
|
67
|
+
version: 2.14.2
|
68
68
|
type: :development
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - '='
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: 2.
|
74
|
+
version: 2.14.2
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
76
|
name: simplecov
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,8 +86,8 @@ dependencies:
|
|
86
86
|
- - ">="
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '0'
|
89
|
-
description: Obtain manual db locks to guard blocks of code from parallel execution.
|
90
|
-
|
89
|
+
description: Obtain manual db locks to guard blocks of code from parallel execution.Supports
|
90
|
+
mysql, postgres and ms-sql-server.
|
91
91
|
email:
|
92
92
|
- konstantin@munteanu.de
|
93
93
|
executables: []
|
@@ -100,8 +100,10 @@ files:
|
|
100
100
|
- lib/db_lock/adapter.rb
|
101
101
|
- lib/db_lock/adapter/base.rb
|
102
102
|
- lib/db_lock/adapter/mysql.rb
|
103
|
+
- lib/db_lock/adapter/postgres.rb
|
103
104
|
- lib/db_lock/adapter/sqlserver.rb
|
104
105
|
- lib/db_lock/lock.rb
|
106
|
+
- lib/db_lock/locking.rb
|
105
107
|
homepage: https://github.com/mkon/db_lock
|
106
108
|
licenses:
|
107
109
|
- MIT
|