Skip to content

Commit b1182d3

Browse files
committed
Move to use activerecord connectin pooling
1 parent 1d98c86 commit b1182d3

9 files changed

Lines changed: 410 additions & 280 deletions

File tree

.github/workflows/rspec.yml

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77

88
services:
99
mysql:
10-
image: mysql:5.7
10+
image: mysql:8.0
1111
ports:
1212
- 3306:3306
1313
env:
@@ -22,16 +22,8 @@ jobs:
2222
- uses: actions/checkout@v2
2323
- uses: ruby/setup-ruby@v1
2424
with:
25-
ruby-version: 2.4
25+
ruby-version: 3.4
2626
bundler-cache: true
2727

2828
- name: Run tests
2929
run: bundle exec rspec
30-
31-
- name: Code Coverage
32-
uses: paambaati/codeclimate-action@v2.7.5
33-
env:
34-
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
35-
with:
36-
coverageLocations: |
37-
${{github.workspace}}/coverage/.resultset.json:simplecov

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: '2.1'
22

33
services:
44
test-runner:
5-
image: ruby:2.4
5+
image: ruby:3.4
66
working_dir: /usr/src/app
77
container_name: test-runner
88
command: sh -c "while true; do echo 'Container is running..'; sleep 5; done"
@@ -21,7 +21,7 @@ services:
2121

2222
test-mysql:
2323
container_name: test-mysql
24-
image: mysql:5.7
24+
image: mysql:8
2525
restart: always
2626
environment:
2727
MYSQL_ROOT_PASSWORD: admin

lib/mysql_framework/connector.rb

Lines changed: 87 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,104 @@
11
# frozen_string_literal: true
22

3+
require 'active_record'
4+
require 'active_record/connection_adapters/mysql2_adapter'
5+
6+
# Monkeypatch the MySQL2 adapter to return hashes with symbol keys by default
7+
module MysqlFramework
8+
module Mysql2AdapterPatch
9+
def configure_connection
10+
super
11+
@raw_connection.query_options[:as] = :hash
12+
@raw_connection.query_options[:symbolize_keys] = true
13+
@raw_connection.query_options[:cast_booleans] = true
14+
end
15+
end
16+
end
17+
18+
ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(MysqlFramework::Mysql2AdapterPatch)
19+
320
module MysqlFramework
421
class Connector
522
def initialize(options = {})
623
@options = default_options.merge(options)
7-
@mutex = Mutex.new
8-
9-
Mysql2::Client.default_query_options.merge!(symbolize_keys: true, cast_booleans: true)
24+
@connection_map = nil
25+
@map_mutex = Mutex.new
26+
@setup_mutex = Mutex.new
27+
@setup_complete = false
1028
end
1129

12-
# This method is called to setup a pool of MySQL connections.
30+
# This method is called to setup the ActiveRecord connection pool.
1331
def setup
14-
return unless connection_pool_enabled?
32+
return if @setup_complete
1533

16-
@connection_pool = ::Queue.new
34+
@setup_mutex.synchronize do
35+
return if @setup_complete
1736

18-
start_pool_size.times { @connection_pool.push(new_client) }
19-
20-
@created_connections = start_pool_size
37+
ActiveRecord::Base.establish_connection(active_record_config)
38+
@connection_map = {}
39+
@setup_complete = true
40+
end
2141
end
2242

2343
# This method is called to close all MySQL connections in the pool and dispose of the pool itself.
2444
def dispose
25-
return if @connection_pool.nil?
45+
return unless @setup_complete
2646

27-
until @connection_pool.empty?
28-
conn = @connection_pool.pop(true)
29-
conn&.close
47+
ActiveRecord::Base.connection_pool.disconnect!
48+
49+
@map_mutex.synchronize do
50+
@connection_map.clear
3051
end
3152

32-
@connection_pool = nil
53+
@setup_complete = false
3354
end
3455

35-
# This method is called to get the idle connection queue for this connector.
56+
# This method is called to get the connection pool for this connector.
3657
def connections
37-
@connection_pool
58+
return nil unless @setup_complete
59+
60+
ActiveRecord::Base.connection_pool
3861
end
3962

4063
# This method is called to fetch a client from the connection pool.
4164
def check_out
42-
@mutex.synchronize do
43-
begin
44-
return new_client unless connection_pool_enabled?
45-
46-
client = @connection_pool.pop(true)
47-
48-
client.ping if @options[:reconnect]
49-
50-
client
51-
rescue ThreadError
52-
if @created_connections < max_pool_size
53-
client = new_client
54-
@created_connections += 1
55-
return client
56-
end
65+
setup unless @setup_complete
5766

58-
MysqlFramework.logger.error { "[#{self.class}] - Database connection pool depleted." }
67+
adapter = ActiveRecord::Base.connection_pool.checkout
68+
raw_conn = adapter.raw_connection
5969

60-
raise 'Database connection pool depleted.'
61-
end
70+
@map_mutex.synchronize do
71+
@connection_map[raw_conn.object_id] = adapter
6272
end
73+
74+
raw_conn
6375
end
6476

6577
# This method is called to check a client back in to the connection when no longer needed.
6678
def check_in(client)
67-
@mutex.synchronize do
68-
return client&.close unless connection_pool_enabled?
79+
return if client.nil? || !@setup_complete
80+
81+
adapter = @map_mutex.synchronize do
82+
@connection_map.delete(client.object_id)
83+
end
6984

