herdst_worker 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,41 @@
1
+ module HerdstWorker
2
+ module Configuration
3
+ class Paths
4
+
5
+
6
+ attr_accessor :root_path, :paths
7
+
8
+
9
+ def initialize
10
+ self.root_path = ENV["ROOT_PATH"] || Dir.pwd
11
+
12
+ self.paths = ActiveSupport::HashWithIndifferentAccess.new
13
+ self.paths[:root] = self.root_path
14
+ self.paths[:app] = "#{self.root_path}/app"
15
+ self.paths[:config] = "#{self.root_path}/config"
16
+ self.paths[:temp] = "#{self.root_path}/tmp"
17
+ end
18
+
19
+
20
+ def []=(name, value)
21
+ self.paths[name] = "#{self.root_path}/#{value}"
22
+ end
23
+
24
+
25
+ def [](name)
26
+ self.paths[name]
27
+ end
28
+
29
+
30
+ def method_missing(name)
31
+ if self.paths.include?(name)
32
+ self.paths[name]
33
+ else
34
+ raise NoMethodError
35
+ end
36
+ end
37
+
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,87 @@
1
+ require 'logger'
2
+
3
+
4
+ module HerdstWorker
5
+ module Log
6
+
7
+
8
+ DEFAULT_PROGNAME = "application"
9
+
10
+
11
+ class Facade
12
+
13
+
14
+ attr_accessor :logger, :progname, :single_line, :level
15
+
16
+
17
+ def initialize(level = "DEBUG", progname = DEFAULT_PROGNAME, single_line = false)
18
+ self.progname = progname
19
+ self.single_line = single_line
20
+
21
+ self.logger = Logger.new(STDOUT)
22
+ self.logger.progname = self.get_progname(progname)
23
+ self.set_log_level(level)
24
+ end
25
+
26
+
27
+ def set_log_level(level)
28
+ self.level = level
29
+ self.logger.level = Logger.const_get(level)
30
+ end
31
+
32
+
33
+ def debug(*attributes)
34
+ write_message :debug, attributes
35
+ end
36
+
37
+
38
+ def info(*attributes)
39
+ write_message :info, attributes
40
+ end
41
+
42
+
43
+ def warn(*attributes)
44
+ write_message :warn, attributes
45
+ end
46
+
47
+
48
+ def error(*attributes)
49
+ write_message :error, attributes
50
+ end
51
+
52
+
53
+ def fatal(*attributes)
54
+ write_message :fatal, attributes
55
+ end
56
+
57
+
58
+ def method_missing(name)
59
+ full_name = self.progname === DEFAULT_PROGNAME ?
60
+ name :
61
+ "#{self.progname}.#{name}"
62
+
63
+ Facade.new(self.level, full_name, self.single_line)
64
+ end
65
+
66
+
67
+ def write_message(type, message)
68
+ if self.single_line
69
+ full_message = message.map { |i| i.to_s }.join(" ")
70
+ else
71
+ full_message = message.map { |i| i.to_s }.join("\n")
72
+ end
73
+
74
+ self.logger.send(type, full_message)
75
+ end
76
+
77
+
78
+ protected
79
+ def get_progname(name)
80
+ "[#{name.upcase}]"
81
+ end
82
+
83
+
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,162 @@
1
+ require 'securerandom'
2
+ require_relative 'processor'
3
+
4
+
5
+ module HerdstWorker
6
+ module Queue
7
+ class Facade
8
+
9
+
10
+ attr_accessor :app, :processor
11
+
12
+
13
+ def initialize(app, enabled, url, queue_wait_time)
14
+ self.app = app
15
+ self.processor = HerdstWorker::Queue::Processor.new(app, enabled, url, queue_wait_time)
16
+ end
17
+
18
+
19
+ def get_status
20
+ self.processor.processor_status
21
+ end
22
+
23
+
24
+ def start
25
+ self.processor.start
26
+ end
27
+
28
+
29
+ def halt
30
+ self.processor.halt
31
+ end
32
+
33
+
34
+ def stop
35
+ self.processor.stop
36
+ end
37
+
38
+
39
+ def get_processor
40
+ self.processor
41
+ end
42
+
43
+
44
+ def send_message(type, company_id, user_id, data = {}, queue = :primary, attributes = nil, delay = nil)
45
+ client = get_queue_client(queue)
46
+ message = create_queue_message(type, company_id, user_id, data, attributes, delay)
47
+ message.delete(:id)
48
+
49
+ client.send_message(message)
50
+ end
51
+
52
+
53
+ def send_messages(messages = [], queue = :primary)
54
+ client = get_queue_client(queue)
55
+
56
+ messages.each_slice(10) do |messages_group|
57
+ client.send_messages({ :entries => messages_group })
58
+ end
59
+ end
60
+
61
+
62
+ def send_email_message(template, company, data = {}, attributes = nil, delay = nil)
63
+ client = get_queue_client(:notifications)
64
+
65
+ message = create_email_queue_message(template, company, data, attributes, delay)
66
+ message.delete(:id)
67
+
68
+ client.send_message(message)
69
+ end
70
+
71
+
72
+ def create_queue_message(type, company_id, user_id, data, attributes = nil, delay = nil)
73
+ message = Hash.new
74
+ message[:id] = SecureRandom.hex(32)
75
+ message[:message_body] = Hash.new
76
+ message[:message_body][:eventVersion] = 1.0
77
+ message[:message_body][:eventSource] = "application:Que"
78
+ message[:message_body][:eventTime] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ")
79
+ message[:message_body][:eventName] = type.to_s.camelize(:lower)
80
+ message[:message_body][:userIdentity] = { :principalId => user_id, :companyId => company_id }
81
+ message[:message_body][:Que] = data
82
+ message[:message_body][:Que][:configurationId] = message[:message_body][:eventName]
83
+
84
+ message[:message_body] = { :Records => [message[:message_body]] }.to_json
85
+ message[:message_attributes] = attributes if attributes
86
+ message[:delay_seconds] = delay if delay
87
+ message
88
+ end
89
+
90
+
91
+ def create_email_queue_message(template, company, message_data, attributes = nil, delay = 2)
92
+ # Convert instances of ApplicationRecord to attributes instead of json representation
93
+ message_data.each do |key, value|
94
+ message_data[key] = value.attributes if value.is_a?(ActiveRecord::Base)
95
+ end
96
+
97
+ # Build data
98
+ data = Hash.new
99
+ data[:app] = self.app.name
100
+ data[:app_id] = company.id
101
+ data[:app_name] = company.name
102
+ data[:app_slug] = company.get_host_name
103
+ data[:app_brand_id] = company.brand_id
104
+ data[:app_host] = company.get_url
105
+ data[:type] = "email"
106
+ data[:template] = template
107
+ data[:data] = message_data
108
+
109
+ # Add message structure
110
+ message = Hash.new
111
+ message[:id] = SecureRandom.hex(32)
112
+ message[:message_body] = data.to_json
113
+ message[:message_attributes] = attributes if attributes
114
+ message[:delay_seconds] = delay
115
+ message
116
+ end
117
+
118
+
119
+ def create_notification_message(message_data, attributes = nil, expiry = nil, delay = nil)
120
+ message = Hash.new
121
+ message[:id] = SecureRandom.hex(32)
122
+ message[:message_body] = Hash.new
123
+ message[:message_body][:Type] = "Notification"
124
+ message[:message_body][:Message] = message_data.to_json
125
+ message[:message_body] = message[:message_body].to_json
126
+ message[:message_attributes] = attributes ? attributes : Hash.new
127
+ message[:message_attributes]["expiry"] = { :string_value => expiry.to_s, :data_type => "Number" } if expiry
128
+ message[:delay_seconds] = delay if delay
129
+
130
+ message.delete(:message_attributes) if message[:message_attributes].size == 0
131
+
132
+ message
133
+ end
134
+
135
+
136
+ def get_queue_client(queue_name)
137
+ queue_url = self.app.queues[queue_name]
138
+ sqs_client = Aws::SQS::Client.new(
139
+ :credentials => self.app.config.metadata.get_aws_credentials
140
+ )
141
+
142
+ Aws::SQS::Queue.new(
143
+ :url => queue_url,
144
+ :client => sqs_client
145
+ )
146
+ end
147
+
148
+
149
+ def self.get_delay(index)
150
+ messages_per_second = 10
151
+ send_time_per_message = 0.4
152
+
153
+ offset = (index / messages_per_second)
154
+ delay = (offset * (messages_per_second * send_time_per_message)).to_i
155
+
156
+ delay > 900 ? 900 : delay
157
+ end
158
+
159
+
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,223 @@
1
+ require 'aws-sdk-sqs'
2
+
3
+ require_relative 'runner'
4
+
5
+
6
+ module HerdstWorker
7
+ module Queue
8
+ class Processor < Runner
9
+
10
+
11
+ attr_accessor :app, :enabled, :queue_url, :queue_wait_time, :poller
12
+ attr_accessor :start_time, :restart_time
13
+ attr_accessor :processor_status, :job_count, :max_jobs
14
+ attr_accessor :attempt_threshold, :visibility_timeout, :ignored_notifications
15
+
16
+
17
+ def initialize(app, enabled, queue_url, queue_wait_time)
18
+ self.app = app
19
+ self.enabled = enabled
20
+ self.queue_url = queue_url
21
+ self.queue_wait_time = queue_wait_time
22
+ self.poller = Aws::SQS::QueuePoller.new(queue_url)
23
+ self.job_count = 0
24
+ self.max_jobs = 10
25
+ self.attempt_threshold = 6
26
+ self.visibility_timeout = 15
27
+ self.ignored_notifications = [
28
+ "AmazonSnsSubscriptionSucceeded"
29
+ ]
30
+
31
+ # Set the start time
32
+ self.reset_time
33
+
34
+ # Start the processor as working
35
+ self.set_status "starting"
36
+
37
+ # Log queue stats
38
+ self.poller.before_request do |stats|
39
+ before_request(stats)
40
+ end
41
+ end
42
+
43
+
44
+ # Runs the poller
45
+ def start_poller
46
+ if self.enabled
47
+ self.poller.poll(:wait_time_seconds => self.queue_wait_time, :skip_delete => false) do |msg|
48
+ process_message(msg)
49
+ end
50
+ end
51
+ end
52
+
53
+
54
+ # Starts or resets the application to a working status
55
+ def start
56
+ if self.processor_status == "starting"
57
+ self.set_status "working"
58
+ self.reset_time
59
+ self.start_poller
60
+ else
61
+ return if self.processor_status == "working"
62
+
63
+ self.set_status "working"
64
+ self.reset_time
65
+ end
66
+ end
67
+
68
+
69
+ # Sets the processor status to finishing. The sqs before action will
70
+ # take care of setting the idle state once all jobs have finished.
71
+ def halt
72
+ return if self.processor_status === "finishing"
73
+ set_status "finishing"
74
+ end
75
+
76
+
77
+ # Sets the processor status to stopping. The sqs before action will
78
+ # take care of stopping the application once all jobs have finished.
79
+ def stop
80
+ return if self.processor_status == "stopping"
81
+ set_status "stopping"
82
+ end
83
+
84
+
85
+ # Set the processor status. The status is alos logged to file so services
86
+ # like capastranio can see the current status
87
+ def set_status(status)
88
+ statuses = ["starting", "idle", "working", "finishing", "stopping", "stopped"]
89
+
90
+ if statuses.include? status
91
+ # Set status
92
+ self.processor_status = status
93
+
94
+ # Write the current status to file for capastranio to use
95
+ process_file = self.app.config.paths.temp + "/process_status"
96
+ File.open(process_file, "w") { |file| file.write(status) }
97
+ else
98
+ raise "Invalid status (#{status})"
99
+ end
100
+ end
101
+
102
+
103
+ def before_request(stats)
104
+ if self.app.config.is_dev?
105
+ self.app.logger.queue_stats.info "STATS (#{self.processor_status}): #{stats.inspect}"
106
+ end
107
+
108
+ # After 1 hour of running terminate application.
109
+ # The app will automatically restart in production
110
+ current_time = Time.now.utc.to_i
111
+ if (self.processor_status == "working") && (current_time >= self.restart_time)
112
+ runtime = current_time - self.start_time
113
+ self.app.logger.queue.info "Stopping after #{runtime} seconds of work"
114
+ set_status "stopping"
115
+
116
+ # On finishing wait for jobs to complete and then set status
117
+ # to idle
118
+ elsif self.processor_status == "finishing"
119
+ if self.job_count == 0
120
+ self.app.logger.queue.info "Setting processor status to idle"
121
+ set_status "idle"
122
+ end
123
+
124
+ # On stopping wait for jobs to complete and then set status
125
+ # to stopped. Once stopped the polling will terminate.
126
+ elsif self.processor_status == "stopping"
127
+ if self.job_count == 0
128
+ self.app.logger.queue.info "Setting processor status to stopped"
129
+ set_status "stopped"
130
+ end
131
+
132
+ end
133
+
134
+ if self.processor_status == "stopped"
135
+ self.app.logger.queue.info "Exiting program, Service requested to stop"
136
+ throw :stop_polling
137
+ end
138
+ end
139
+
140
+
141
+ def process_message(msg)
142
+ if self.processor_status == "working"
143
+ # If the app is already processing the max number of jobs
144
+ # put the message back in the queue with a short wait time
145
+ if self.job_count >= self.max_jobs
146
+ self.poller.change_message_visibility_timeout(msg, self.visibility_timeout)
147
+ throw :skip_delete
148
+ end
149
+
150
+ # Find out how many attempts there has been already for
151
+ # the message.
152
+ msg_attrs = msg.message_attributes.dup
153
+ attempt_number = msg_attrs.include?("attempts") ? msg_attrs["attempts"]["string_value"].to_i + 1 : 1
154
+ will_fail_permanently = attempt_number > self.attempt_threshold
155
+
156
+ # Run the job and increase the job count
157
+ # Once successful the job count is decreased by one
158
+ # and the message is deleted.
159
+ # If an error occured the job count is decreased by
160
+ # one and the error is logged locally and with sentry
161
+ self.job_count += 1
162
+ message = JSON.parse(msg.body)
163
+ process_message!(message, msg, will_fail_permanently).then {
164
+ self.job_count -= 1
165
+
166
+ }.rescue { |ex|
167
+ if will_fail_permanently
168
+ self.app.logger.queue.error "Message failed #{attempt_number} times, Reporting and failing permanently. \n#{ex.to_s} \n#{ex.backtrace.join("\n")}"
169
+ Raven.capture_exception(ex, {
170
+ :level => "fatal",
171
+ :extra => {
172
+ "queue_attempts" => attempt_number,
173
+ "queue_message_body" => msg.body
174
+ }
175
+ })
176
+
177
+ else
178
+ self.app.logger.queue.error "Message failed #{attempt_number} times, Adding back to queue."
179
+
180
+ if self.app.config.is_dev?
181
+ puts ex.inspect
182
+ puts ex.backtrace
183
+ end
184
+
185
+ replaced_message = {
186
+ :queue_url => self.poller.queue_url,
187
+ :message_body => msg.body,
188
+ :delay_seconds => self.visibility_timeout,
189
+ :message_attributes => msg_attrs.merge({
190
+ "attempts" => {
191
+ :string_value => attempt_number.to_s,
192
+ :data_type => "Number"
193
+ }
194
+ })
195
+ }
196
+
197
+ self.poller.client.send_message replaced_message
198
+ end
199
+
200
+ if self.app.config.is_dev?
201
+ self.app.logger.queue.error "Processor Error:"
202
+ self.app.logger.queue.error ex.message
203
+ self.app.logger.queue.error ex.backtrace
204
+ end
205
+
206
+ self.job_count -= 1
207
+ }.execute
208
+ else
209
+ self.poller.change_message_visibility_timeout(msg, self.visibility_timeout * 2)
210
+ throw :skip_delete
211
+ end
212
+ end
213
+
214
+
215
+ private
216
+ def reset_time
217
+ self.start_time = Time.now.utc.to_i
218
+ self.restart_time = self.start_time + (3600) # One hour
219
+ end
220
+
221
+ end
222
+ end
223
+ end