web47core 3.2.3.41 → 3.2.3.43

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: 5167f51146e44ae39a5cb3411634be0fcef03e696859bb7eabf9fe7222ad65a0
4
- data.tar.gz: 4a7c22205a6b2033c41cccce824db4cf6be86e11fec06578165fab0c367aff20
3
+ metadata.gz: 1c38f0e545dfc8cb448c6f81d1797865f85b73a3ad8efe6e3e8912eaea1017db
4
+ data.tar.gz: 4fb1faf78c41405b5d0fab207b640d7b428c424bf53a361f59fbf3b4a83b7e64
5
5
  SHA512:
6
- metadata.gz: fc25f298a11e9bb9d70feba03cdc92f4bf61918f740e7f40d6eb1fce5bb0d0613933c18e248877bf0fdc3b10cc58711fff64e501eba95c7435830906639e9a1e
7
- data.tar.gz: 9602c563c2118543ab4cc6858ba668854f599f85342cbf08a08f639d7e2e95093203fafc6e57760c8e93e32e08043c340744e684dd89cfceede3f4a64c5e625f
6
+ metadata.gz: a3ae117203e66f4b9c2a02cadd2fa322b9e62be14fe68e9d7588fb37a566ef8121b58bff09a95f95668458100133f14b44fa8d976ec8c80d31a7c0209c2670cc
7
+ data.tar.gz: 9d7c26fe8b4d246dff55bcb1e4545ff57ed7de3ec84e9f4697fba8385be903761d2d3f5529cb9add05e608d93586b4cd162acfb4164b9a2e172fe4722bebc124
data/README.md CHANGED
@@ -10,20 +10,21 @@ Core components used commonly among all web apps for both App47 and RedMonocle
10
10
 
11
11
  ## Requirements
12
12
 
13
- * Ruby 3.2.3
13
+ * web47core 3.2.3.43
14
+ * Ruby 3.2.3 through 3.4. This repository develops on 3.4.10.
14
15
 
15
16
  ### Working with Bundler and RBENV
16
17
 
