herdst_worker 0.1.0

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