db_lock 0.7 → 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -5
- data/lib/db_lock/adapter/mysql.rb +3 -3
- data/lib/db_lock/adapter/sqlserver.rb +11 -5
- data/lib/db_lock/adapter.rb +3 -3
- data/lib/db_lock/lock.rb +51 -0
- data/lib/db_lock/version.rb +1 -1
- data/lib/db_lock.rb +2 -44
- metadata +61 -19
- data/config/database_mssql.yml +0 -17
- data/config/database_mssql_example.yml +0 -17
- data/config/database_mysql.yml +0 -14
- data/config/database_mysql_example.yml +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f59c12f3f279febb7c5350a2b4b6638d222bc50beeb038563a59afac4efa4328
|
4
|
+
data.tar.gz: 499be9d47073117184c99fdebf1f398eb2b50cb0aa5cd7e082fa55945033e263
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c447141d577b1baa3fef43abd12ee20941603a0659b281514d58b6f797bfec611743c71f7a08b07d1a054417647313dca4847774b3314e2db21ca2f8779e084f
|
7
|
+
data.tar.gz: 941c56299b884aa698b6e9d1bbfa18ea83555dc77f2965a73b21cf6cf0f66a718ec68eb2c681a6ed6a13c0d428e896bce8e40ddef5ca49cc5a010cfafa43afd9
|
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# DBLock
|
2
2
|
|
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/test.yml/badge.svg)](https://github.com/mkon/db_lock/actions/workflows/test.yml)
|
5
|
+
|
3
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:
|
4
7
|
|
5
8
|
- MySQL
|
@@ -23,7 +26,7 @@ end
|
|
23
26
|
|
24
27
|
Before the code block is executed, it will attempt to acquire a mysql 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.
|
25
28
|
|
26
|
-
The current implementation uses a class variable to store lock state so it is not thread
|
29
|
+
The current implementation uses a class variable to store lock state so it is not thread-safe when using multiple threads to acquire/release locks.
|
27
30
|
|
28
31
|
## Smart lock name
|
29
32
|
|
@@ -35,13 +38,13 @@ If the lock name exceeds 64 characters, it will be replaced with a lock name of
|
|
35
38
|
|
36
39
|
Bundle with the adapter you want to use, for example
|
37
40
|
|
38
|
-
```
|
39
|
-
bundle --with
|
41
|
+
```bash
|
42
|
+
$ bundle --with mysql
|
40
43
|
```
|
41
44
|
|
42
45
|
Run rspec with the database url env variables set. It will only run the specs it can run and skip the others.
|
43
46
|
|
44
47
|
For example
|
45
|
-
```
|
46
|
-
MYSQL_URL=mysql2://root:dummy@localhost/test SQLSERVER_URL=sqlserver://root:dummy@localhost/test rspec
|
48
|
+
```bash
|
49
|
+
$ MYSQL_URL=mysql2://root:dummy@localhost/test SQLSERVER_URL=sqlserver://root:dummy@localhost/test rspec
|
47
50
|
```
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module DBLock
|
2
2
|
module Adapter
|
3
3
|
class MYSQL < Base
|
4
|
-
def lock(name, timeout=0)
|
5
|
-
sql = sanitize_sql_array
|
4
|
+
def lock(name, timeout = 0)
|
5
|
+
sql = sanitize_sql_array 'SELECT GET_LOCK(?, ?)', name, timeout
|
6
6
|
res = connection.select_one sql
|
7
7
|
(res && res.values.first == 1)
|
8
8
|
end
|
9
9
|
|
10
10
|
def release(name)
|
11
|
-
sql = sanitize_sql_array
|
11
|
+
sql = sanitize_sql_array 'SELECT RELEASE_LOCK(?)', name
|
12
12
|
res = connection.select_one sql
|
13
13
|
(res && res.values.first == 1)
|
14
14
|
end
|
@@ -1,16 +1,22 @@
|
|
1
1
|
module DBLock
|
2
2
|
module Adapter
|
3
3
|
class Sqlserver < Base
|
4
|
-
def lock(name, timeout=0)
|
5
|
-
connection.execute_procedure 'sp_getapplock', Resource: name,
|
4
|
+
def lock(name, timeout = 0)
|
5
|
+
connection.execute_procedure 'sp_getapplock', Resource: name,
|
6
|
+
LockMode: 'Exclusive',
|
7
|
+
LockOwner: 'Session',
|
8
|
+
LockTimeout: (timeout * 1000).to_i,
|
9
|
+
DbPrincipal: 'public'
|
6
10
|
lock = connection.raw_connection.return_code
|
7
|
-
lock
|
11
|
+
lock.zero?
|
8
12
|
end
|
9
13
|
|
10
14
|
def release(name)
|
11
|
-
connection.execute_procedure 'sp_releaseapplock', Resource: name,
|
15
|
+
connection.execute_procedure 'sp_releaseapplock', Resource: name,
|
16
|
+
LockOwner: 'Session',
|
17
|
+
DbPrincipal: 'public'
|
12
18
|
lock = connection.raw_connection.return_code
|
13
|
-
lock
|
19
|
+
lock.zero?
|
14
20
|
end
|
15
21
|
end
|
16
22
|
end
|
data/lib/db_lock/adapter.rb
CHANGED
@@ -2,9 +2,9 @@ module DBLock
|
|
2
2
|
module Adapter
|
3
3
|
extend self
|
4
4
|
|
5
|
-
autoload :Base,
|
6
|
-
autoload :MYSQL,
|
7
|
-
autoload :Sqlserver,
|
5
|
+
autoload :Base, 'db_lock/adapter/base'
|
6
|
+
autoload :MYSQL, 'db_lock/adapter/mysql'
|
7
|
+
autoload :Sqlserver, 'db_lock/adapter/sqlserver'
|
8
8
|
|
9
9
|
delegate :lock, :release, to: :implementation
|
10
10
|
|
data/lib/db_lock/lock.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module DBLock
|
4
|
+
module Lock
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# rubocop:disable Metrics/AbcSize
|
8
|
+
def get(name, timeout = 0)
|
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
|
26
|
+
end
|
27
|
+
# rubocop:enable Metrics/AbcSize
|
28
|
+
|
29
|
+
def locked?
|
30
|
+
@locked ||= false
|
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
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/db_lock/version.rb
CHANGED
data/lib/db_lock.rb
CHANGED
@@ -3,7 +3,8 @@ require 'digest/md5'
|
|
3
3
|
module DBLock
|
4
4
|
extend self
|
5
5
|
|
6
|
-
autoload :Adapter,
|
6
|
+
autoload :Adapter, 'db_lock/adapter'
|
7
|
+
autoload :Lock, 'db_lock/lock'
|
7
8
|
|
8
9
|
class AlreadyLocked < StandardError; end
|
9
10
|
|
@@ -13,47 +14,4 @@ module DBLock
|
|
13
14
|
# this must be an active record base object or subclass
|
14
15
|
@db_handler || ActiveRecord::Base
|
15
16
|
end
|
16
|
-
|
17
|
-
module Lock
|
18
|
-
extend self
|
19
|
-
|
20
|
-
def get(name, timeout=0)
|
21
|
-
timeout = timeout.to_f # catches nil
|
22
|
-
timeout = 0 if timeout < 0
|
23
|
-
raise "Invalid lock name: #{name.inspect}" if name.empty?
|
24
|
-
raise AlreadyLocked.new("Already lock in progress") if locked?
|
25
|
-
|
26
|
-
name = generate_lock_name(name)
|
27
|
-
|
28
|
-
if Adapter.lock(name, timeout)
|
29
|
-
@locked = true
|
30
|
-
yield
|
31
|
-
else
|
32
|
-
raise AlreadyLocked.new("Unable to obtain lock '#{name}' within #{timeout} seconds") unless locked?
|
33
|
-
end
|
34
|
-
ensure
|
35
|
-
if locked?
|
36
|
-
Adapter.release(name)
|
37
|
-
end
|
38
|
-
@locked = false
|
39
|
-
end
|
40
|
-
|
41
|
-
def locked?
|
42
|
-
@locked ||= false
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def generate_lock_name(name)
|
48
|
-
if (name[0] == "." && defined? Rails)
|
49
|
-
name = "#{Rails.application.class.parent_name}.#{Rails.env}#{name}"
|
50
|
-
end
|
51
|
-
# reduce lock names of > 64 chars in size
|
52
|
-
# MySQL 5.7 only supports 64 chars max, there might be similar limitations elsewhere
|
53
|
-
if name.length > 64
|
54
|
-
name = "#{name.chars.first(15).join}-#{Digest::MD5.hexdigest(name)}-#{name.chars.last(15).join}"
|
55
|
-
end
|
56
|
-
name
|
57
|
-
end
|
58
|
-
end
|
59
17
|
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:
|
4
|
+
version: 0.8.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mkon
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,20 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5.2'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
22
|
+
version: '7.1'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
29
|
+
version: '5.2'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '7.1'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: rspec
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,8 +44,50 @@ dependencies:
|
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '3.7'
|
47
|
-
|
48
|
-
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rubocop
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - '='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.28.2
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - '='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.28.2
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rubocop-rspec
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - '='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 2.10.0
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - '='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 2.10.0
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: simplecov
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
description: Obtain manual db locks to guard blocks of code from parallel execution.Currently
|
90
|
+
only supports mysql and ms-sql-server.
|
49
91
|
email:
|
50
92
|
- konstantin@munteanu.de
|
51
93
|
executables: []
|
@@ -54,21 +96,19 @@ extra_rdoc_files: []
|
|
54
96
|
files:
|
55
97
|
- MIT-LICENSE
|
56
98
|
- README.md
|
57
|
-
- config/database_mssql.yml
|
58
|
-
- config/database_mssql_example.yml
|
59
|
-
- config/database_mysql.yml
|
60
|
-
- config/database_mysql_example.yml
|
61
99
|
- lib/db_lock.rb
|
62
100
|
- lib/db_lock/adapter.rb
|
63
101
|
- lib/db_lock/adapter/base.rb
|
64
102
|
- lib/db_lock/adapter/mysql.rb
|
65
103
|
- lib/db_lock/adapter/sqlserver.rb
|
104
|
+
- lib/db_lock/lock.rb
|
66
105
|
- lib/db_lock/version.rb
|
67
106
|
homepage: https://github.com/mkon/db_lock
|
68
107
|
licenses:
|
69
108
|
- MIT
|
70
|
-
metadata:
|
71
|
-
|
109
|
+
metadata:
|
110
|
+
rubygems_mfa_required: 'true'
|
111
|
+
post_install_message:
|
72
112
|
rdoc_options: []
|
73
113
|
require_paths:
|
74
114
|
- lib
|
@@ -76,16 +116,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
116
|
requirements:
|
77
117
|
- - ">="
|
78
118
|
- !ruby/object:Gem::Version
|
79
|
-
version: '
|
119
|
+
version: '2.7'
|
120
|
+
- - "<"
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '3.1'
|
80
123
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
124
|
requirements:
|
82
125
|
- - ">="
|
83
126
|
- !ruby/object:Gem::Version
|
84
127
|
version: '0'
|
85
128
|
requirements: []
|
86
|
-
|
87
|
-
|
88
|
-
signing_key:
|
129
|
+
rubygems_version: 3.2.15
|
130
|
+
signing_key:
|
89
131
|
specification_version: 4
|
90
132
|
summary: Obtain manual db/mysql locks
|
91
133
|
test_files: []
|
data/config/database_mssql.yml
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# rename/copy to database.yml and adjust to your local settings
|
2
|
-
|
3
|
-
base: &base
|
4
|
-
adapter: sqlserver
|
5
|
-
host: 192.168.99.100
|
6
|
-
port: 50012
|
7
|
-
encoding: utf8
|
8
|
-
reconnect: false
|
9
|
-
pool: 5
|
10
|
-
username: root
|
11
|
-
password: dummy
|
12
|
-
|
13
|
-
development:
|
14
|
-
<<: *base
|
15
|
-
|
16
|
-
test:
|
17
|
-
<<: *base
|
@@ -1,17 +0,0 @@
|
|
1
|
-
# rename/copy to database.yml and adjust to your local settings
|
2
|
-
|
3
|
-
base: &base
|
4
|
-
adapter: sqlserver
|
5
|
-
host: 192.168.99.100
|
6
|
-
port: 50012
|
7
|
-
encoding: utf8
|
8
|
-
reconnect: false
|
9
|
-
pool: 5
|
10
|
-
username: root
|
11
|
-
password: dummy
|
12
|
-
|
13
|
-
development:
|
14
|
-
<<: *base
|
15
|
-
|
16
|
-
test:
|
17
|
-
<<: *base
|
data/config/database_mysql.yml
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
# rename/copy to database.yml and adjust to your local settings
|
2
|
-
|
3
|
-
base: &base
|
4
|
-
adapter: mysql2
|
5
|
-
host: localhost
|
6
|
-
encoding: utf8
|
7
|
-
reconnect: false
|
8
|
-
pool: 5
|
9
|
-
username: root
|
10
|
-
password: dummy
|
11
|
-
|
12
|
-
development:
|
13
|
-
<<: *base
|
14
|
-
|
15
|
-
test:
|
16
|
-
<<: *base
|