70-
client = new_client if client&.closed?
71-
@connection_pool.push(client)
85+
if adapter
86+
ActiveRecord::Base.connection_pool.checkin(adapter)
87+
else
88+
MysqlFramework.logger.warn { "[#{self.class}] - Unable to find adapter for raw connection during check_in" }
7289
end
7390
end
7491

7592
# This method is called to use a client from the connection pool.
7693
def with_client(provided = nil)
77-
client = provided || check_out
78-
yield client
79-
ensure
80-
check_in(client) if client && !provided
94+
if provided
95+
yield provided
96+
else
97+
setup unless @setup_complete
98+
ActiveRecord::Base.connection_pool.with_connection do |connection|
99+
yield connection.raw_connection
100+
end
101+
end
81102
end
82103

83104
# This method is called to execute a prepared statement
@@ -87,14 +108,12 @@ def with_client(provided = nil)
87108
# running different queries at the same time.
88109
def execute(query, provided_client = nil)
89110
with_client(provided_client) do |client|
90-
begin
91-
statement = client.prepare(query.sql)
92-
result = statement.execute(*query.params)
93-
result&.to_a
94-
ensure
95-
result&.free
96-
statement&.close
97-
end
111+
statement = client.prepare(query.sql)
112+
result = statement.execute(*query.params)
113+
result&.to_a
114+
ensure
115+
result&.free
116+
statement&.close
98117
end
99118
end
100119

@@ -146,20 +165,28 @@ def default_options
146165
}
147166
end
148167

149-
def new_client
150-
Mysql2::Client.new(@options)
151-
end
152-
153-
def connection_pool_enabled?
154-
@connection_pool_enabled ||= ENV.fetch('MYSQL_CONNECTION_POOL_ENABLED', 'true').casecmp?('true')
155-
end
156-
157-
def start_pool_size
158-
@start_pool_size ||= Integer(ENV.fetch('MYSQL_START_POOL_SIZE', 1))
168+
def active_record_config
169+
{
170+
adapter: 'mysql2',
171+
host: @options[:host],
172+
port: @options[:port],
173+
database: @options[:database],
174+
username: @options[:username],
175+
password: @options[:password],
176+
reconnect: @options[:reconnect],
177+
read_timeout: @options[:read_timeout],
178+
write_timeout: @options[:write_timeout],
179+
pool: max_pool_size,
180+
checkout_timeout: pool_timeout
181+
}
159182
end
160183

161184
def max_pool_size
162185
@max_pool_size ||= Integer(ENV.fetch('MYSQL_MAX_POOL_SIZE', 5))
163186
end
187+
188+
def pool_timeout
189+
@pool_timeout ||= Integer(ENV.fetch('MYSQL_POOL_TIMEOUT', 5))
190+
end
164191
end
165192
end

lib/mysql_framework/scripts/lock_manager.rb

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
# frozen_string_literal: true
22

33
require 'redlock'
4+
require 'connection_pool'
45

56
module MysqlFramework
67
module Scripts
78
class LockManager
89
def initialize
9-
@pool = Queue.new
10+
@pool = ConnectionPool.new(size: pool_size, timeout: pool_timeout) do
11+
# By not letting redlock retry we will rely on the retry that happens in this class
12+
Redlock::Client.new([redis_url], retry_jitter: retry_jitter, retry_count: 1, retry_delay: 0)
13+
end
1014
end
1115

1216
# This method is called to request a lock (Default 5 minutes)
@@ -63,18 +67,12 @@ def with_lock(key:, ttl: default_ttl, max_attempts: default_max_retries, retry_d
6367

6468
# This method is called to retrieve a Redlock client from the pool
6569
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+
@pool.checkout
7071
end
7172

7273
# This method is called to retrieve a Redlock client from the pool and yield it to a block
7374
def with_client
74-
client = fetch_client
75-
yield client
76-
ensure
77-
@pool.push(client)
75+
@pool.with { |client| yield client }
7876
end
7977

8078
private
@@ -98,6 +96,14 @@ def default_retry_delay
9896
def retry_jitter
9997
@retry_jitter ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_JITTER_MS', 50))
10098
end
99+
100+
def pool_size
101+
@pool_size ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_POOL_SIZE', 5))
102+
end
103+
104+
def pool_timeout
105+
@pool_timeout ||= Integer(ENV.fetch('MYSQL_MIGRATION_LOCK_POOL_TIMEOUT', 5))
106+
end
101107
end
102108
end
103109
end

lib/mysql_framework/scripts/manager.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def execute
2727
end
2828

2929
def apply_by_tag(tags)
30-
lock_manager.with_lock(key: self.class) do
30+
lock_manager.with_lock(key: self.class.name) do
3131
initialize_script_history
3232

3333
mysql_connector.transaction do |client|

lib/mysql_framework/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module MysqlFramework
4-
VERSION = '2.1.9'
4+
VERSION = '2.1.9''
55
end

mysql_framework.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ Gem::Specification.new do |spec|
2626

2727
spec.add_dependency 'mysql2', '~> 0.4'
2828
spec.add_dependency 'redlock'
29+
spec.add_dependency 'connection_pool'
30+
spec.add_dependency 'activerecord', '~> 8.1', '>= 8.1.1'
2931
end

0 commit comments

Comments
 (0)