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 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