web47core 0.0.10 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97cb8fc1a923a3c6a911a319404529ef67ad9de4d1f20d6c90b088f99d8e366a
4
- data.tar.gz: 53ce4e9da8a4279b4bc4eb3ffb7a0f58b9ec1844c6f1f42adeed9d3e572f3f7e
3
+ metadata.gz: b3949ac1edae0d03528d687d4eb39024813fc8fa98325d0407a982800c68ff77
4
+ data.tar.gz: 250122ca58e430a2a366f3adebba47c7c83e17061a5946690fb8010ccbcc293c
5
5
  SHA512:
6
- metadata.gz: e3598aefea3e3b1411e4e4c60c1096129ba90fc5d33ae2c0907762d223ce234f3a4c56e50d28b2dc218a82544a51e9003bc20e0bf4f68bff70d0e767667887ad
7
- data.tar.gz: 773f67eac21317c9da0c286c1071383f864b42f4a1d36dcef624bf3968dedf40d96452f9a3eb252057fb805dd6ce66c4302746e72838a4fcd856d30c23e95563
6
+ metadata.gz: 74c4529df3ec5931a3b7e1d9f53123253dca0beddf26612db997e08d5ce1950897e3306cf5d10ff56d0be1ee581e077915d9d7a8b141765fcce018b5ee4b856b
7
+ data.tar.gz: 4b9b67895fba934193b72badc6a4da20d7dce717dd53169152a0445a581278564a027ea42e4b56c3a1f0b97417160e090632ba0f4aabe6a52159c0d26221ed06
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- web47core (0.0.10)
4
+ web47core (0.1.0)
5
5
  activesupport (~> 5.0)
6
6
  aws-sdk-ec2 (> 1.140, <= 1.160)
7
+ daemons
7
8
  delayed_job_mongoid (~> 2.3)
8
9
  email_format
9
10
  haml
@@ -92,6 +93,7 @@ GEM
92
93
  crack (0.4.3)
93
94
  safe_yaml (~> 1.0.0)
94
95
  crass (1.0.6)
96
+ daemons (1.3.1)
95
97
  database_cleaner (1.8.3)
96
98
  delayed_job (4.1.8)
97
99
  activesupport (>= 3.0, < 6.1)
