mysql_framework 2.1.9 → 2.3.0
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/connector.rb +124 -85
- data/lib/mysql_framework/mysql_connection_pool.rb +176 -0
- data/lib/mysql_framework/scripts/manager.rb +1 -1
- data/lib/mysql_framework/stats/aws_metric_publisher.rb +124 -0
- data/lib/mysql_framework/stats/dimension_map.rb +51 -0
- data/lib/mysql_framework/version.rb +1 -1
- data/spec/lib/mysql_framework/connector_spec.rb +125 -148
- data/spec/lib/mysql_framework/mysql_connection_pool_spec.rb +239 -0
- data/spec/lib/mysql_framework/stats/aws_metric_publisher_spec.rb +89 -0
- data/spec/support/fixtures.rb +10 -0
- metadata +47 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e86d1afb0cbbd571d728e469bf5fffa14ad3e0f89f268dcf951564aca98043a
|
|
4
|
+
data.tar.gz: be53e02e828f37e5d7c651b1526b353e8dc599df0396b6b28f50632efecbfd9f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e4fae53d9831f913be250ace9a5b4d5ad85e89433561b7be804a11b0e30f6faf434195fb34b2b4c8d31b9669ac486da07e5dd5a65ee17ca1d52d172bd13b824
|
|
7
|
+
data.tar.gz: 4ee13ac87b94966188ddd33a2d9b090192f7bddac0e4bf7623d14553176fa1706fa58b83728fc89746383c60e12165f4f092052013bffb51a03218895269ec21
|
|
@@ -1,133 +1,171 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'mysql_connection_pool'
|
|
4
|
+
|
|
3
5
|
module MysqlFramework
|
|
4
6
|
class Connector
|
|
7
|
+
attr_reader :connection_pool
|
|
8
|
+
|
|
9
|
+
# Initializes a connector instance with MySQL client options.
|
|
10
|
+
#
|
|
11
|
+
# @param options [Hash] custom MySQL client options that override defaults
|
|
12
|
+
# @return [void]
|
|
5
13
|
def initialize(options = {})
|
|
6
14
|
@options = default_options.merge(options)
|
|
7
|
-
@mutex = Mutex.new
|
|
8
|
-
|
|
9
15
|
Mysql2::Client.default_query_options.merge!(symbolize_keys: true, cast_booleans: true)
|
|
10
16
|
end
|
|
11
17
|
|
|
12
|
-
#
|
|
18
|
+
# Sets up the MySQL connection pool when pooling is enabled.
|
|
19
|
+
#
|
|
20
|
+
# @return [ConnectionPool, nil] configured pool, or nil when pooling is disabled
|
|
13
21
|
def setup
|
|
14
22
|
return unless connection_pool_enabled?
|
|
15
23
|
|
|
16
|
-
@connection_pool = ::
|
|
17
|
-
|
|
18
|
-
start_pool_size.times { @connection_pool.push(new_client) }
|
|
19
|
-
|
|
20
|
-
@created_connections = start_pool_size
|
|
24
|
+
@connection_pool = MysqlFramework::MysqlConnectionPool.new(@options)
|
|
25
|
+
@connection_pool.setup
|
|
21
26
|
end
|
|
22
27
|
|
|
23
|
-
#
|
|
28
|
+
# Disposes of the connection pool and closes pooled connections.
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
24
31
|
def dispose
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
until @connection_pool.empty?
|
|
28
|
-
conn = @connection_pool.pop(true)
|
|
29
|
-
conn&.close
|
|
30
|
-
end
|
|
32
|
+
return unless connection_pool_enabled?
|
|
31
33
|
|
|
34
|
+
@connection_pool&.dispose
|
|
32
35
|
@connection_pool = nil
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# This method is called to fetch a client from the connection pool.
|
|
38
|
+
# Checks out a MySQL client, sanitizing it before use.
|
|
39
|
+
#
|
|
40
|
+
# @return [Mysql2::Client] checked-out client
|
|
41
|
+
# @raise [ConnectionSanitizationError] when sanitization repeatedly fails
|
|
42
|
+
# @raise [Mysql2::Error] when checkout or sanitization fails due to MySQL errors
|
|
41
43
|
def check_out
|
|
42
|
-
|
|
43
|
-
begin
|
|
44
|
-
return new_client unless connection_pool_enabled?
|
|
45
|
-
|
|
46
|
-
client = @connection_pool.pop(true)
|
|
47
|
-
|
|
48
|
-
client.ping if @options[:reconnect]
|
|
44
|
+
return new_client unless connection_pool_enabled?
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
rescue ThreadError
|
|
52
|
-
if @created_connections < max_pool_size
|
|
53
|
-
client = new_client
|
|
54
|
-
@created_connections += 1
|
|
55
|
-
return client
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
MysqlFramework.logger.error { "[#{self.class}] - Database connection pool depleted." }
|
|
59
|
-
|
|
60
|
-
raise 'Database connection pool depleted.'
|
|
61
|
-
end
|
|
62
|
-
end
|
|
46
|
+
@connection_pool.check_out
|
|
63
47
|
end
|
|
64
48
|
|
|
65
|
-
#
|
|
49
|
+
# Returns a MySQL client back to the pool or closes it when pooling is disabled.
|
|
50
|
+
#
|
|
51
|
+
# @param client [Mysql2::Client, nil] client to return or close
|
|
52
|
+
# @return [void]
|
|
66
53
|
def check_in(client)
|
|
67
|
-
|
|
68
|
-
return client&.close unless connection_pool_enabled?
|
|
54
|
+
return client&.close unless connection_pool_enabled?
|
|
69
55
|
|
|
70
|
-
|
|
71
|
-
@connection_pool.push(client)
|
|
72
|
-
end
|
|
56
|
+
@connection_pool.check_in(client)
|
|
73
57
|
end
|
|
74
58
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
# Yields a MySQL client from the pool, or yields the provided client directly.
|
|
60
|
+
#
|
|
61
|
+
# @param provided_client [Mysql2::Client, nil] existing client to yield without pool checkout
|
|
62
|
+
# @param discard_current_pool_connection [Boolean] whether to discard the pooled connection after use
|
|
63
|
+
# @yield [client] block that performs work with a MySQL client
|
|
64
|
+
# @yieldparam client [Mysql2::Client]
|
|
65
|
+
# @return [Object] block result
|
|
66
|
+
# @raise [Mysql2::Error] re-raises MySQL errors from the block
|
|
67
|
+
def with_client(provided_client = nil, discard_current_pool_connection: false)
|
|
68
|
+
return yield provided_client if provided_client
|
|
69
|
+
return with_new_client { |c| yield c } unless connection_pool_enabled?
|
|
70
|
+
|
|
71
|
+
@connection_pool.with_client(discard_current_pool_connection:) { |c| yield c }
|
|
81
72
|
end
|
|
82
73
|
|
|
83
|
-
#
|
|
74
|
+
# Executes a prepared statement.
|
|
75
|
+
#
|
|
76
|
+
# @param query [Object] query object responding to +sql+ and +params+
|
|
77
|
+
# @param provided_client [Mysql2::Client, nil] optional existing client
|
|
78
|
+
# @return [Array<Hash>, nil] query result rows
|
|
79
|
+
# @raise [Mysql2::Error] when statement preparation or execution fails
|
|
84
80
|
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
81
|
+
# NOTE:
|
|
82
|
+
# We must always free the result and close the prepared statement.
|
|
83
|
+
# Otherwise MySQL may raise "Commands out of sync" when the same
|
|
84
|
+
# connection is reused (e.g. via connection pooling).
|
|
85
|
+
#
|
|
86
|
+
# The connection itself must NOT be closed here because it is
|
|
87
|
+
# managed by the connection pool.
|
|
88
88
|
def execute(query, provided_client = nil)
|
|
89
89
|
with_client(provided_client) do |client|
|
|
90
|
+
statement = nil
|
|
91
|
+
result = nil
|
|
92
|
+
|
|
90
93
|
begin
|
|
91
94
|
statement = client.prepare(query.sql)
|
|
92
|
-
result = statement.execute(
|
|
93
|
-
|
|
95
|
+
result = statement.execute(
|
|
96
|
+
*query.params, symbolize_keys: true, cast_booleans: true
|
|
97
|
+
)
|
|
98
|
+
final = result&.to_a
|
|
99
|
+
final
|
|
94
100
|
ensure
|
|
101
|
+
client&.abandon_results!
|
|
95
102
|
result&.free
|
|
96
103
|
statement&.close
|
|
97
104
|
end
|
|
98
105
|
end
|
|
99
106
|
end
|
|
100
107
|
|
|
101
|
-
#
|
|
108
|
+
# Executes a SQL query.
|
|
109
|
+
#
|
|
110
|
+
# @param query_string [String] SQL query to execute
|
|
111
|
+
# @param provided_client [Mysql2::Client, nil] optional existing client
|
|
112
|
+
# @return [Mysql2::Result] raw MySQL result
|
|
113
|
+
# @raise [Mysql2::Error] when query execution fails
|
|
102
114
|
def query(query_string, provided_client = nil)
|
|
103
|
-
with_client(provided_client) { |
|
|
115
|
+
with_client(provided_client) { |conn| conn.query(query_string) }
|
|
104
116
|
end
|
|
105
117
|
|
|
106
|
-
#
|
|
118
|
+
# Executes a multi-statement SQL query and collects all result sets.
|
|
119
|
+
#
|
|
120
|
+
# @param query_string [String] multi-statement SQL query
|
|
121
|
+
# @param provided_client [Mysql2::Client, nil] optional existing client
|
|
122
|
+
# @return [Array<Array<Hash>>] list of result sets
|
|
123
|
+
# @raise [Mysql2::Error] when query execution or result fetching fails
|
|
107
124
|
def query_multiple_results(query_string, provided_client = nil)
|
|
108
|
-
results =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
results = nil
|
|
126
|
+
|
|
127
|
+
# Multiple statement query is buggy and client cannot be reused after calling next_result/store_result
|
|
128
|
+
# Client's state gets corrupted and leaks into next queries. The reason is unknown.
|
|
129
|
+
# As a result we do not return client back to the pool but instead close connection which is not optimal.
|
|
130
|
+
with_client(provided_client, discard_current_pool_connection: true) do |client|
|
|
131
|
+
raw_results = []
|
|
132
|
+
query_call = client.query(query_string)
|
|
133
|
+
raw_results << query_call&.to_a
|
|
134
|
+
query_call&.free
|
|
135
|
+
|
|
136
|
+
while client.more_results?
|
|
137
|
+
client.next_result
|
|
138
|
+
query_call = client.store_result
|
|
139
|
+
raw_results << query_call&.to_a
|
|
140
|
+
query_call&.free
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
results = raw_results.compact
|
|
144
|
+
results
|
|
145
|
+
ensure
|
|
146
|
+
client&.abandon_results!
|
|
113
147
|
end
|
|
114
148
|
|
|
115
|
-
results
|
|
149
|
+
results
|
|
116
150
|
end
|
|
117
151
|
|
|
118
|
-
#
|
|
152
|
+
# Executes a block within a database transaction.
|
|
153
|
+
#
|
|
154
|
+
# @yield [client] block executed between BEGIN and COMMIT
|
|
155
|
+
# @yieldparam client [Mysql2::Client]
|
|
156
|
+
# @return [Object] block result
|
|
157
|
+
# @raise [ArgumentError] when no block is given
|
|
158
|
+
# @raise [StandardError] re-raises any exception after rollback
|
|
119
159
|
def transaction
|
|
120
160
|
raise ArgumentError, 'No block was given' unless block_given?
|
|
121
161
|
|
|
122
162
|
with_client do |client|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
raise e
|
|
130
|
-
end
|
|
163
|
+
client.query('BEGIN')
|
|
164
|
+
yield client
|
|
165
|
+
client.query('COMMIT')
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
client.query('ROLLBACK')
|
|
168
|
+
raise e
|
|
131
169
|
end
|
|
132
170
|
end
|
|
133
171
|
|
|
@@ -146,20 +184,21 @@ module MysqlFramework
|
|
|
146
184
|
}
|
|
147
185
|
end
|
|
148
186
|
|
|
187
|
+
def with_new_client
|
|
188
|
+
client = new_client
|
|
189
|
+
yield client
|
|
190
|
+
ensure
|
|
191
|
+
client&.close
|
|
192
|
+
end
|
|
193
|
+
|
|
149
194
|
def new_client
|
|
150
195
|
Mysql2::Client.new(@options)
|
|
151
196
|
end
|
|
152
197
|
|
|
153
198
|
def connection_pool_enabled?
|
|
154
|
-
@connection_pool_enabled
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def start_pool_size
|
|
158
|
-
@start_pool_size ||= Integer(ENV.fetch('MYSQL_START_POOL_SIZE', 1))
|
|
159
|
-
end
|
|
199
|
+
return @connection_pool_enabled unless @connection_pool_enabled.nil?
|
|
160
200
|
|
|
161
|
-
|
|
162
|
-
@max_pool_size ||= Integer(ENV.fetch('MYSQL_MAX_POOL_SIZE', 5))
|
|
201
|
+
@connection_pool_enabled = ENV.fetch('MYSQL_CONNECTION_POOL_ENABLED', 'true').casecmp?('true')
|
|
163
202
|
end
|
|
164
203
|
end
|
|
165
204
|
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'connection_pool'
|
|
4
|
+
|
|
5
|
+
module MysqlFramework
|
|
6
|
+
class MysqlConnectionPool
|
|
7
|
+
class ConnectionSanitizationError < StandardError; end
|
|
8
|
+
|
|
9
|
+
CLEAN_IDLE_CONNECTIONS_THREAD_NAME = 'clean-idle-connections'
|
|
10
|
+
|
|
11
|
+
attr_reader :connections
|
|
12
|
+
|
|
13
|
+
# Initializes a connection pool instance with MySQL client options.
|
|
14
|
+
#
|
|
15
|
+
# @param options [Hash] MySQL client options passed to each pooled connection
|
|
16
|
+
# @return [void]
|
|
17
|
+
def initialize(options)
|
|
18
|
+
@options = options
|
|
19
|
+
@setup_mutex = Mutex.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Sets up the MySQL connection pool. Idempotent — safe to call more than once.
|
|
23
|
+
#
|
|
24
|
+
# @return [ConnectionPool] configured pool
|
|
25
|
+
def setup
|
|
26
|
+
@setup_mutex.synchronize do
|
|
27
|
+
return if connections
|
|
28
|
+
|
|
29
|
+
@connections = ConnectionPool.new(size: max_pool_size, timeout: pool_timeout) do
|
|
30
|
+
Mysql2::Client.new(@options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
start_clean_idle_connections_thread
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Disposes of the connection pool and closes pooled connections.
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
40
|
+
def dispose
|
|
41
|
+
@setup_mutex.synchronize do
|
|
42
|
+
dispose_clean_idle_connections_thread
|
|
43
|
+
connections&.shutdown(&:close)
|
|
44
|
+
@connections = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns key connection-pool metrics for monitoring.
|
|
49
|
+
#
|
|
50
|
+
# @return [Hash{Symbol => Integer}] pool size and availability metrics
|
|
51
|
+
def pool_stats
|
|
52
|
+
return { size: 0, available: 0, idle: 0 } if connections.nil?
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
size: connections.size,
|
|
56
|
+
available: connections.available,
|
|
57
|
+
idle: connections.idle
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Checks out a MySQL client, sanitizing it before use.
|
|
62
|
+
#
|
|
63
|
+
# @return [Mysql2::Client] checked-out client
|
|
64
|
+
# @raise [ConnectionSanitizationError] when sanitization repeatedly fails
|
|
65
|
+
# @raise [Mysql2::Error] when checkout or sanitization fails due to MySQL errors
|
|
66
|
+
def check_out
|
|
67
|
+
sanitization_retries = 0
|
|
68
|
+
begin
|
|
69
|
+
conn = connections.checkout
|
|
70
|
+
sanitize_connection!(conn)
|
|
71
|
+
conn
|
|
72
|
+
rescue ConnectionSanitizationError
|
|
73
|
+
discard_current_connection!
|
|
74
|
+
sanitization_retries += 1
|
|
75
|
+
retry if sanitization_retries <= 1
|
|
76
|
+
raise
|
|
77
|
+
rescue Mysql2::Error
|
|
78
|
+
discard_current_connection!
|
|
79
|
+
raise
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns a MySQL client back to the pool or closes it when pooling is disabled.
|
|
84
|
+
#
|
|
85
|
+
# @param client [Mysql2::Client, nil] client to return or close
|
|
86
|
+
# @return [void]
|
|
87
|
+
def check_in(client)
|
|
88
|
+
return if client.nil?
|
|
89
|
+
|
|
90
|
+
discard_current_connection! if client.closed?
|
|
91
|
+
connections.checkin
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Yields a MySQL client from the pool, or yields the provided client directly.
|
|
95
|
+
#
|
|
96
|
+
# @param provided_client [Mysql2::Client, nil] existing client to yield without pool checkout
|
|
97
|
+
# @param discard_current_pool_connection [Boolean] whether to discard the pooled connection after use
|
|
98
|
+
# @yield [client] block that performs work with a MySQL client
|
|
99
|
+
# @yieldparam client [Mysql2::Client]
|
|
100
|
+
# @return [Object] block result
|
|
101
|
+
# @raise [Mysql2::Error] re-raises MySQL errors from the block
|
|
102
|
+
def with_client(discard_current_pool_connection: false)
|
|
103
|
+
sanitization_retries = 0
|
|
104
|
+
|
|
105
|
+
begin
|
|
106
|
+
connections.with do |conn|
|
|
107
|
+
sanitize_connection!(conn)
|
|
108
|
+
yield conn
|
|
109
|
+
rescue ConnectionSanitizationError, Mysql2::Error
|
|
110
|
+
discard_current_connection!
|
|
111
|
+
raise
|
|
112
|
+
ensure
|
|
113
|
+
discard_current_connection! if discard_current_pool_connection
|
|
114
|
+
end
|
|
115
|
+
rescue ConnectionSanitizationError
|
|
116
|
+
sanitization_retries += 1
|
|
117
|
+
retry if sanitization_retries <= 1
|
|
118
|
+
raise
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def start_clean_idle_connections_thread
|
|
125
|
+
thread_name = "#{CLEAN_IDLE_CONNECTIONS_THREAD_NAME}-#{object_id}"
|
|
126
|
+
@idle_connections_thread = Thread.new do
|
|
127
|
+
Thread.current.name = thread_name
|
|
128
|
+
loop do
|
|
129
|
+
sleep idle_reap_loop_time
|
|
130
|
+
break unless Thread.current == @idle_connections_thread
|
|
131
|
+
|
|
132
|
+
connections&.reap(idle_seconds: idle_timeout, &:close)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@idle_connections_thread.abort_on_exception # = false
|
|
137
|
+
@idle_connections_thread
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def dispose_clean_idle_connections_thread
|
|
141
|
+
@idle_connections_thread&.join(5)
|
|
142
|
+
@idle_connections_thread&.kill
|
|
143
|
+
@idle_connections_thread = nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def sanitize_connection!(conn)
|
|
147
|
+
conn.ping
|
|
148
|
+
conn.abandon_results!
|
|
149
|
+
conn.query('ROLLBACK')
|
|
150
|
+
rescue Mysql2::Error => e
|
|
151
|
+
raise ConnectionSanitizationError, "Connection sanitization failed: #{e.message}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def discard_current_connection!
|
|
155
|
+
connections&.discard_current_connection(&:close)
|
|
156
|
+
rescue StandardError
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def max_pool_size
|
|
161
|
+
@max_pool_size ||= Integer(ENV.fetch('MYSQL_MAX_POOL_SIZE', 5))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def pool_timeout
|
|
165
|
+
@pool_timeout ||= Integer(ENV.fetch('MYSQL_POOL_TIMEOUT', 5))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def idle_timeout
|
|
169
|
+
@idle_timeout ||= Integer(ENV.fetch('MYSQL_POOL_IDLE_TIMEOUT', 300))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def idle_reap_loop_time
|
|
173
|
+
@idle_reap_loop_time ||= Integer(ENV.fetch('MYSQL_POOL_IDLE_REAP_TIME', 60))
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aws-sdk-cloudwatch'
|
|
4
|
+
require_relative 'dimension_map'
|
|
5
|
+
|
|
6
|
+
module MysqlFramework
|
|
7
|
+
module Stats
|
|
8
|
+
class AwsMetricPublisher
|
|
9
|
+
THREAD_NAME = 'mysql-connector-pool-stats'
|
|
10
|
+
JOIN_TIMEOUT = 5 # seconds to wait for clean thread exit before force-killing
|
|
11
|
+
METRIC_UNIT = 'Count'
|
|
12
|
+
METRIC_NAME_MAP = {
|
|
13
|
+
size: 'MysqlConnectionPoolSize',
|
|
14
|
+
available: 'MysqlConnectionPoolAvailable',
|
|
15
|
+
idle: 'MysqlConnectionPoolIdle'
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# Initializes AWS metric publishing dependencies.
|
|
19
|
+
#
|
|
20
|
+
# @param connector [MysqlFramework::Connector, nil] connector used to read connection-pool stats
|
|
21
|
+
# @param dimension_map [MysqlFramework::Stats::DimensionMap, nil] CloudWatch namespace and dimensions
|
|
22
|
+
# @param cloudwatch_client [Aws::CloudWatch::Client, nil] CloudWatch client instance
|
|
23
|
+
# @param publish_interval [Integer] metric publish interval in seconds
|
|
24
|
+
# @return [void]
|
|
25
|
+
def initialize(
|
|
26
|
+
connector: nil,
|
|
27
|
+
dimension_map: nil,
|
|
28
|
+
cloudwatch_client: nil,
|
|
29
|
+
publish_interval: 300
|
|
30
|
+
)
|
|
31
|
+
@thread = nil
|
|
32
|
+
@connector = connector
|
|
33
|
+
@cloudwatch_client = cloudwatch_client
|
|
34
|
+
@dimension_map = dimension_map || MysqlFramework::Stats::DimensionMap.new
|
|
35
|
+
@publish_interval = publish_interval
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Spawns the background sampling thread. Safe to call more than once –
|
|
39
|
+
# subsequent calls are no-ops while the thread is already running.
|
|
40
|
+
#
|
|
41
|
+
# @return [Thread, nil] reporter thread when started, or nil when already running
|
|
42
|
+
def start
|
|
43
|
+
return if running?
|
|
44
|
+
|
|
45
|
+
thread_name = "#{THREAD_NAME}-#{object_id}"
|
|
46
|
+
thread = Thread.new do
|
|
47
|
+
Thread.current.name = thread_name
|
|
48
|
+
loop do
|
|
49
|
+
sleep @publish_interval
|
|
50
|
+
break unless Thread.current == @thread
|
|
51
|
+
|
|
52
|
+
sample
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
thread.abort_on_exception = false
|
|
57
|
+
@thread = thread
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Cooperatively stops the background thread and waits up to JOIN_TIMEOUT
|
|
61
|
+
# seconds for it to exit before force-killing it.
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
64
|
+
def stop
|
|
65
|
+
thread = @thread
|
|
66
|
+
@thread = nil # cooperative stop signal: loop checks this after each sleep
|
|
67
|
+
thread&.join(JOIN_TIMEOUT)
|
|
68
|
+
thread&.kill # force-kill only if still alive after timeout
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns true when the reporter thread is alive.
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def running?
|
|
75
|
+
@thread&.alive? || false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Reads pool stats and publishes them to CloudWatch using a low-cardinality
|
|
81
|
+
# dimension set so all ECS tasks for the same service aggregate together.
|
|
82
|
+
# Errors are swallowed and logged so that a reporting failure never
|
|
83
|
+
# propagates to the caller.
|
|
84
|
+
def sample
|
|
85
|
+
connection_pool = @connector&.connection_pool
|
|
86
|
+
return if connection_pool.nil?
|
|
87
|
+
|
|
88
|
+
stats = connection_pool.pool_stats
|
|
89
|
+
metric_data = build_metric_data(stats)
|
|
90
|
+
return if metric_data.empty?
|
|
91
|
+
|
|
92
|
+
MysqlFramework.logger.debug { "[#{self.class}] - CloudWatch/#{@dimension_map.namespace} - #{stats.inspect}" }
|
|
93
|
+
|
|
94
|
+
cloudwatch_client.put_metric_data(
|
|
95
|
+
namespace: @dimension_map.namespace,
|
|
96
|
+
metric_data: metric_data
|
|
97
|
+
)
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
MysqlFramework.logger.error { "[#{self.class}] - Failed to record pool stats: #{e.message}" }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_metric_data(stats)
|
|
103
|
+
timestamp = Time.now.utc
|
|
104
|
+
|
|
105
|
+
METRIC_NAME_MAP.filter_map do |key, metric_name|
|
|
106
|
+
value = stats[key]
|
|
107
|
+
next if value.nil?
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
metric_name: metric_name,
|
|
111
|
+
dimensions: @dimension_map.to_cloudwatch_dimensions,
|
|
112
|
+
timestamp: timestamp,
|
|
113
|
+
unit: METRIC_UNIT,
|
|
114
|
+
value: value.to_f
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def cloudwatch_client
|
|
120
|
+
@cloudwatch_client ||= Aws::CloudWatch::Client.new
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MysqlFramework
|
|
4
|
+
module Stats
|
|
5
|
+
# Class to handle dimensions for AWS reporting
|
|
6
|
+
class DimensionMap
|
|
7
|
+
attr_accessor :service_name, :application, :environment, :landscape, :namespace
|
|
8
|
+
|
|
9
|
+
# Initializes dimension values used for CloudWatch metrics.
|
|
10
|
+
#
|
|
11
|
+
# @param service_name [String, nil] service dimension
|
|
12
|
+
# @param application [String, nil] application dimension
|
|
13
|
+
# @param environment [String, nil] environment dimension
|
|
14
|
+
# @param landscape [String, nil] landscape dimension
|
|
15
|
+
# @param namespace [String, nil] CloudWatch namespace override
|
|
16
|
+
# @return [void]
|
|
17
|
+
def initialize(
|
|
18
|
+
service_name: nil,
|
|
19
|
+
application: nil,
|
|
20
|
+
environment: nil,
|
|
21
|
+
landscape: nil,
|
|
22
|
+
namespace: nil
|
|
23
|
+
)
|
|
24
|
+
@service_name = service_name
|
|
25
|
+
@application = application
|
|
26
|
+
@environment = environment
|
|
27
|
+
@landscape = landscape
|
|
28
|
+
@namespace = namespace
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Builds CloudWatch dimensions from configured values or environment variables.
|
|
32
|
+
#
|
|
33
|
+
# @return [Array<Hash{Symbol => String}>] dimensions with non-nil values only
|
|
34
|
+
def to_cloudwatch_dimensions
|
|
35
|
+
[
|
|
36
|
+
{ name: 'ServiceName', value: service_name || ENV.fetch('SERVICE_NAME', nil) },
|
|
37
|
+
{ name: 'Application', value: application || ENV.fetch('APPLICATION', nil) },
|
|
38
|
+
{ name: 'Environment', value: environment || ENV.fetch('ENVIRONMENT', nil) },
|
|
39
|
+
{ name: 'Landscape', value: landscape || ENV.fetch('LANDSCAPE', nil) }
|
|
40
|
+
].reject { |dimension| dimension[:value].nil? }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the CloudWatch namespace.
|
|
44
|
+
#
|
|
45
|
+
# @return [String] configured namespace or default namespace value
|
|
46
|
+
def namespace
|
|
47
|
+
@namespace || ENV.fetch('AWS_METRICS_NAMESPACE', 'MysqlFramework')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|