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.
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "activerecord/health/extensions"
5
+
6
+ class ConnectionExtensionTest < ActiveRecord::Health::TestCase
7
+ def test_healthy_returns_true_when_below_threshold
8
+ cache = MockCache.new
9
+ cache.write("activerecord_health:load_pct:primary", 0.5)
10
+
11
+ ActiveRecord::Health.configure do |config|
12
+ config.vcpu_count = 16
13
+ config.threshold = 0.75
14
+ config.cache = cache
15
+ end
16
+
17
+ connection = MockConnectionWithExtension.new("primary")
18
+ assert connection.healthy?
19
+ end
20
+
21
+ def test_healthy_returns_false_when_above_threshold
22
+ cache = MockCache.new
23
+ cache.write("activerecord_health:load_pct:primary", 0.9)
24
+
25
+ ActiveRecord::Health.configure do |config|
26
+ config.vcpu_count = 16
27
+ config.threshold = 0.75
28
+ config.cache = cache
29
+ end
30
+
31
+ connection = MockConnectionWithExtension.new("primary")
32
+ refute connection.healthy?
33
+ end
34
+
35
+ def test_load_pct_returns_cached_value
36
+ cache = MockCache.new
37
+ cache.write("activerecord_health:load_pct:primary", 0.625)
38
+
39
+ ActiveRecord::Health.configure do |config|
40
+ config.vcpu_count = 16
41
+ config.cache = cache
42
+ end
43
+
44
+ connection = MockConnectionWithExtension.new("primary")
45
+ assert_equal 0.625, connection.load_pct
46
+ end
47
+ end
48
+
49
+ class ModelExtensionTest < ActiveRecord::Health::TestCase
50
+ def test_database_healthy_returns_true_when_below_threshold
51
+ cache = MockCache.new
52
+ cache.write("activerecord_health:load_pct:primary", 0.5)
53
+
54
+ ActiveRecord::Health.configure do |config|
55
+ config.vcpu_count = 16
56
+ config.threshold = 0.75
57
+ config.cache = cache
58
+ end
59
+
60
+ assert MockModelWithExtension.database_healthy?
61
+ end
62
+
63
+ def test_database_healthy_returns_false_when_above_threshold
64
+ cache = MockCache.new
65
+ cache.write("activerecord_health:load_pct:primary", 0.9)
66
+
67
+ ActiveRecord::Health.configure do |config|
68
+ config.vcpu_count = 16
69
+ config.threshold = 0.75
70
+ config.cache = cache
71
+ end
72
+
73
+ refute MockModelWithExtension.database_healthy?
74
+ end
75
+ end
76
+
77
+ MockExtensionDbConfig = Struct.new(:name)
78
+
79
+ class MockConnectionWithExtension
80
+ include ActiveRecord::Health::ConnectionExtension
81
+
82
+ def initialize(db_config_name)
83
+ @db_config_name = db_config_name
84
+ end
85
+
86
+ def adapter_name
87
+ "PostgreSQL"
88
+ end
89
+
90
+ def select_value(query)
91
+ 0
92
+ end
93
+
94
+ def pool
95
+ self
96
+ end
97
+
98
+ def db_config
99
+ MockExtensionDbConfig.new(@db_config_name)
100
+ end
101
+ end
102
+
103
+ class MockModelWithExtension
104
+ extend ActiveRecord::Health::ModelExtension
105
+
106
+ def self.connection_db_config
107
+ MockExtensionDbConfig.new("primary")
108
+ end
109
+
110
+ def self.connection
111
+ MockConnectionWithExtension.new("primary")
112
+ end
113
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HealthTest < ActiveRecord::Health::TestCase
6
+ def test_ok_returns_true_when_load_below_threshold
7
+ cache = MockCache.new
8
+ cache.write("activerecord_health:load_pct:primary", 0.5)
9
+
10
+ ActiveRecord::Health.configure do |config|
11
+ config.vcpu_count = 16
12
+ config.threshold = 0.75
13
+ config.cache = cache
14
+ end
15
+
16
+ mock_model = MockModel.new("primary")
17
+ assert ActiveRecord::Health.ok?(model: mock_model)
18
+ end
19
+
20
+ def test_ok_returns_false_when_load_above_threshold
21
+ cache = MockCache.new
22
+ cache.write("activerecord_health:load_pct:primary", 0.9)
23
+
24
+ ActiveRecord::Health.configure do |config|
25
+ config.vcpu_count = 16
26
+ config.threshold = 0.75
27
+ config.cache = cache
28
+ end
29
+
30
+ mock_model = MockModel.new("primary")
31
+ refute ActiveRecord::Health.ok?(model: mock_model)
32
+ end
33
+
34
+ def test_ok_returns_true_when_load_equals_threshold
35
+ cache = MockCache.new
36
+ cache.write("activerecord_health:load_pct:primary", 0.75)
37
+
38
+ ActiveRecord::Health.configure do |config|
39
+ config.vcpu_count = 16
40
+ config.threshold = 0.75
41
+ config.cache = cache
42
+ end
43
+
44
+ mock_model = MockModel.new("primary")
45
+ assert ActiveRecord::Health.ok?(model: mock_model)
46
+ end
47
+
48
+ def test_ok_returns_true_when_cache_fails
49
+ ActiveRecord::Health.configure do |config|
50
+ config.vcpu_count = 16
51
+ config.cache = FailingCache.new
52
+ end
53
+
54
+ mock_model = MockModel.new("primary")
55
+ assert ActiveRecord::Health.ok?(model: mock_model)
56
+ end
57
+
58
+ def test_load_pct_returns_cached_value
59
+ cache = MockCache.new
60
+ cache.write("activerecord_health:load_pct:primary", 0.625)
61
+
62
+ ActiveRecord::Health.configure do |config|
63
+ config.vcpu_count = 16
64
+ config.cache = cache
65
+ end
66
+
67
+ mock_model = MockModel.new("primary")
68
+ assert_equal 0.625, ActiveRecord::Health.load_pct(model: mock_model)
69
+ end
70
+
71
+ def test_load_pct_returns_zero_when_cache_fails
72
+ ActiveRecord::Health.configure do |config|
73
+ config.vcpu_count = 16
74
+ config.cache = FailingCache.new
75
+ end
76
+
77
+ mock_model = MockModel.new("primary")
78
+ assert_equal 0.0, ActiveRecord::Health.load_pct(model: mock_model)
79
+ end
80
+
81
+ def test_load_pct_queries_database_when_not_cached
82
+ cache = MockCache.new
83
+
84
+ ActiveRecord::Health.configure do |config|
85
+ config.vcpu_count = 16
86
+ config.cache = cache
87
+ end
88
+
89
+ connection = MockConnection.new(active_session_count: 8)
90
+ mock_model = MockModel.new("primary", connection)
91
+
92
+ assert_equal 0.5, ActiveRecord::Health.load_pct(model: mock_model)
93
+ assert_equal 0.5, cache.read("activerecord_health:load_pct:primary")
94
+ end
95
+
96
+ def test_load_pct_returns_1_0_when_database_query_fails
97
+ cache = MockCache.new
98
+
99
+ ActiveRecord::Health.configure do |config|
100
+ config.vcpu_count = 16
101
+ config.cache = cache
102
+ end
103
+
104
+ connection = MockConnection.new(should_fail: true)
105
+ mock_model = MockModel.new("primary", connection)
106
+
107
+ assert_equal 1.0, ActiveRecord::Health.load_pct(model: mock_model)
108
+ end
109
+
110
+ def test_ok_returns_false_when_database_query_fails
111
+ cache = MockCache.new
112
+
113
+ ActiveRecord::Health.configure do |config|
114
+ config.vcpu_count = 16
115
+ config.threshold = 0.75
116
+ config.cache = cache
117
+ end
118
+
119
+ connection = MockConnection.new(should_fail: true)
120
+ mock_model = MockModel.new("primary", connection)
121
+
122
+ refute ActiveRecord::Health.ok?(model: mock_model)
123
+ end
124
+
125
+ def test_uses_per_model_configuration
126
+ cache = MockCache.new
127
+ cache.write("activerecord_health:load_pct:animals", 0.6)
128
+
129
+ ActiveRecord::Health.configure do |config|
130
+ config.vcpu_count = 16
131
+ config.threshold = 0.75
132
+ config.cache = cache
133
+
134
+ config.for_model(AnimalsRecord) do |db|
135
+ db.vcpu_count = 8
136
+ db.threshold = 0.5
137
+ end
138
+ end
139
+
140
+ mock_model = MockModel.new("animals")
141
+ mock_model.define_singleton_method(:class) { AnimalsRecord }
142
+
143
+ refute ActiveRecord::Health.ok?(model: mock_model)
144
+ end
145
+ end
146
+
147
+ MockDbConfig = Struct.new(:name)
148
+
149
+ class MockModel
150
+ attr_reader :connection
151
+
152
+ def initialize(db_config_name, connection = nil)
153
+ @db_config_name = db_config_name
154
+ @connection = connection || MockConnection.new
155
+ end
156
+
157
+ def connection_db_config
158
+ MockDbConfig.new(@db_config_name)
159
+ end
160
+
161
+ def class
162
+ ActiveRecord::Base
163
+ end
164
+ end
165
+
166
+ class MockConnection
167
+ def initialize(active_session_count: 0, should_fail: false)
168
+ @active_session_count = active_session_count
169
+ @should_fail = should_fail
170
+ end
171
+
172
+ def adapter_name
173
+ "PostgreSQL"
174
+ end
175
+
176
+ def select_value(query)
177
+ raise "Connection failed" if @should_fail
178
+ @active_session_count
179
+ end
180
+
181
+ def execute(query)
182
+ raise "Connection failed" if @should_fail
183
+ end
184
+
185
+ def transaction
186
+ raise "Connection failed" if @should_fail
187
+ yield
188
+ end
189
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class SheddableTest < ActiveRecord::Health::TestCase
6
+ def test_sheddable_executes_block_and_returns_true_when_healthy
7
+ cache = MockCache.new
8
+ cache.write("activerecord_health:load_pct:primary", 0.5)
9
+
10
+ ActiveRecord::Health.configure do |config|
11
+ config.vcpu_count = 16
12
+ config.threshold = 0.75
13
+ config.cache = cache
14
+ end
15
+
16
+ mock_model = MockModel.new("primary")
17
+ executed = false
18
+ result = ActiveRecord::Health.sheddable(model: mock_model) { executed = true }
19
+
20
+ assert executed
21
+ assert result
22
+ end
23
+
24
+ def test_sheddable_returns_false_and_skips_block_when_overloaded
25
+ cache = MockCache.new
26
+ cache.write("activerecord_health:load_pct:primary", 0.9)
27
+
28
+ ActiveRecord::Health.configure do |config|
29
+ config.vcpu_count = 16
30
+ config.threshold = 0.75
31
+ config.cache = cache
32
+ end
33
+
34
+ mock_model = MockModel.new("primary")
35
+ executed = false
36
+ result = ActiveRecord::Health.sheddable(model: mock_model) { executed = true }
37
+
38
+ refute executed
39
+ refute result
40
+ end
41
+
42
+ def test_sheddable_pct_executes_block_and_returns_true_when_below_threshold
43
+ cache = MockCache.new
44
+ cache.write("activerecord_health:load_pct:primary", 0.4)
45
+
46
+ ActiveRecord::Health.configure do |config|
47
+ config.vcpu_count = 16
48
+ config.cache = cache
49
+ end
50
+
51
+ mock_model = MockModel.new("primary")
52
+ executed = false
53
+ result = ActiveRecord::Health.sheddable_pct(pct: 0.5, model: mock_model) { executed = true }
54
+
55
+ assert executed
56
+ assert result
57
+ end
58
+
59
+ def test_sheddable_pct_returns_false_and_skips_block_when_above_threshold
60
+ cache = MockCache.new
61
+ cache.write("activerecord_health:load_pct:primary", 0.6)
62
+
63
+ ActiveRecord::Health.configure do |config|
64
+ config.vcpu_count = 16
65
+ config.cache = cache
66
+ end
67
+
68
+ mock_model = MockModel.new("primary")
69
+ executed = false
70
+ result = ActiveRecord::Health.sheddable_pct(pct: 0.5, model: mock_model) { executed = true }
71
+
72
+ refute executed
73
+ refute result
74
+ end
75
+
76
+ def test_sheddable_pct_executes_block_and_returns_true_when_at_threshold
77
+ cache = MockCache.new
78
+ cache.write("activerecord_health:load_pct:primary", 0.5)
79
+
80
+ ActiveRecord::Health.configure do |config|
81
+ config.vcpu_count = 16
82
+ config.cache = cache
83
+ end
84
+
85
+ mock_model = MockModel.new("primary")
86
+ executed = false
87
+ result = ActiveRecord::Health.sheddable_pct(pct: 0.5, model: mock_model) { executed = true }
88
+
89
+ assert executed
90
+ assert result
91
+ end
92
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-health
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nate Berkopec
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ description: A gem that checks database health by monitoring active session count
41
+ relative to available vCPUs. Intended for automatic load shedding.
42
+ email:
43
+ - nate.berkopec@speedshop.co
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CONTRIBUTING.md
49
+ - LICENSE.txt
50
+ - README.md
51
+ - Rakefile
52
+ - docker-compose.yml
53
+ - lib/activerecord-health.rb
54
+ - lib/activerecord/health.rb
55
+ - lib/activerecord/health/adapters/mysql_adapter.rb
56
+ - lib/activerecord/health/adapters/postgresql_adapter.rb
57
+ - lib/activerecord/health/configuration.rb
58
+ - lib/activerecord/health/extensions.rb
59
+ - lib/activerecord/health/railtie.rb
60
+ - lib/activerecord/health/version.rb
61
+ - test/integration/mysql_integration_test.rb
62
+ - test/integration/postgresql_integration_test.rb
63
+ - test/test_helper.rb
64
+ - test/unit/adapters/mysql_adapter_test.rb
65
+ - test/unit/adapters/postgresql_adapter_test.rb
66
+ - test/unit/configuration_test.rb
67
+ - test/unit/extensions_test.rb
68
+ - test/unit/health_test.rb
69
+ - test/unit/sheddable_test.rb
70
+ homepage: https://github.com/nateberkopec/activerecord-health
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ allowed_push_host: https://rubygems.org
75
+ homepage_uri: https://github.com/nateberkopec/activerecord-health
76
+ source_code_uri: https://github.com/nateberkopec/activerecord-health
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 3.2.0
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.6.9
92
+ specification_version: 4
93
+ summary: Database health monitoring for ActiveRecord with automatic load shedding
94
+ test_files: []