data/bin/cron_server ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment'))
4
+ require 'cron/command'
5
+ Cron::Worker.logger ||= Logger.new(File.join(Rails.root, 'log', 'cron_server.log'))
6
+ Cron::Command.new(ARGV).daemonize
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Base application job that all jobs extend from
5
+ #
6
+ class ApplicationJob < ActiveJob::Base
7
+ include App47Logger
8
+
9
+ #
10
+ # If this job should run in this current environment, defaults to true
11
+ #
12
+ def self.valid_environments
13
+ []
14
+ end
15
+
16
+ #
17
+ # Is this job in a valid environment
18
+ #
19
+ def self.valid_environment?
20
+ my_environments = valid_environments
21
+ (my_environments.empty? || my_environments.include?(Rails.env))
22
+ end
23
+ end
@@ -0,0 +1,79 @@
1
+ unless ENV['RAILS_ENV'] == 'test'
2
+ begin
3
+ require 'daemons'
4
+ rescue LoadError
5
+ raise "You need to add gem 'daemons' to your Gemfile if you wish to use it."
6
+ end
7
+ end
8
+ require 'fileutils'
9
+ require 'optparse'
10
+ require 'pathname'
11
+
12
+ module Cron
13
+ class Command # rubocop:disable ClassLength
14
+
15
+ DIR_PWD = Pathname.new Dir.pwd
16
+
17
+ def initialize(args) # rubocop:disable MethodLength
18
+ @options = { pid_dir: "#{root}/tmp/pids", log_dir: "#{root}/log", monitor: false }
19
+
20
+ opts = OptionParser.new do |opt|
21
+ opt.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options] start|stop|restart|run"
22
+
23
+ opt.on('-h', '--help', 'Show this message') do
24
+ puts opt
25
+ exit 1
26
+ end
27
+ opt.on('-e', '--environment=NAME', 'Specifies the environment to run this delayed jobs under (test/development/production).') do |_e|
28
+ STDERR.puts 'The -e/--environment option has been deprecated and has no effect. Use RAILS_ENV and see http://github.com/collectiveidea/delayed_job/issues/7'
29
+ end
30
+ opt.on('--pid-dir=DIR', 'Specifies an alternate directory in which to store the process ids.') do |dir|
31
+ @options[:pid_dir] = dir
32
+ end
33
+ opt.on('--log-dir=DIR', 'Specifies an alternate directory in which to store the delayed_job log.') do |dir|
34
+ @options[:log_dir] = dir
35
+ end
36
+ opt.on('-m', '--monitor', 'Start monitor process.') do
37
+ @options[:monitor] = true
38
+ end
39
+ opt.on('--exit-on-complete', 'Exit when no more jobs are available to run. This will exit if all jobs are scheduled to run in the future.') do
40
+ @options[:exit_on_complete] = true
41
+ end
42
+ opt.on('--daemon-options a, b, c', Array, 'options to be passed through to daemons gem') do |daemon_options|
43
+ @daemon_options = daemon_options
44
+ end
45
+ end
46
+ @args = opts.parse!(args) + (@daemon_options || [])
47
+ end
48
+
49
+ def daemonize # rubocop:disable PerceivedComplexity
50
+ dir = @options[:pid_dir]
51
+ FileUtils.mkdir_p(dir) unless File.exist?(dir)
52
+
53
+ Cron::JobTab.ensure_cron_tabs
54
+ Cron::Worker.logger ||= Logger.new(File.join(@options[:log_dir], 'cron.log'))
55
+ Daemons.run_proc('cron_server',
56
+ dir: options[:pid_dir],
57
+ dir_mode: :normal,
58
+ monitor: @options[:monitor],
59
+ ARGV: @args) do
60
+ run @options
61
+ end
62
+ end
63
+
64
+ def run(options = {})
65
+ Dir.chdir(root)
66
+
67
+ Cron::Worker.new(options).start
68
+ rescue StandardError => error
69
+ App47Logger.log_error 'Unable to start Cron Server', error
70
+ exit 1
71
+ end
72
+
73
+ private
74
+
75
+ def root
76
+ @root ||= defined?(::Rails.root) ? ::Rails.root : DIR_PWD
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cron
4
+ #
5
+ # Base class for WeeklyJobs.
6
+ #
7
+ # Support running only on certain days as defined by day_of_week method
8
+ #
9
+ class Job < ApplicationJob
10
+ cattr_accessor :cron_tab
11
+ #
12
+ # Method to set the cron_tab
13
+ #
14
+ def self.cron_tab_entry(entry)
15
+ self.cron_tab = entry
16
+ end
17
+
18
+ #
19
+ # Sends support an email if something goes awry
20
+ #
21
+ def send_support_email(error, event)
22
+ email = EmailNotification.new
23
+ email.to = 'support@app47.com'
24
+ params = { current_date: I18n.l(Time.zone.today, format: :medium),
25
+ event: event,
26
+ error: error }
27
+ email.from_template('support_ticket_notification', params)
28
+ email.send_notification
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Value object for a cron tab entry
5
+ #
6
+ module Cron
7
+ class JobTab < Tab
8
+ unless defined? FRAMEWORK_CLASSES
9
+ FRAMEWORK_CLASSES = %w[job trim_collection command server tab job_tab worker].freeze
10
+ end
11
+ cattr_accessor :jobs
12
+ #
13
+ # Validations
14
+ #
15
+ validates :name, presence: true, uniqueness: true
16
+ validate :valid_name
17
+
18
+ class << self
19
+ #
20
+ # Pull form system configuration
21
+ #
22
+ def from_string(name, value)
23
+ tab = JobTab.find_or_initialize_by name: name
24
+ return unless tab.valid?
25
+
26
+ tab.update_from_string(value)
27
+ tab
28
+ end
29
+
30
+ def ensure_cron_tabs
31
+ self.jobs = []
32
+ Cron.constants.each do |job|
33
+ job_name = job.to_s.underscore
34
+ next if FRAMEWORK_CLASSES.include?(job_name) ||
35
+ job_name.end_with?('_test') ||
36
+ job_name.start_with?('base_')
37
+
38
+ jobs << job_name
39
+ klass = "cron/#{job_name}".camelize.constantize
40
+ tab = JobTab.find_or_initialize_by name: job_name
41
+ next if tab.persisted?
42
+
43
+ configure_cron_tab(tab, klass.cron_tab)
44
+ end
45
+ purge_cron_tabs
46
+ end
47
+
48
+
49
+ private
50
+
51
+ def configure_cron_tab(tab, type)
52
+ case type
53
+ when :always
54
+ tab.update! min: WILDCARD, hour: WILDCARD
55
+ when :hourly
56
+ tab.update! min: 0, hour: WILDCARD
57
+ when :daily
58
+ tab.update! min: 0, hour: 0
59
+ when :weekly
60
+ tab.update! min: 0, hour: 0, wday: 0
61
+ when :monthly
62
+ tab.update! min: 0, hour: 0, mday: 0
63
+ else
64
+ tab.update_from_string(type)
65
+ tab.save!
66
+ end
67
+ end
68
+
69
+ def purge_cron_tabs
70
+ JobTab.all.each do |tab|
71
+ tab.destroy unless jobs.include?(tab.name)
72
+ end
73
+ end
74
+ end
75
+
76
+ #
77
+ # Update our values based on the value
78
+ #
79
+ def update_from_string(value)
80
+ values = value.split(' ')
81
+ self.min = values[0]
82
+ self.hour = values[1]
83
+ self.mday = values[2]
84
+ self.month = values[3]
85
+ self.wday = values[4]
86
+ end
87
+
88
+ #
89
+ # Run this job cron tab
90
+ #
91
+ def run
92
+ return unless valid_environment?
93
+
94
+ cron_job_class.perform_later
95
+ super
96
+ end
97
+
98
+ #
99
+ # Is this enabled and a valid environment
100
+ #
101
+ def valid_environment?
102
+ enabled? && cron_job_class.valid_environment?
103
+ end
104
+
105
+ #
106
+ # Return the class associated with this job cron tab
107
+ #
108
+ def cron_job_class
109
+ @cron_job_class ||= "cron/#{name}".camelize.constantize
110
+ end
111
+
112
+ #
113
+ # Convert back to a standard string value
114
+ #
115
+ def to_string
116
+ [min, hour, mday, month, wday].join(' ')
117
+ end
118
+
119
+ #
120
+ # Test the name is the list of jobs discovered
121
+ #
122
+ def valid_name
123
+ errors.add(:name, 'Invalid Tab') unless jobs.include?(name)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,285 @@
1
+ #
2
+ # Handle the coordination of which server should be running the cron jobs
3
+ #
4
+ module Cron
5
+ class Server
6
+ include StandardModel
7
+ #
8
+ # Constants
9
+ #
10
+ STATE_PRIMARY = 'primary'.freeze unless defined? STATE_PRIMARY
11
+ STATE_SECONDARY = 'secondary'.freeze unless defined? STATE_SECONDARY
12
+ ALL_STATES = [STATE_PRIMARY, STATE_SECONDARY].freeze unless defined? ALL_STATES
13
+ #
14
+ # Fields
15
+ #
16
+ field :host_name, type: String
17
+ field :pid, type: Integer
18
+ field :desired_server_count, type: Integer, default: 0
19
+ field :current_server_count, type: Integer, default: 0
20
+ field :last_check_in_at, type: Time, default: Time.now.utc
21
+ field :state, type: String, default: STATE_SECONDARY
22
+ #
23
+ # Validations
24
+ #
25
+ validates :host_name, presence: true
26
+ validates :pid, presence: true
27
+ validates :last_check_in_at, presence: true
28
+ validates :state, inclusion: { in: ALL_STATES }
29
+ validate :high_lander
30
+ #
31
+ # Go through the logic once a minute
32
+ #
33
+ def execute
34
+ if primary?
35
+ run_cron_jobs
36
+ else
37
+ primary = Cron::Server.where(state: STATE_PRIMARY).first
38
+ if primary.blank? || primary.dead?
39
+ become_primary
40
+ run_cron_jobs
41
+ end
42
+ end
43
+ time_to_next_run
44
+ rescue StandardError => error
45
+ App47Logger.log_error 'Unable to run cron jobs', error
46
+ time_to_next_run
47
+ ensure
48
+ check_in
49
+ end
50
+
51
+ def run_cron_jobs
52
+ run_jobs
53
+ check_auto_scale
54
+ end
55
+
56
+ #
57
+ # Run all cron tab jobs
58
+ #
59
+ def run_jobs
60
+ now = Time.now.utc
61
+ CronTab.all.each { |tab| tab.run if tab.time_to_run?(now) }
62
+ end
63
+
64
+ #
65
+ # Determine the next minute to run,
66
+ #
67
+ def time_to_next_run
68
+ 60 - Time.now.utc.to_i % 60
69
+ end
70
+
71
+ #
72
+ # Find a record for this server
73
+ #
74
+ def self.find_or_create_server
75
+ Cron::Server.find_or_create_by!(host_name: Socket.gethostname, pid: Process.pid)
76
+ end
77
+
78
+ #
79
+ # Find a the current master
80
+ #
81
+ def self.primary_server
82
+ Cron::Server.where(state: STATE_PRIMARY).first
83
+ end
84
+
85
+ #
86
+ # Warm up a server on the next evaluation
87
+ #
88
+ def self.warm_up_server
89
+ return unless SystemConfiguration.auto_scaling_configured?
90
+
91
+ primary_server.auto_scale([primary_server.desired_server_count + 1, 10].min)
92
+ end
93
+
94
+ #
95
+ # Become primary, making others secondary
96
+ #
97
+ def become_primary
98
+ Cron::Server.each(&:become_secondary)
99
+ # sleep a small amount of time to randomize a new primary
100
+ sleep rand(1..15)
101
+ # Check to see if another node already became primary
102
+ primary = Cron::Server.primary_server
103
+ return if primary.present? && primary.alive?
104
+
105
+ # no one else is in, so become primary
106
+ update_attributes! state: STATE_PRIMARY, last_check_in_at: Time.now.utc
107
+ end
108
+
109
+ #
110
+ # Become secondary node
111
+ #
112
+ def become_secondary(user = nil)
113
+ if user.present?
114
+ update_attributes_and_log! user, state: STATE_SECONDARY
115
+ else
116
+ update_attributes! state: STATE_SECONDARY
117
+ end
118
+ end
119
+
120
+ #
121
+ # Am I the primary server
122
+ #
123
+ def primary?
124
+ alive? && STATE_PRIMARY.eql?(state)
125
+ end
126
+
127
+ #
128
+ # Am I a secondary server
129
+ #
130
+ def secondary?
131
+ STATE_SECONDARY.eql?(state)
132
+ end
133
+
134
+ #
135
+ # Return true if I've reported in the last two minutes
136
+ #
137
+ def alive?
138
+ last_check_in_at >= 90.seconds.ago.utc
139
+ end
140
+
141
+ #
142
+ # Is the server dead, meaning is it not reporting within the last two minutes
143
+ #
144
+ def dead?
145
+ !alive?
146
+ end
147
+
148
+ #
149
+ # Perform a check in for the server
150
+ #
151
+ def check_in
152
+ set last_check_in_at: Time.now.utc
153
+ end
154
+
155
+ #
156
+ # Auto scale environment
157
+ #
158
+ def check_auto_scale
159
+ return unless SystemConfiguration.auto_scaling_configured?
160
+
161
+ if delayed_jobs_count.eql?(0)
162
+ handle_zero_job_count
163
+ else
164
+ handle_auto_scale_jobs
165
+ end
166
+ end
167
+
168
+ #
169
+ # Returns the AWS AutoScaling Client
170
+ #
171
+ def client
172
+ credentials = { access_key_id: sys_config.access_key_id,
173
+ secret_access_key: sys_config.secret_access_key,
174
+ region: sys_config.region }
175
+ @client ||= Aws::AutoScaling::Client.new(credentials)
176
+ end
177
+
178
+ def sys_config
179
+ @sys_config ||= SystemConfiguration.configuration
180
+ end
181
+
182
+ #
183
+ # Returns the AutoScalingGroup associated with the account
184
+ #
185
+ def auto_scaling_group
186
+ filter = { auto_scaling_group_names: [sys_config.auto_scaling_group_name] }
187
+ @auto_scaling_group ||= client.describe_auto_scaling_groups(filter).auto_scaling_groups.first
188
+ end
189
+
190
+ #
191
+ # Returns a count of the Delayed Jobs in queue that have not failed
192
+ #
193
+ def delayed_jobs_count
194
+ @delayed_jobs_count ||= Delayed::Backend::Mongoid::Job.where(failed_at: nil).read(mode: :primary).count
195
+ end
196
+
197
+ #
198
+ # Returns the current value of 'desired capacity' for the AutoScalingGroup
199
+ #
200
+ def current_desired_capacity
201
+ current = auto_scaling_group.desired_capacity
202
+ set current_server_count: current
203
+ current
204
+ rescue StandardError
205
+ 0
206
+ end
207
+
208
+ #
209
+ # Calls the 'auto_scale' method with a 'desired_count' of 0 unless the capacity is already at 0
210
+ #
211
+ def handle_zero_job_count
212
+ return if current_desired_capacity.eql?(0)
213
+
214
+ auto_scale
215
+ end
216
+
217
+ #
218
+ # Calls the 'auto_scale' method with a variable 'desired_count' based on how many jobs are running
219
+ # We don't need any more workers if the job count is less than 1,000
220
+ #
221
+ def handle_auto_scale_jobs
222
+ return if delayed_jobs_count < 50
223
+
224
+ case delayed_jobs_count
225
+ when 50..250
226
+ auto_scale(1)
227
+ when 251..500
228
+ auto_scale(2)
229
+ when 501..1_000
230
+ auto_scale(3)
231
+ when 1_001..2_000
232
+ auto_scale(4)
233
+ when 2_001..3_999
234
+ auto_scale(4)
235
+ when 4_000..7_999
236
+ auto_scale(5)
237
+ when 8_000..10_999
238
+ auto_scale(5)
239
+ when 11_000..13_999
240
+ auto_scale(6)
241
+ when 14_000..17_999
242
+ auto_scale(6)
243
+ else
244
+ auto_scale(7)
245
+ end
246
+ end
247
+
248
+ #
249
+ # Sets the desired and minimum number of EC2 instances to run
250
+ #
251
+ def auto_scale(desired_count = 0)
252
+ set desired_server_count: desired_count
253
+ # Make sure we don't remove any workers with assigned jobs by accident
254
+ return if desired_count.positive? && desired_count <= current_desired_capacity
255
+
256
+ client.update_auto_scaling_group(auto_scaling_group_name: sys_config.auto_scaling_group_name,
257
+ min_size: desired_count,
258
+ desired_capacity: desired_count)
259
+ end
260
+
261
+ #
262
+ # Look to make sure there is only one primary
263
+ #
264
+ def high_lander
265
+ return if secondary? # Don't need to check if not primary
266
+
267
+ primary = Cron::Server.where(state: STATE_PRIMARY).first
268
+ errors.add(:state, 'there can only be one primary') unless primary.blank? || primary.eql?(self)
269
+ end
270
+
271
+ #
272
+ # Returns the count of active servers
273
+ #
274
+ def active_count
275
+ current_server_count
276
+ end
277
+
278
+ #
279
+ # Returns the count of inactive servers
280
+ #
281
+ def inactive_count
282
+ desired_server_count
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,66 @@
1
+ #
2
+ # Run in cron
3
+ #
4
+ module Cron
5
+ #
6
+ # Sync configuration with switchboard
7
+ #
8
+ class SwitchboardSyncConfiguration < Job
9
+ cron_tab_entry :hourly
10
+
11
+ #
12
+ # Only run in environments where switchboard is configured
13
+ #
14
+ def self.valid_environment?
15
+ SystemConfiguration.switchboard_configured?
16
+ end
17
+
18
+ #
19
+ # Cycle through all configuration keys
20
+ #
21
+ def perform
22
+ Rails.cache.reconnect
23
+ RestClient.get(switchboard_url,
24
+ ACCESS_TOKEN: SystemConfiguration.switchboard_stack_api_token,
25
+ content_type: 'application/json') do |response, _request, _result, &block|
26
+ case response.code
27
+ when 200
28
+ json = JSON.parse(response.body)
29
+ config = SystemConfiguration.configuration
30
+ json['results'].each { |key, value| update_config(config, key, value) }
31
+ config.save!
32
+ else
33
+ App47Logger.log_error "Unable to fetch switchboard config, #{response.inspect}"
34
+ response.return!(&block)
35
+ end
36
+ end
37
+ end
38
+
39
+ #
40
+ # First see if it's updateable against system config, then see if it's in JobCronTab
41
+ #
42
+ def update_config(config, key, value)
43
+ config.send("#{key}=", value) if config.respond_to?("#{key}=")
44
+ return unless key.end_with?('_crontab')
45
+
46
+ name = key.chomp('_crontab')
47
+ tab = Cron::JobTab.from_string(name, value)
48
+ if tab.present? && tab.valid?
49
+ tab.save
50
+ App47Logger.log_debug "Crontab #{name} updated with #{value}"
51
+ else
52
+ App47Logger.log_warn "Unable to update crontab #{name} updated with #{value}"
53
+ end
54
+ end
55
+
56
+ #
57
+ # Generate the switchboard URL
58
+ #
59
+ def switchboard_url
60
+ [SystemConfiguration.switchboard_base_url,
61
+ 'stacks',
62
+ SystemConfiguration.switchboard_stack_id,
63
+ 'items.json'].join('/')
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ #
2
+ # Run in cron
3
+ #
4
+ module Cron
5
+ #
6
+ # Cycle through all members and tell them to sync with with switchboard
7
+ #
8
+ class SwitchboardSyncModels <Job
9
+ cron_tab_entry :daily
10
+
11
+ #
12
+ # Only run in environments where switchboard is configured
13
+ #
14
+ def self.valid_environment?
15
+ SystemConfiguration.switchboard_configured? && Web47core.config.switchboard_able_models.present?
16
+ end
17
+
18
+ #
19
+ # Cycle through the collection and perform an upsert on it
20
+ #
21
+ def perform
22
+ Web47core.config.switchboard_able_models.each { |model| model.each(&:switchboard_upsert) }
23
+ end
24
+ end
25
+ end