mysql_framework 0.0.12 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|