activerecord-health 0.1.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 +7 -0
- data/CONTRIBUTING.md +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +239 -0
- data/Rakefile +25 -0
- data/docker-compose.yml +27 -0
- data/lib/activerecord/health/adapters/mysql_adapter.rb +60 -0
- data/lib/activerecord/health/adapters/postgresql_adapter.rb +23 -0
- data/lib/activerecord/health/configuration.rb +61 -0
- data/lib/activerecord/health/extensions.rb +44 -0
- data/lib/activerecord/health/railtie.rb +11 -0
- data/lib/activerecord/health/version.rb +7 -0
- data/lib/activerecord/health.rb +134 -0
- data/lib/activerecord-health.rb +3 -0
- data/test/integration/mysql_integration_test.rb +104 -0
- data/test/integration/postgresql_integration_test.rb +104 -0
- data/test/test_helper.rb +74 -0
- data/test/unit/adapters/mysql_adapter_test.rb +94 -0
- data/test/unit/adapters/postgresql_adapter_test.rb +25 -0
- data/test/unit/configuration_test.rb +140 -0
- data/test/unit/extensions_test.rb +113 -0
- data/test/unit/health_test.rb +189 -0
- data/test/unit/sheddable_test.rb +92 -0
- metadata +94 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "health/version"
|
|
4
|
+
require_relative "health/configuration"
|
|
5
|
+
require_relative "health/adapters/postgresql_adapter"
|
|
6
|
+
require_relative "health/adapters/mysql_adapter"
|
|
7
|
+
require_relative "health/railtie" if defined?(Rails::Railtie)
|
|
8
|
+
|
|
9
|
+
module ActiveRecord
|
|
10
|
+
module Health
|
|
11
|
+
QUERY_TIMEOUT = 1
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def configure
|
|
15
|
+
yield(configuration)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configuration
|
|
19
|
+
@configuration ||= Configuration.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset_configuration!
|
|
23
|
+
@configuration = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ok?(model: ActiveRecord::Base)
|
|
27
|
+
load_pct(model: model) <= config_for(model).threshold
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def load_pct(model: ActiveRecord::Base)
|
|
31
|
+
db_config_name = model.connection_db_config.name
|
|
32
|
+
cache_key = "activerecord_health:load_pct:#{db_config_name}"
|
|
33
|
+
|
|
34
|
+
read_from_cache(cache_key) { query_load_pct(model) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def sheddable(model: ActiveRecord::Base)
|
|
38
|
+
return false unless ok?(model: model)
|
|
39
|
+
yield
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def sheddable_pct(pct:, model: ActiveRecord::Base)
|
|
44
|
+
return false if load_pct(model: model) > pct
|
|
45
|
+
yield
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def config_for(model)
|
|
52
|
+
model_class = model.is_a?(Class) ? model : model.class
|
|
53
|
+
configuration.for_model(model_class)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def read_from_cache(cache_key)
|
|
57
|
+
cached_value = configuration.cache.read(cache_key)
|
|
58
|
+
return cached_value unless cached_value.nil?
|
|
59
|
+
|
|
60
|
+
value = yield
|
|
61
|
+
configuration.cache.write(cache_key, value, expires_in: configuration.cache_ttl)
|
|
62
|
+
value
|
|
63
|
+
rescue
|
|
64
|
+
0.0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def query_load_pct(model)
|
|
68
|
+
connection = model.connection
|
|
69
|
+
adapter = adapter_for(connection)
|
|
70
|
+
config = config_for(model)
|
|
71
|
+
db_config_name = model.connection_db_config.name
|
|
72
|
+
|
|
73
|
+
active_sessions = execute_with_timeout(connection, adapter.active_session_count_query)
|
|
74
|
+
load_pct = active_sessions.to_f / config.vcpu_count
|
|
75
|
+
|
|
76
|
+
instrument(db_config_name, load_pct, active_sessions)
|
|
77
|
+
|
|
78
|
+
load_pct
|
|
79
|
+
rescue
|
|
80
|
+
1.0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def execute_with_timeout(connection, query)
|
|
84
|
+
adapter_name = connection.adapter_name.downcase
|
|
85
|
+
|
|
86
|
+
case adapter_name
|
|
87
|
+
when /postgresql/
|
|
88
|
+
execute_with_postgresql_timeout(connection, query)
|
|
89
|
+
when /mysql/
|
|
90
|
+
execute_with_mysql_timeout(connection, query)
|
|
91
|
+
else
|
|
92
|
+
connection.select_value(query)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def execute_with_postgresql_timeout(connection, query)
|
|
97
|
+
connection.transaction do
|
|
98
|
+
connection.execute("SET LOCAL statement_timeout = '#{QUERY_TIMEOUT}s'")
|
|
99
|
+
connection.select_value(query)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def execute_with_mysql_timeout(connection, query)
|
|
104
|
+
connection.transaction do
|
|
105
|
+
connection.execute("SET max_execution_time = #{QUERY_TIMEOUT * 1000}")
|
|
106
|
+
connection.select_value(query)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def adapter_for(connection)
|
|
111
|
+
case connection.adapter_name.downcase
|
|
112
|
+
when /postgresql/
|
|
113
|
+
Adapters::PostgreSQLAdapter.new
|
|
114
|
+
when /mysql/
|
|
115
|
+
version = connection.select_value("SELECT VERSION()")
|
|
116
|
+
Adapters::MySQLAdapter.new(version)
|
|
117
|
+
else
|
|
118
|
+
raise "Unsupported database adapter: #{connection.adapter_name}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def instrument(db_config_name, load_pct, active_sessions)
|
|
123
|
+
return unless defined?(ActiveSupport::Notifications)
|
|
124
|
+
|
|
125
|
+
ActiveSupport::Notifications.instrument(
|
|
126
|
+
"health_check.activerecord_health",
|
|
127
|
+
database: db_config_name,
|
|
128
|
+
load_pct: load_pct,
|
|
129
|
+
active_sessions: active_sessions
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "mysql2"
|
|
5
|
+
|
|
6
|
+
class MySQLIntegrationTest < ActiveRecord::Health::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
super
|
|
9
|
+
skip "MySQL not available" unless mysql_available?
|
|
10
|
+
|
|
11
|
+
ActiveRecord::Base.establish_connection(
|
|
12
|
+
adapter: "mysql2",
|
|
13
|
+
host: mysql_host,
|
|
14
|
+
port: ENV.fetch("MYSQL_PORT", 3306).to_i,
|
|
15
|
+
username: ENV.fetch("MYSQL_USER", "root"),
|
|
16
|
+
password: ENV.fetch("MYSQL_PASSWORD", "root"),
|
|
17
|
+
database: ENV.fetch("MYSQL_DB", "activerecord_health_test")
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@cache = MockCache.new
|
|
21
|
+
@baseline_sessions = count_active_sessions
|
|
22
|
+
ActiveRecord::Health.configure do |config|
|
|
23
|
+
config.vcpu_count = @baseline_sessions + 4
|
|
24
|
+
config.threshold = 0.75
|
|
25
|
+
config.cache = @cache
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def teardown
|
|
30
|
+
@sleep_threads&.each(&:kill)
|
|
31
|
+
@sleep_threads&.each(&:join)
|
|
32
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_load_pct_increases_with_active_sessions
|
|
37
|
+
baseline_load = ActiveRecord::Health.load_pct(model: ActiveRecord::Base)
|
|
38
|
+
@cache.clear
|
|
39
|
+
|
|
40
|
+
spawn_sleeping_connections(2)
|
|
41
|
+
with_load = ActiveRecord::Health.load_pct(model: ActiveRecord::Base)
|
|
42
|
+
|
|
43
|
+
expected_increase = 2.0 / (@baseline_sessions + 4)
|
|
44
|
+
assert_in_delta baseline_load + expected_increase, with_load, 0.01
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_ok_returns_true_when_below_threshold
|
|
48
|
+
@cache.clear
|
|
49
|
+
|
|
50
|
+
assert ActiveRecord::Health.ok?(model: ActiveRecord::Base)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_ok_returns_false_when_above_threshold
|
|
54
|
+
@cache.clear
|
|
55
|
+
spawn_sleeping_connections(4)
|
|
56
|
+
|
|
57
|
+
refute ActiveRecord::Health.ok?(model: ActiveRecord::Base)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def count_active_sessions
|
|
61
|
+
version = ActiveRecord::Base.connection.select_value("SELECT VERSION()")
|
|
62
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new(version)
|
|
63
|
+
ActiveRecord::Base.connection.select_value(adapter.active_session_count_query).to_i
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def mysql_available?
|
|
69
|
+
Mysql2::Client.new(mysql_config).close
|
|
70
|
+
true
|
|
71
|
+
rescue Mysql2::Error => e
|
|
72
|
+
warn "MySQL connection failed: #{e.message}"
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def spawn_sleeping_connections(count)
|
|
77
|
+
@sleep_threads = count.times.map do
|
|
78
|
+
Thread.new do
|
|
79
|
+
client = Mysql2::Client.new(mysql_config)
|
|
80
|
+
client.query("SELECT SLEEP(30)")
|
|
81
|
+
rescue
|
|
82
|
+
ensure
|
|
83
|
+
client&.close
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sleep 0.5
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def mysql_config
|
|
91
|
+
{
|
|
92
|
+
host: mysql_host,
|
|
93
|
+
port: ENV.fetch("MYSQL_PORT", 3306).to_i,
|
|
94
|
+
username: ENV.fetch("MYSQL_USER", "root"),
|
|
95
|
+
password: ENV.fetch("MYSQL_PASSWORD", "root"),
|
|
96
|
+
database: ENV.fetch("MYSQL_DB", "activerecord_health_test")
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def mysql_host
|
|
101
|
+
host = ENV.fetch("MYSQL_HOST", "127.0.0.1")
|
|
102
|
+
(host == "localhost") ? "127.0.0.1" : host
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "pg"
|
|
5
|
+
|
|
6
|
+
class PostgreSQLIntegrationTest < ActiveRecord::Health::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
super
|
|
9
|
+
skip "PostgreSQL not available" unless postgresql_available?
|
|
10
|
+
|
|
11
|
+
ActiveRecord::Base.establish_connection(
|
|
12
|
+
adapter: "postgresql",
|
|
13
|
+
host: ENV.fetch("POSTGRES_HOST", "localhost"),
|
|
14
|
+
port: ENV.fetch("POSTGRES_PORT", 5432),
|
|
15
|
+
username: ENV.fetch("POSTGRES_USER", "postgres"),
|
|
16
|
+
password: ENV.fetch("POSTGRES_PASSWORD", "postgres"),
|
|
17
|
+
database: ENV.fetch("POSTGRES_DB", "activerecord_health_test")
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@cache = MockCache.new
|
|
21
|
+
@baseline_sessions = count_active_sessions
|
|
22
|
+
ActiveRecord::Health.configure do |config|
|
|
23
|
+
config.vcpu_count = @baseline_sessions + 4
|
|
24
|
+
config.threshold = 0.75
|
|
25
|
+
config.cache = @cache
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def teardown
|
|
30
|
+
@sleep_threads&.each(&:kill)
|
|
31
|
+
@sleep_threads&.each(&:join)
|
|
32
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_load_pct_increases_with_active_sessions
|
|
37
|
+
baseline_load = ActiveRecord::Health.load_pct(model: ActiveRecord::Base)
|
|
38
|
+
@cache.clear
|
|
39
|
+
|
|
40
|
+
spawn_sleeping_connections(2)
|
|
41
|
+
with_load = ActiveRecord::Health.load_pct(model: ActiveRecord::Base)
|
|
42
|
+
|
|
43
|
+
expected_increase = 2.0 / (@baseline_sessions + 4)
|
|
44
|
+
assert_in_delta baseline_load + expected_increase, with_load, 0.01
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_ok_returns_true_when_below_threshold
|
|
48
|
+
@cache.clear
|
|
49
|
+
|
|
50
|
+
assert ActiveRecord::Health.ok?(model: ActiveRecord::Base)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_ok_returns_false_when_above_threshold
|
|
54
|
+
@cache.clear
|
|
55
|
+
spawn_sleeping_connections(4)
|
|
56
|
+
|
|
57
|
+
refute ActiveRecord::Health.ok?(model: ActiveRecord::Base)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def count_active_sessions
|
|
61
|
+
ActiveRecord::Base.connection.select_value(<<~SQL).to_i
|
|
62
|
+
SELECT count(*)
|
|
63
|
+
FROM pg_stat_activity
|
|
64
|
+
WHERE state = 'active'
|
|
65
|
+
AND backend_type = 'client backend'
|
|
66
|
+
AND pid != pg_backend_pid()
|
|
67
|
+
SQL
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def postgresql_available?
|
|
73
|
+
PG.connect(
|
|
74
|
+
host: ENV.fetch("POSTGRES_HOST", "localhost"),
|
|
75
|
+
port: ENV.fetch("POSTGRES_PORT", 5432),
|
|
76
|
+
user: ENV.fetch("POSTGRES_USER", "postgres"),
|
|
77
|
+
password: ENV.fetch("POSTGRES_PASSWORD", "postgres"),
|
|
78
|
+
dbname: ENV.fetch("POSTGRES_DB", "activerecord_health_test")
|
|
79
|
+
).close
|
|
80
|
+
true
|
|
81
|
+
rescue PG::ConnectionBad
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def spawn_sleeping_connections(count)
|
|
86
|
+
@sleep_threads = count.times.map do
|
|
87
|
+
Thread.new do
|
|
88
|
+
conn = PG.connect(
|
|
89
|
+
host: ENV.fetch("POSTGRES_HOST", "localhost"),
|
|
90
|
+
port: ENV.fetch("POSTGRES_PORT", 5432),
|
|
91
|
+
user: ENV.fetch("POSTGRES_USER", "postgres"),
|
|
92
|
+
password: ENV.fetch("POSTGRES_PASSWORD", "postgres"),
|
|
93
|
+
dbname: ENV.fetch("POSTGRES_DB", "activerecord_health_test")
|
|
94
|
+
)
|
|
95
|
+
conn.exec("SELECT pg_sleep(30)")
|
|
96
|
+
rescue
|
|
97
|
+
ensure
|
|
98
|
+
conn&.close
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
sleep 0.5
|
|
103
|
+
end
|
|
104
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
4
|
+
|
|
5
|
+
require "active_record"
|
|
6
|
+
require "active_support"
|
|
7
|
+
require "active_support/core_ext/string"
|
|
8
|
+
require "active_support/cache"
|
|
9
|
+
require "activerecord/health"
|
|
10
|
+
|
|
11
|
+
require "minitest/autorun"
|
|
12
|
+
require "minitest/reporters"
|
|
13
|
+
|
|
14
|
+
Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new
|
|
15
|
+
|
|
16
|
+
if ENV["FAIL_ON_SKIP"]
|
|
17
|
+
module Minitest
|
|
18
|
+
class << self
|
|
19
|
+
alias_method :original_run, :run
|
|
20
|
+
|
|
21
|
+
def run(args = [])
|
|
22
|
+
result = original_run(args)
|
|
23
|
+
reporter = Minitest::Reporters.reporters.first
|
|
24
|
+
if reporter && reporter.skips > 0
|
|
25
|
+
warn "\nCI failed: #{reporter.skips} tests were skipped"
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class ActiveRecord::Health::TestCase < Minitest::Test
|
|
35
|
+
def setup
|
|
36
|
+
ActiveRecord::Health.reset_configuration!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def teardown
|
|
40
|
+
ActiveRecord::Health.reset_configuration!
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class MockCache
|
|
45
|
+
def initialize
|
|
46
|
+
@store = {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def read(key)
|
|
50
|
+
@store[key]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def write(key, value, options = {})
|
|
54
|
+
@store[key] = value
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def delete(key)
|
|
58
|
+
@store.delete(key)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def clear
|
|
62
|
+
@store.clear
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class FailingCache
|
|
67
|
+
def read(key)
|
|
68
|
+
raise "Cache connection failed"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write(key, value, options = {})
|
|
72
|
+
raise "Cache connection failed"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class MySQLAdapterTest < ActiveRecord::Health::TestCase
|
|
6
|
+
def test_active_session_count_query_for_mysql_8_0_22_plus
|
|
7
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new("8.0.22")
|
|
8
|
+
|
|
9
|
+
expected_query = <<~SQL.squish
|
|
10
|
+
SELECT COUNT(*)
|
|
11
|
+
FROM performance_schema.processlist
|
|
12
|
+
WHERE COMMAND != 'Sleep'
|
|
13
|
+
AND ID != CONNECTION_ID()
|
|
14
|
+
AND USER NOT IN ('event_scheduler', 'system user')
|
|
15
|
+
SQL
|
|
16
|
+
|
|
17
|
+
assert_equal expected_query, adapter.active_session_count_query
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_active_session_count_query_for_mysql_8_0_21
|
|
21
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new("8.0.21")
|
|
22
|
+
|
|
23
|
+
expected_query = <<~SQL.squish
|
|
24
|
+
SELECT COUNT(*)
|
|
25
|
+
FROM information_schema.processlist
|
|
26
|
+
WHERE Command != 'Sleep'
|
|
27
|
+
AND ID != CONNECTION_ID()
|
|
28
|
+
AND User NOT IN ('event_scheduler', 'system user')
|
|
29
|
+
AND Command NOT IN ('Binlog Dump', 'Binlog Dump GTID')
|
|
30
|
+
SQL
|
|
31
|
+
|
|
32
|
+
assert_equal expected_query, adapter.active_session_count_query
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_active_session_count_query_for_mysql_5
|
|
36
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new("5.7.35")
|
|
37
|
+
|
|
38
|
+
expected_query = <<~SQL.squish
|
|
39
|
+
SELECT COUNT(*)
|
|
40
|
+
FROM information_schema.processlist
|
|
41
|
+
WHERE Command != 'Sleep'
|
|
42
|
+
AND ID != CONNECTION_ID()
|
|
43
|
+
AND User NOT IN ('event_scheduler', 'system user')
|
|
44
|
+
AND Command NOT IN ('Binlog Dump', 'Binlog Dump GTID')
|
|
45
|
+
SQL
|
|
46
|
+
|
|
47
|
+
assert_equal expected_query, adapter.active_session_count_query
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_active_session_count_query_for_mariadb
|
|
51
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new("10.5.12-MariaDB")
|
|
52
|
+
|
|
53
|
+
expected_query = <<~SQL.squish
|
|
54
|
+
SELECT COUNT(*)
|
|
55
|
+
FROM information_schema.processlist
|
|
56
|
+
WHERE Command != 'Sleep'
|
|
57
|
+
AND ID != CONNECTION_ID()
|
|
58
|
+
AND User NOT IN ('event_scheduler', 'system user')
|
|
59
|
+
AND Command NOT IN ('Binlog Dump', 'Binlog Dump GTID')
|
|
60
|
+
SQL
|
|
61
|
+
|
|
62
|
+
assert_equal expected_query, adapter.active_session_count_query
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_adapter_name
|
|
66
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new("8.0.22")
|
|
67
|
+
|
|
68
|
+
assert_equal :mysql, adapter.name
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_uses_performance_schema_returns_true_for_mysql_8_0_22_plus
|
|
72
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new("8.0.22")
|
|
73
|
+
|
|
74
|
+
assert adapter.uses_performance_schema?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_uses_performance_schema_returns_true_for_mysql_8_1
|
|
78
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new("8.1.0")
|
|
79
|
+
|
|
80
|
+
assert adapter.uses_performance_schema?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_uses_performance_schema_returns_false_for_mysql_8_0_21
|
|
84
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new("8.0.21")
|
|
85
|
+
|
|
86
|
+
refute adapter.uses_performance_schema?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_uses_performance_schema_returns_false_for_mariadb
|
|
90
|
+
adapter = ActiveRecord::Health::Adapters::MySQLAdapter.new("10.5.12-MariaDB")
|
|
91
|
+
|
|
92
|
+
refute adapter.uses_performance_schema?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class PostgreSQLAdapterTest < ActiveRecord::Health::TestCase
|
|
6
|
+
def test_active_session_count_query
|
|
7
|
+
adapter = ActiveRecord::Health::Adapters::PostgreSQLAdapter.new
|
|
8
|
+
|
|
9
|
+
expected_query = <<~SQL.squish
|
|
10
|
+
SELECT count(*)
|
|
11
|
+
FROM pg_stat_activity
|
|
12
|
+
WHERE state = 'active'
|
|
13
|
+
AND backend_type = 'client backend'
|
|
14
|
+
AND pid != pg_backend_pid()
|
|
15
|
+
SQL
|
|
16
|
+
|
|
17
|
+
assert_equal expected_query, adapter.active_session_count_query
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_adapter_name
|
|
21
|
+
adapter = ActiveRecord::Health::Adapters::PostgreSQLAdapter.new
|
|
22
|
+
|
|
23
|
+
assert_equal :postgresql, adapter.name
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class ConfigurationTest < ActiveRecord::Health::TestCase
|
|
6
|
+
def test_configure_sets_vcpu_count
|
|
7
|
+
ActiveRecord::Health.configure do |config|
|
|
8
|
+
config.vcpu_count = 16
|
|
9
|
+
config.cache = MockCache.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
assert_equal 16, ActiveRecord::Health.configuration.vcpu_count
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_configure_sets_threshold_with_default
|
|
16
|
+
ActiveRecord::Health.configure do |config|
|
|
17
|
+
config.vcpu_count = 16
|
|
18
|
+
config.cache = MockCache.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
assert_equal 0.75, ActiveRecord::Health.configuration.threshold
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_configure_sets_custom_threshold
|
|
25
|
+
ActiveRecord::Health.configure do |config|
|
|
26
|
+
config.vcpu_count = 16
|
|
27
|
+
config.threshold = 0.5
|
|
28
|
+
config.cache = MockCache.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
assert_equal 0.5, ActiveRecord::Health.configuration.threshold
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_configure_sets_cache
|
|
35
|
+
cache = MockCache.new
|
|
36
|
+
ActiveRecord::Health.configure do |config|
|
|
37
|
+
config.vcpu_count = 16
|
|
38
|
+
config.cache = cache
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
assert_same cache, ActiveRecord::Health.configuration.cache
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_configure_sets_cache_ttl_with_default
|
|
45
|
+
ActiveRecord::Health.configure do |config|
|
|
46
|
+
config.vcpu_count = 16
|
|
47
|
+
config.cache = MockCache.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
assert_equal 60, ActiveRecord::Health.configuration.cache_ttl
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_configure_sets_custom_cache_ttl
|
|
54
|
+
ActiveRecord::Health.configure do |config|
|
|
55
|
+
config.vcpu_count = 16
|
|
56
|
+
config.cache = MockCache.new
|
|
57
|
+
config.cache_ttl = 120
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
assert_equal 120, ActiveRecord::Health.configuration.cache_ttl
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_raises_without_vcpu_count
|
|
64
|
+
ActiveRecord::Health.configure do |config|
|
|
65
|
+
config.cache = MockCache.new
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
error = assert_raises(ActiveRecord::Health::ConfigurationError) do
|
|
69
|
+
ActiveRecord::Health.configuration.validate!
|
|
70
|
+
end
|
|
71
|
+
assert_match(/vcpu_count/, error.message)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_raises_without_cache
|
|
75
|
+
ActiveRecord::Health.configure do |config|
|
|
76
|
+
config.vcpu_count = 16
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
error = assert_raises(ActiveRecord::Health::ConfigurationError) do
|
|
80
|
+
ActiveRecord::Health.configuration.validate!
|
|
81
|
+
end
|
|
82
|
+
assert_match(/cache/, error.message)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_for_model_configures_specific_database
|
|
86
|
+
ActiveRecord::Health.configure do |config|
|
|
87
|
+
config.vcpu_count = 16
|
|
88
|
+
config.cache = MockCache.new
|
|
89
|
+
|
|
90
|
+
config.for_model(AnimalsRecord) do |db|
|
|
91
|
+
db.vcpu_count = 8
|
|
92
|
+
db.threshold = 0.5
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
assert_equal 16, ActiveRecord::Health.configuration.vcpu_count
|
|
97
|
+
assert_equal 8, ActiveRecord::Health.configuration.for_model(AnimalsRecord).vcpu_count
|
|
98
|
+
assert_equal 0.5, ActiveRecord::Health.configuration.for_model(AnimalsRecord).threshold
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_for_model_inherits_cache_from_default
|
|
102
|
+
cache = MockCache.new
|
|
103
|
+
ActiveRecord::Health.configure do |config|
|
|
104
|
+
config.vcpu_count = 16
|
|
105
|
+
config.cache = cache
|
|
106
|
+
|
|
107
|
+
config.for_model(AnimalsRecord) do |db|
|
|
108
|
+
db.vcpu_count = 8
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
assert_same cache, ActiveRecord::Health.configuration.for_model(AnimalsRecord).cache
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_for_model_inherits_threshold_from_default_when_not_specified
|
|
116
|
+
ActiveRecord::Health.configure do |config|
|
|
117
|
+
config.vcpu_count = 16
|
|
118
|
+
config.cache = MockCache.new
|
|
119
|
+
|
|
120
|
+
config.for_model(AnimalsRecord) do |db|
|
|
121
|
+
db.vcpu_count = 8
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
assert_equal 0.75, ActiveRecord::Health.configuration.for_model(AnimalsRecord).threshold
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_max_healthy_sessions_calculated_correctly
|
|
129
|
+
ActiveRecord::Health.configure do |config|
|
|
130
|
+
config.vcpu_count = 16
|
|
131
|
+
config.threshold = 0.75
|
|
132
|
+
config.cache = MockCache.new
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
assert_equal 12, ActiveRecord::Health.configuration.max_healthy_sessions
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
class AnimalsRecord
|
|
140
|
+
end
|