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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d259c4f34ff15a7c344bdfd29788a81e09a9d02bb3eba445fb8826658bb20fe
4
- data.tar.gz: 2cb83a4a3082c83605694b20d3c67e89f99b1e3f494b98d182759eefeab43888
3
+ metadata.gz: b3dccb6db17fc9881471f03e6450c885a168adae746b98b913847a2499df1fc1
4
+ data.tar.gz: 4fa04c68791c529d75ce4889695907c6fd5b60d06c800480550de2161608d532
5
5
  SHA512:
6
- metadata.gz: 363b29a8cc5ac738d72ad5b6406f3e3bb7c2897364f7cdee4df72b4769af02a78ce31781baa1545060fc51d266c584dcfc02c155e614048c4e80eaa9986c34af
7
- data.tar.gz: 962a2dd53afc91a9ab80cf1f6df071577e36b197f7b981a03962425ac7c444da2603fae5a492c5265630f8f8e90f627b79006211943a9d47a63a6a9d676ab338
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.lock(self.class, migration_ttl) do |locked|
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.lock(self.class, migration_ttl) do |locked|
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 ||= Redlock::Client.new([ENV.fetch('REDIS_URL')])
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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'scripts/base'
4
+ require_relative 'scripts/lock_manager'
4
5
  require_relative 'scripts/manager'
5
6
  require_relative 'scripts/table'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MysqlFramework
4
- VERSION = '0.0.12'
4
+ VERSION = '0.1.1'
5
5
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'mysql2'
4
- require 'redlock'
5
4
 
6
5
  require_relative 'mysql_framework/connector'
7
6
  require_relative 'mysql_framework/logger'
@@ -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.0.12
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-27 00:00:00.000000000 Z
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