mysql_framework 0.0.12 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/mysql_framework/scripts/lock_manager.rb +103 -0
- data/lib/mysql_framework/scripts/manager.rb +3 -11
- data/lib/mysql_framework/scripts.rb +1 -0
- data/lib/mysql_framework/version.rb +1 -1
- data/lib/mysql_framework.rb +0 -1
- data/spec/lib/mysql_framework/scripts/lock_manager_spec.rb +84 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3dccb6db17fc9881471f03e6450c885a168adae746b98b913847a2499df1fc1
|
4
|
+
data.tar.gz: 4fa04c68791c529d75ce4889695907c6fd5b60d06c800480550de2161608d532
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 60be2204e33c1044793119d9e8fa6340f5960df8a0430beb47672b6687465a435f13da528e2a60d6f6667871b89f3cd25136b1f0eddc1d991131b4626c867cd9
|
7
|
+
data.tar.gz: 32a4c64e9febc04f5135026e08095180acad6e9a2fd39f17d6fdca521f16978584ad3f3a3def0a0c0fc00da7a26660b6330d46350f8687f232670a13429d50b2
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redlock'
|
4
|
+
|
5
|
+
module MysqlFramework
|
6
|
+
module Scripts
|
7
|
+
class LockManager
|
8
|
+
def initialize
|
9
|
+
@pool = Queue.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# This method is called to request a lock (Default 5 minutes)
|
13
|
+
def request_lock(key:, ttl: default_ttl, max_attempts: default_max_retries, retry_delay: default_retry_delay)
|
14
|
+
MysqlFramework.logger.info { "[#{self.class}] - Requesting lock: #{key}." }
|
15
|
+
|
16
|
+
lock = false
|
17
|
+
count = 0
|
18
|
+
|
19
|
+
loop do
|
20
|
+
# request a lock
|
21
|
+
lock = with_client { |client| client.lock(key, ttl) }
|
22
|
+
|
23
|
+
# if lock was received break out of the loop
|
24
|
+
break if lock
|
25
|
+
|
26
|
+
# lock was not received so increment request count
|
27
|
+
count += 1
|
28
|
+
|
29
|
+
MysqlFramework.logger.debug do
|
30
|
+
"[#{self.class}] - Key is currently locked, waiting for lock: #{key} | Wait count: #{count}."
|
31
|
+
end
|
32
|
+
|
33
|
+
# check if lock requests have exceeded max request attempts
|
34
|
+
raise "Resource is already locked. Lock key: #{key}. Max attempt exceeded." if count == max_attempts
|
35
|
+
|
36
|
+
# sleep and try requesting the lock again
|
37
|
+
sleep(retry_delay)
|
38
|
+
end
|
39
|
+
|
40
|
+
lock
|
41
|
+
end
|
42
|
+
|
43
|
+
# This method is called to release a lock
|
44
|
+
def release_lock(key:, lock:)
|
45
|
+
return if lock.nil?
|
46
|
+
|
47
|
+
MysqlFramework.logger.info { "[#{self.class}] - Releasing lock: #{key}." }
|
48
|
+
|
49
|
+
with_client { |client| client.unlock(lock) }
|
50
|
+
end
|
51
|
+
|
52
|
+
# This method is called to request and release a lock around yielding to a user supplied block
|
53
|
+
def with_lock(key:, ttl: default_ttl, max_attempts: default_max_retries, retry_delay: default_retry_delay)
|
54
|
+
raise 'Block must be specified.' unless block_given?
|
55
|
+
|
56
|
+
begin
|
57
|
+
lock = request_lock(key: key, ttl: ttl, max_attempts: max_attempts, retry_delay: retry_delay)
|
58
|
+
yield
|
59
|
+
ensure
|
60
|
+
release_lock(key: key, lock: lock)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# This method is called to retrieve a Redlock client from the pool
|
65
|
+
def fetch_client
|
66
|
+
@pool.pop(true)
|
67
|
+
rescue StandardError
|
68
|
+
# By not letting redlock retry we will rely on the retry that happens in this class
|
69
|
+
Redlock::Client.new([redis_url], retry_jitter: retry_jitter, retry_count: 1, retry_delay: 0)
|
70
|
+
end
|
71
|
+
|
72
|
+
# This method is called to retrieve a Redlock client from the pool and yield it to a block
|
73
|
+
def with_client
|
74
|
+
client = fetch_client
|
75
|
+
yield client
|
76
|
+
ensure
|
77
|
+
@pool.push(client)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def redis_url
|
83
|
+
ENV.fetch('REDIS_URL')
|
84
|
+
end
|
85
|
+
|
86
|
+
def default_ttl
|
87
|
+
@default_ttl ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_TTL', 2000))
|
88
|
+
end
|
89
|
+
|
90
|
+
def default_max_retries
|
91
|
+
@default_max_retries ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_MAX_ATTEMPTS', 300))
|
92
|
+
end
|
93
|
+
|
94
|
+
def default_retry_delay
|
95
|
+
@default_retry_delay ||= Float(ENV.fetch('MYSQL_MIGRATION_LOCK_RETRY_DELAY_S', 1.0))
|
96
|
+
end
|
97
|
+
|
98
|
+
def retry_jitter
|
99
|
+
@retry_jitter ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_JITTER_MS', 50))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -8,9 +8,7 @@ module MysqlFramework
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def execute
|
11
|
-
lock_manager.
|
12
|
-
raise unless locked
|
13
|
-
|
11
|
+
lock_manager.with_lock(key: self.class) do
|
14
12
|
initialize_script_history
|
15
13
|
|
16
14
|
executed_scripts = retrieve_executed_scripts
|
@@ -29,9 +27,7 @@ module MysqlFramework
|
|
29
27
|
end
|
30
28
|
|
31
29
|
def apply_by_tag(tags)
|
32
|
-
lock_manager.
|
33
|
-
raise unless locked
|
34
|
-
|
30
|
+
lock_manager.with_lock(key: self.class) do
|
35
31
|
initialize_script_history
|
36
32
|
|
37
33
|
mysql_connector.transaction do |client|
|
@@ -113,11 +109,7 @@ module MysqlFramework
|
|
113
109
|
attr_reader :mysql_connector
|
114
110
|
|
115
111
|
def lock_manager
|
116
|
-
@lock_manager ||=
|
117
|
-
end
|
118
|
-
|
119
|
-
def migration_ttl
|
120
|
-
@migration_ttl ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_TTL', 2000))
|
112
|
+
@lock_manager ||= MysqlFramework::Scripts::LockManager.new
|
121
113
|
end
|
122
114
|
|
123
115
|
def migration_table_name
|
data/lib/mysql_framework.rb
CHANGED
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe MysqlFramework::Scripts::LockManager do
|
4
|
+
describe '#fetch_client' do
|
5
|
+
context 'when the connection pool is empty' do
|
6
|
+
it 'returns a new redlock client' do
|
7
|
+
expect(subject.fetch_client).to be_a(Redlock::Client)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'when the connection pool is NOT empty' do
|
12
|
+
let(:client) { Redlock::Client.new([ENV['REDIS_URL']]) }
|
13
|
+
|
14
|
+
before do
|
15
|
+
pool = subject.instance_variable_get(:@pool)
|
16
|
+
pool.push(client)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'returns a redlock client from the pool' do
|
20
|
+
expect(subject.fetch_client).to eq client
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#with_lock' do
|
26
|
+
let(:key) { SecureRandom.uuid }
|
27
|
+
|
28
|
+
context 'When key is NOT locked' do
|
29
|
+
context 'when a block is specified' do
|
30
|
+
let(:foo) { {} }
|
31
|
+
|
32
|
+
it 'requests a lock, executes the block and releases the lock' do
|
33
|
+
subject.with_lock(key: key, ttl: 1_000) do
|
34
|
+
foo[:bar] = 'abc'
|
35
|
+
sleep(0.1)
|
36
|
+
end
|
37
|
+
|
38
|
+
expect(foo[:bar]).to eq 'abc'
|
39
|
+
expect(subject.request_lock(key: key)).not_to be_nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#request_lock' do
|
46
|
+
let(:key) { SecureRandom.uuid }
|
47
|
+
|
48
|
+
context 'When key is NOT locked' do
|
49
|
+
it 'returns a lock' do
|
50
|
+
expect(subject.request_lock(key: key)).not_to be_nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'When key is locked' do
|
55
|
+
before { subject.request_lock(key: key, ttl: 3_000) }
|
56
|
+
|
57
|
+
context 'but the lock expires within the wait ttl' do
|
58
|
+
it 'returns a lock' do
|
59
|
+
expect(subject.request_lock(key: key, max_attempts: 5, retry_delay: 1)).not_to be_nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'raises a KeyLockError' do
|
64
|
+
expect { subject.request_lock(key: key, max_attempts: 1, retry_delay: 1) }.to raise_error(RuntimeError)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '#release_lock' do
|
70
|
+
let(:key) { SecureRandom.uuid }
|
71
|
+
let(:lock) { subject.request_lock(key: key) }
|
72
|
+
|
73
|
+
it 'releases the lock' do
|
74
|
+
expect { subject.release_lock(key: key, lock: lock) }.not_to raise_error
|
75
|
+
expect(subject.request_lock(key: key, max_attempts: 1, retry_delay: 1)).not_to be_nil
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'when lock is nil' do
|
79
|
+
it 'does not error' do
|
80
|
+
expect { subject.release_lock(key: key, lock: nil) }.not_to raise_error
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mysql_framework
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sage
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-11-
|
11
|
+
date: 2018-11-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -108,6 +108,7 @@ files:
|
|
108
108
|
- lib/mysql_framework/logger.rb
|
109
109
|
- lib/mysql_framework/scripts.rb
|
110
110
|
- lib/mysql_framework/scripts/base.rb
|
111
|
+
- lib/mysql_framework/scripts/lock_manager.rb
|
111
112
|
- lib/mysql_framework/scripts/manager.rb
|
112
113
|
- lib/mysql_framework/scripts/table.rb
|
113
114
|
- lib/mysql_framework/sql_column.rb
|
@@ -118,6 +119,7 @@ files:
|
|
118
119
|
- spec/lib/mysql_framework/connector_spec.rb
|
119
120
|
- spec/lib/mysql_framework/logger_spec.rb
|
120
121
|
- spec/lib/mysql_framework/scripts/base_spec.rb
|
122
|
+
- spec/lib/mysql_framework/scripts/lock_manager_spec.rb
|
121
123
|
- spec/lib/mysql_framework/scripts/manager_spec.rb
|
122
124
|
- spec/lib/mysql_framework/sql_column_spec.rb
|
123
125
|
- spec/lib/mysql_framework/sql_condition_spec.rb
|