17
18
  This project manages [RBENV](https://github.com/rbenv/rbenv) and manages dependencies via [Bundler](http://gembundler.com/).
18
19
 
19
20
  You must first [install RBENV](https://github.com/rbenv/rbenv#installation).
20
- Then install Ruby 3.2.3 via RBENV
21
+ Then install Ruby 3.4.10 via RBENV
21
22
  ``` shell script
22
- rbenv install 3.2.3
23
+ rbenv install 3.4.10
23
24
  ```
24
- You'll now notice that this project (as well as other App47 ones) contains a .ruby-version file, which is executed upon opening the project's root directory in a terminal (and IDE's like RubyMine). The .ruby-version file simply states `2.7.0` which tells RBENV to ensure the Ruby version to use for this project is 2.7.0.
25
+ You'll now notice that this project (as well as other App47 ones) contains a .ruby-version file, which is executed upon opening the project's root directory in a terminal (and IDE's like RubyMine). The .ruby-version file simply states `3.4.10` which tells RBENV to ensure the Ruby version to use for this project is 3.4.10.
25
26
 
26
- Please note, your Ruby `3.2.3` version might need bundler installed:
27
+ Please note, your Ruby `3.4.10` version might need bundler installed:
27
28
  ``` shell script
28
29
  gem install bundler -v 2.5.4
29
30
  ```
@@ -118,15 +118,15 @@ class StatusController < ActionController::Base
118
118
  end
119
119
 
120
120
  #
121
- # Desired and current worker status for servers
121
+ # Desired and current worker counts, last written by AutoScaler.
122
122
  #
123
123
  def workers_status
124
- server = cron_job_server
125
- raise 'No primary server' if server.blank? || server.dead?
126
-
127
- report_success "Workers #{server.desired_server_count}/ #{server.current_server_count}",
128
- desired_count: server.desired_server_count,
129
- current_count: server.current_server_count
124
+ config = SystemConfiguration.configuration
125
+ desired = config.aws_desired_worker_count.to_i
126
+ current = config.aws_current_worker_count.to_i
127
+ report_success "Workers #{desired}/ #{current}",
128
+ desired_count: desired,
129
+ current_count: current
130
130
  rescue StandardError => error
131
131
  report_error(error, desired_count: 0, current_count: 0)
132
132
  end
@@ -16,8 +16,6 @@ module Cron
16
16
  #
17
17
  # Fields
18
18
  #
19
- field :desired_server_count, type: Integer, default: 0
20
- field :current_server_count, type: Integer, default: 0
21
19
  field :state, type: String, default: STATE_SECONDARY
22
20
  #
23
21
  # Validations
@@ -52,7 +50,7 @@ module Cron
52
50
 
53
51
  def run_cron_jobs
54
52
  run_jobs
55
- check_auto_scale
53
+ AutoScaler.check
56
54
  end
57
55
 
58
56
  # @abstract Run all cron tab and emitter jobs
@@ -85,15 +83,6 @@ module Cron
85
83
  Cron::Server.where(state: STATE_PRIMARY).first
86
84
  end
87
85
 
88
- #
89
- # Warm up a server on the next evaluation
90
- #
91
- def self.warm_up_server
92
- return unless auto_scaling_configured?
93
-
94
- primary_server.auto_scale([primary_server.desired_server_count + 1, 10].min)
95
- end
96
-
97
86
  #
98
87
  # Become primary, making others secondary
99
88
  #
@@ -148,124 +137,6 @@ module Cron
148
137
  !alive?
149
138
  end
150
139
 
151
- #
152
- # Auto scale environment
153
- #
154
- def check_auto_scale
155
- return unless auto_scaling_configured?
156
-
157
- if delayed_jobs_count.zero?
158
- handle_zero_job_count
159
- else
160
- handle_auto_scale_jobs
161
- end
162
- end
163
-
164
- def sys_config
165
- @sys_config ||= SystemConfiguration.configuration
166
- end
167
-
168
- #
169
- # Test if autoscaling is configured, return false if there is an error
170
- #
171
- def auto_scaling_configured?
172
- @auto_scaling_configured ||= sys_config.aws_auto_scaling_configured?
173
- rescue StandardError
174
- false
175
- end
176
-
177
- #
178
- # Test if autoscaling is configured, return false if there is an error
179
- #
180
- def self.auto_scaling_configured?
181
- SystemConfiguration.aws_auto_scaling_configured?
182
- rescue StandardError
183
- false
184
- end
185
-
186
- # @abstract Returns the AWS ECS service
187
- # @return [Aws::ECS::Types::Service]
188
- def ecs_service
189
- filter = { cluster: sys_config.aws_ecs_cluster_worker_name, services: [sys_config.aws_ecs_service_worker_name] }
190
- @ecs_service ||= ecs_client.describe_services(filter).services.first
191
- end
192
-
193
- # @abstract Returns a count of the Delayed Jobs in queue that have not failed
194
- # @return [Integer]
195
- def delayed_jobs_count
196
- @delayed_jobs_count ||= Delayed::Backend::Mongoid::Job.where(failed_at: nil).read(mode: :primary).count
197
- end
198
-
199
- # @abstract Returns the current value of 'desired capacity' for the ECS Service
200
- # @return [Integer]
201
- def current_desired_capacity
202
- current = ecs_service.desired_count
203
- set current_server_count: current
204
- current
205
- rescue StandardError
206
- puts 'error on desirect capacy'
207
- 0
208
- end
209
-
210
- #
211
- # Calls the 'auto_scale' method with a 'desired_count' of 0 unless the capacity is already at 0
212
- #
213
- def handle_zero_job_count
214
- return if current_desired_capacity.zero?
215
-
216
- auto_scale
217
- end
218
-
219
- #
220
- # Calls the 'auto_scale' method with a variable 'desired_count' based on how many jobs are running
221
- # We don't need any more workers if the job count is less than 1,000
222
- #
223
- def handle_auto_scale_jobs
224
- return if delayed_jobs_count < sys_config.aws_min_worker_job_count
225
-
226
- case delayed_jobs_count
227
- when 1..200
228
- auto_scale(1)
229
- when 201..500
230
- auto_scale(2)
231
- when 501..800
232
- auto_scale(3)
233
- when 801..1_200
234
- auto_scale(4)
235
- when 1_201..2_000
236
- auto_scale(4)
237
- when 2_001..3_000
238
- auto_scale(5)
239
- when 3_001..4_000
240
- auto_scale(5)
241
- when 4_001..6_000
242
- auto_scale(6)
243
- when 6_001..8_000
244
- auto_scale(7)
245
- when 8_001..10_000
246
- auto_scale(8)
247
- when 10_001..13_000
248
- auto_scale(9)
249
- else
250
- auto_scale(10)
251
- end
252
- end
253
-
254
- #
255
- # Sets the desired and minimum number of EC2 instances to run
256
- #
257
- def auto_scale(desired_count = 0)
258
- set desired_server_count: desired_count
259
- # Make sure we don't remove any workers with assigned jobs by accident
260
- return if desired_count.positive? && desired_count <= current_desired_capacity
261
-
262
- ecs_client.update_service(cluster: sys_config.aws_ecs_cluster_worker_name,
263
- service: sys_config.aws_ecs_service_worker_name,
264
- desired_count: desired_count)
265
- rescue StandardError => error
266
- App47Logger.log_error "Unable to set ECS service auto scaler to #{desired_count}", error
267
- end
268
-
269
140
  #
270
141
  # Look to make sure there is only one primary
271
142
  #
@@ -276,28 +147,8 @@ module Cron
276
147
  errors.add(:state, 'there can only be one primary') unless primary.blank? || primary.eql?(self)
277
148
  end
278
149
 
279
- #
280
- # Returns the count of active servers
281
- #
282
- def active_count
283
- current_server_count
284
- end
285
-
286
- #
287
- # Returns the count of inactive servers
288
- #
289
- def inactive_count
290
- desired_server_count
291
- end
292
-
293
150
  def ensure_last_check_in
294
151
  self.last_check_in_at ||= Time.now.utc
295
152
  end
296
-
297
- # @abstract Returns the AWS ECS client
298
- # @return [Aws::ECS::Client]
299
- def ecs_client
300
- @ecs_client ||= sys_config.aws_ecs_client
301
- end
302
153
  end
303
154
  end
@@ -13,9 +13,13 @@ module AwsConfiguration
13
13
  #
14
14
  field :aws_region, type: String
15
15
  field :aws_bucket_name, type: String
16
- field :aws_min_worker_job_count, type: Integer, default: 1
16
+ # Worker fleet size asked for by AutoScaler. Not edited from the admin form.
17
+ field :aws_desired_worker_count, type: Integer, default: 0
18
+ # ECS service desired count from the last DescribeServices. Not the running task count.
19
+ field :aws_current_worker_count, type: Integer, default: 0
17
20
 
18
- validates :aws_min_worker_job_count, numericality: { only_integer: true, greater_than: 0 }
21
+ validates :aws_desired_worker_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
22
+ validates :aws_current_worker_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
19
23
  end
20
24
  end
21
25
 
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ #
6
+ # Resize the ECS worker service from the current thread.
7
+ #
8
+ # check runs after the primary cron server has launched due jobs.
9
+ # warm_up raises the fleet to a floor. bump_worker adds one worker.
10
+ # Neither call lowers a fleet that is already larger, except check when the
11
+ # ladder maps the queue to zero workers.
12
+ #
13
+ # A host project replaces the ladder at boot:
14
+ #
15
+ # Web47core::Config.worker_ladder = [
16
+ # [0..49, 0],
17
+ # [50..500, 2],
18
+ # [501.., 6]
19
+ # ]
20
+ #
21
+ # Queue depth and the live ECS desired count are read on every call.
22
+ # The singleton only keeps the ECS client.
23
+ #
24
+ class AutoScaler
25
+ include Singleton
26
+
27
+ unless defined? DEFAULT_WORKER_LADDER
28
+ DEFAULT_WORKER_LADDER = [
29
+ [0..0, 0],
30
+ [1..200, 1],
31
+ [201..500, 2],
32
+ [501..800, 3],
33
+ [801..2_000, 4],
34
+ [2_001..4_000, 5],
35
+ [4_001..6_000, 6],
36
+ [6_001..8_000, 7],
37
+ [8_001..10_000, 8],
38
+ [10_001..13_000, 9],
39
+ [13_001.., 10]
40
+ ].freeze
41
+ end
42
+
43
+ def self.check
44
+ instance.check
45
+ end
46
+
47
+ def self.warm_up(min: 1)
48
+ instance.warm_up(min: min)
49
+ end
50
+
51
+ def self.bump_worker
52
+ instance.bump_worker
53
+ end
54
+
55
+ def self.reset
56
+ instance.reset
57
+ end
58
+
59
+ def check
60
+ return unless configured?
61
+
62
+ apply_target(desired_for(delayed_jobs_count))
63
+ end
64
+
65
+ #
66
+ # Raise the ECS desired count to at least min. Never lowers the fleet.
67
+ #
68
+ def warm_up(min: 1)
69
+ return unless configured?
70
+
71
+ target = [[min.to_i, 1].max, max_worker_count].min
72
+ scale_to(target)
73
+ end
74
+
75
+ #
76
+ # Add one worker to the live ECS desired count, capped at the ladder maximum.
77
+ #
78
+ def bump_worker
79
+ return unless configured?
80
+
81
+ current = live_desired_count
82
+ return if current.nil?
83
+
84
+ scale_to([current + 1, max_worker_count].min, current: current)
85
+ end
86
+
87
+ def reset
88
+ @ecs_client = nil
89
+ end
90
+
91
+ private
92
+
93
+ def apply_target(target)
94
+ record_desired(target)
95
+ current = live_desired_count
96
+ return if current.nil?
97
+ return if target.positive? && target <= current
98
+ return if target.zero? && current.zero?
99
+
100
+ update_service(target)
101
+ end
102
+
103
+ def scale_to(target, current: nil)
104
+ current = live_desired_count if current.nil?
105
+ return if current.nil? || target <= current
106
+
107
+ record_desired(target)
108
+ update_service(target)
109
+ end
110
+
111
+ def desired_for(job_count)
112
+ worker_ladder.each do |range, workers|
113
+ return workers if range.cover?(job_count)
114
+ end
115
+ worker_ladder.last.last
116
+ end
117
+
118
+ def max_worker_count
119
+ worker_ladder.map(&:last).max
120
+ end
121
+
122
+ def worker_ladder
123
+ Web47core::Config.worker_ladder.presence || DEFAULT_WORKER_LADDER
124
+ end
125
+
126
+ #
127
+ # ECS service desired count. nil when the service cannot be read, so a
128
+ # failed describe does not look like a fleet of zero.
129
+ #
130
+ def live_desired_count
131
+ service = describe_service
132
+ return nil unless service
133
+
134
+ count = service.desired_count
135
+ configuration.set(aws_current_worker_count: count)
136
+ count
137
+ end
138
+
139
+ def describe_service
140
+ client = ecs_client
141
+ return nil unless client
142
+
143
+ filter = {
144
+ cluster: configuration.aws_ecs_cluster_worker_name,
145
+ services: [configuration.aws_ecs_service_worker_name]
146
+ }
147
+ client.describe_services(filter).services.first
148
+ rescue StandardError => error
149
+ App47Logger.log_error 'Unable to read ECS worker service', error
150
+ nil
151
+ end
152
+
153
+ def record_desired(count)
154
+ configuration.set(aws_desired_worker_count: count)
155
+ end
156
+
157
+ def update_service(desired_count)
158
+ ecs_client.update_service(cluster: configuration.aws_ecs_cluster_worker_name,
159
+ service: configuration.aws_ecs_service_worker_name,
160
+ desired_count: desired_count)
161
+ rescue StandardError => error
162
+ App47Logger.log_error "Unable to set ECS service auto scaler to #{desired_count}", error
163
+ end
164
+
165
+ def delayed_jobs_count
166
+ Delayed::Backend::Mongoid::Job.where(failed_at: nil).read(mode: :primary).count
167
+ end
168
+
169
+ def configured?
170
+ configuration.aws_auto_scaling_configured?
171
+ rescue StandardError
172
+ false
173
+ end
174
+
175
+ def configuration
176
+ SystemConfiguration.configuration
177
+ end
178
+
179
+ def ecs_client
180
+ @ecs_client ||= configuration.aws_ecs_client
181
+ rescue StandardError => error
182
+ App47Logger.log_error 'Unable to build ECS client', error
183
+ nil
184
+ end
185
+ end
@@ -6,18 +6,20 @@ module Web47core
6
6
  #
7
7
  class Config
8
8
  include Singleton
9
- attr_accessor :email_able_models, :switchboard_able_models, :audit_model
9
+ attr_accessor :email_able_models, :switchboard_able_models, :audit_model, :worker_ladder
10
10
 
11
11
  def initialize
12
12
  @email_able_models = []
13
13
  @switchboard_able_models = []
14
14
  @audit_model = :user
15
+ @worker_ladder = nil
15
16
  end
16
17
 
17
18
  def self.reset
18
19
  instance.email_able_models = []
19
20
  instance.switchboard_able_models = []
20
21
  instance.audit_model = :user
22
+ instance.worker_ladder = nil
21
23
  end
22
24
 
23
25
  def self.audit_model_log_class
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Web47core
4
- VERSION = '3.2.3.41'
4
+ VERSION = '3.2.3.43'
5
5
  end
data/lib/web47core.rb CHANGED
@@ -61,6 +61,7 @@ require 'app/jobs/cron/record_delayed_job_metrics'
61
61
  require 'app/jobs/cron/restart_orphaned_delayed_jobs'
62
62
  require 'app/jobs/cron/trim_delayed_job_metrics'
63
63
  require 'app/jobs/cron/trim_delayed_job_workers'
64
+ require 'app/services/auto_scaler'
64
65
  require 'app/jobs/cron/server'
65
66
  require 'app/jobs/cron/switchboard_sync_configuration'
66
67
  require 'app/jobs/cron/switchboard_sync_models'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web47core
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.3.41
4
+ version: 3.2.3.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Schroeder
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-09-30 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activemodel
@@ -94,6 +93,26 @@ dependencies:
94
93
  - - ">="
95
94
  - !ruby/object:Gem::Version
96
95
  version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: json
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '2.7'
103
+ - - "<"
104
+ - !ruby/object:Gem::Version
105
+ version: '3'
106
+ type: :runtime
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '2.7'
113
+ - - "<"
114
+ - !ruby/object:Gem::Version
115
+ version: '3'
97
116
  - !ruby/object:Gem::Dependency
98
117
  name: jwt
99
118
  requirement: !ruby/object:Gem::Requirement
@@ -6606,6 +6625,7 @@ files:
6606
6625
  - lib/app/models/user_action_audit_log.rb
6607
6626
  - lib/app/models/user_audit_log.rb
6608
6627
  - lib/app/models/user_model_audit_log.rb
6628
+ - lib/app/services/auto_scaler.rb
6609
6629
  - lib/app/validators/email_format_validator.rb
6610
6630
  - lib/app/validators/http_url_validator.rb
6611
6631
  - lib/merge_coverage.rb
@@ -6624,7 +6644,6 @@ homepage: https://app47.com
6624
6644
  licenses:
6625
6645
  - MIT
6626
6646
  metadata: {}
6627
- post_install_message:
6628
6647
  rdoc_options: []
6629
6648
  require_paths:
6630
6649
  - lib
@@ -6639,8 +6658,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
6639
6658
  - !ruby/object:Gem::Version
6640
6659
  version: '0'
6641
6660
  requirements: []
6642
- rubygems_version: 3.4.19
6643
- signing_key:
6661
+ rubygems_version: 3.6.9
6644
6662
  specification_version: 4
6645
6663
  summary: App47 Web Core Library.
6646
6664
  test_files: []