web47core 0.0.10 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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