kicks 3.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +24 -0
- data/.gitignore +12 -0
- data/ChangeLog.md +142 -0
- data/Dockerfile +24 -0
- data/Dockerfile.slim +20 -0
- data/Gemfile +8 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +209 -0
- data/Rakefile +12 -0
- data/bin/sneakers +6 -0
- data/docker-compose.yml +24 -0
- data/examples/benchmark_worker.rb +22 -0
- data/examples/max_retry_handler.rb +68 -0
- data/examples/metrics_worker.rb +34 -0
- data/examples/middleware_worker.rb +36 -0
- data/examples/newrelic_metrics_worker.rb +40 -0
- data/examples/profiling_worker.rb +69 -0
- data/examples/sneakers.conf.rb.example +11 -0
- data/examples/title_scraper.rb +36 -0
- data/examples/workflow_worker.rb +23 -0
- data/kicks.gemspec +44 -0
- data/lib/sneakers/cli.rb +122 -0
- data/lib/sneakers/concerns/logging.rb +34 -0
- data/lib/sneakers/concerns/metrics.rb +34 -0
- data/lib/sneakers/configuration.rb +125 -0
- data/lib/sneakers/content_encoding.rb +47 -0
- data/lib/sneakers/content_type.rb +47 -0
- data/lib/sneakers/error_reporter.rb +33 -0
- data/lib/sneakers/errors.rb +2 -0
- data/lib/sneakers/handlers/maxretry.rb +219 -0
- data/lib/sneakers/handlers/oneshot.rb +26 -0
- data/lib/sneakers/metrics/logging_metrics.rb +16 -0
- data/lib/sneakers/metrics/newrelic_metrics.rb +32 -0
- data/lib/sneakers/metrics/null_metrics.rb +13 -0
- data/lib/sneakers/metrics/statsd_metrics.rb +21 -0
- data/lib/sneakers/middleware/config.rb +23 -0
- data/lib/sneakers/publisher.rb +49 -0
- data/lib/sneakers/queue.rb +87 -0
- data/lib/sneakers/runner.rb +91 -0
- data/lib/sneakers/spawner.rb +30 -0
- data/lib/sneakers/support/production_formatter.rb +11 -0
- data/lib/sneakers/support/utils.rb +18 -0
- data/lib/sneakers/tasks.rb +66 -0
- data/lib/sneakers/version.rb +3 -0
- data/lib/sneakers/worker.rb +162 -0
- data/lib/sneakers/workergroup.rb +60 -0
- data/lib/sneakers.rb +125 -0
- data/log/.gitkeep +0 -0
- data/scripts/local_integration +2 -0
- data/scripts/local_worker +3 -0
- data/spec/fixtures/integration_worker.rb +18 -0
- data/spec/fixtures/require_worker.rb +23 -0
- data/spec/gzip_helper.rb +15 -0
- data/spec/sneakers/cli_spec.rb +75 -0
- data/spec/sneakers/concerns/logging_spec.rb +39 -0
- data/spec/sneakers/concerns/metrics_spec.rb +38 -0
- data/spec/sneakers/configuration_spec.rb +97 -0
- data/spec/sneakers/content_encoding_spec.rb +81 -0
- data/spec/sneakers/content_type_spec.rb +81 -0
- data/spec/sneakers/integration_spec.rb +158 -0
- data/spec/sneakers/publisher_spec.rb +179 -0
- data/spec/sneakers/queue_spec.rb +169 -0
- data/spec/sneakers/runner_spec.rb +70 -0
- data/spec/sneakers/sneakers_spec.rb +77 -0
- data/spec/sneakers/support/utils_spec.rb +44 -0
- data/spec/sneakers/tasks/sneakers_run_spec.rb +115 -0
- data/spec/sneakers/worker_handlers_spec.rb +469 -0
- data/spec/sneakers/worker_spec.rb +712 -0
- data/spec/sneakers/workergroup_spec.rb +83 -0
- data/spec/spec_helper.rb +21 -0
- metadata +352 -0
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Sneakers
|
5
|
+
module Handlers
|
6
|
+
#
|
7
|
+
# Maxretry uses dead letter policies on Rabbitmq to requeue and retry
|
8
|
+
# messages after failure (rejections and errors). When the maximum
|
9
|
+
# number of retries is reached it will put the message on an error queue.
|
10
|
+
# This handler will only retry at the queue level. To accomplish that, the
|
11
|
+
# setup is a bit complex.
|
12
|
+
#
|
13
|
+
# Input:
|
14
|
+
# worker_exchange (eXchange)
|
15
|
+
# worker_queue (Queue)
|
16
|
+
# We create:
|
17
|
+
# worker_queue-retry - (X) where we setup the worker queue to dead-letter.
|
18
|
+
# worker_queue-retry - (Q) queue bound to ^ exchange, dead-letters to
|
19
|
+
# worker_queue-retry-requeue.
|
20
|
+
# worker_queue-error - (X) where to send max-retry failures
|
21
|
+
# worker_queue-error - (Q) bound to worker_queue-error.
|
22
|
+
# worker_queue-retry-requeue - (X) exchange to bind worker_queue to for
|
23
|
+
# requeuing directly to the worker_queue.
|
24
|
+
#
|
25
|
+
# This requires that you setup arguments to the worker queue to line up the
|
26
|
+
# dead letter queue. See the example for more information.
|
27
|
+
#
|
28
|
+
# Many of these can be override with options:
|
29
|
+
# - retry_exchange - sets retry exchange & queue
|
30
|
+
# - retry_error_exchange - sets error exchange and queue
|
31
|
+
# - retry_requeue_exchange - sets the exchange created to re-queue things
|
32
|
+
# back to the worker queue.
|
33
|
+
#
|
34
|
+
class Maxretry
|
35
|
+
|
36
|
+
def initialize(channel, queue, opts)
|
37
|
+
@worker_queue_name = queue.name
|
38
|
+
Sneakers.logger.debug do
|
39
|
+
"#{log_prefix} creating handler, opts=#{opts}"
|
40
|
+
end
|
41
|
+
|
42
|
+
@channel = channel
|
43
|
+
@opts = opts
|
44
|
+
|
45
|
+
# Construct names, defaulting where suitable
|
46
|
+
retry_name = @opts[:retry_exchange] || "#{@worker_queue_name}-retry"
|
47
|
+
error_name = @opts[:retry_error_exchange] || "#{@worker_queue_name}-error"
|
48
|
+
requeue_name = @opts[:retry_requeue_exchange] || "#{@worker_queue_name}-retry-requeue"
|
49
|
+
retry_routing_key = @opts[:retry_routing_key] || "#"
|
50
|
+
|
51
|
+
# Create the exchanges
|
52
|
+
@retry_exchange, @error_exchange, @requeue_exchange = [retry_name, error_name, requeue_name].map do |name|
|
53
|
+
Sneakers.logger.debug { "#{log_prefix} creating exchange=#{name}" }
|
54
|
+
@channel.exchange(name,
|
55
|
+
:type => 'topic',
|
56
|
+
:durable => exchange_durable?)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Create the queues and bindings
|
60
|
+
Sneakers.logger.debug do
|
61
|
+
"#{log_prefix} creating queue=#{retry_name} x-dead-letter-exchange=#{requeue_name}"
|
62
|
+
end
|
63
|
+
@retry_queue = @channel.queue(retry_name,
|
64
|
+
:durable => queue_durable?,
|
65
|
+
:arguments => {
|
66
|
+
:'x-dead-letter-exchange' => requeue_name,
|
67
|
+
:'x-message-ttl' => @opts[:retry_timeout] || 60000
|
68
|
+
})
|
69
|
+
@retry_queue.bind(@retry_exchange, :routing_key => '#')
|
70
|
+
|
71
|
+
Sneakers.logger.debug do
|
72
|
+
"#{log_prefix} creating queue=#{error_name}"
|
73
|
+
end
|
74
|
+
@error_queue = @channel.queue(error_name,
|
75
|
+
:durable => queue_durable?)
|
76
|
+
@error_queue.bind(@error_exchange, :routing_key => '#')
|
77
|
+
|
78
|
+
# Finally, bind the worker queue to our requeue exchange
|
79
|
+
queue.bind(@requeue_exchange, :routing_key => retry_routing_key)
|
80
|
+
|
81
|
+
@max_retries = @opts[:retry_max_times] || 5
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.configure_queue(name, opts)
|
86
|
+
retry_name = opts.fetch(:retry_exchange, "#{name}-retry")
|
87
|
+
opt_args = opts[:queue_options][:arguments] ? opts[:queue_options][:arguments].inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo} : {}
|
88
|
+
opts[:queue_options][:arguments] = { :'x-dead-letter-exchange' => retry_name }.merge(opt_args)
|
89
|
+
opts[:queue_options]
|
90
|
+
end
|
91
|
+
|
92
|
+
def acknowledge(hdr, props, msg)
|
93
|
+
@channel.acknowledge(hdr.delivery_tag, false)
|
94
|
+
end
|
95
|
+
|
96
|
+
def reject(hdr, props, msg, requeue = false)
|
97
|
+
if requeue
|
98
|
+
# This was explicitly rejected specifying it be requeued so we do not
|
99
|
+
# want it to pass through our retry logic.
|
100
|
+
@channel.reject(hdr.delivery_tag, requeue)
|
101
|
+
else
|
102
|
+
handle_retry(hdr, props, msg, :reject)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def error(hdr, props, msg, err)
|
108
|
+
handle_retry(hdr, props, msg, err)
|
109
|
+
end
|
110
|
+
|
111
|
+
def noop(hdr, props, msg)
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
# Helper logic for retry handling. This will reject the message if there
|
116
|
+
# are remaining retries left on it, otherwise it will publish it to the
|
117
|
+
# error exchange along with the reason.
|
118
|
+
# @param hdr [Bunny::DeliveryInfo]
|
119
|
+
# @param props [Bunny::MessageProperties]
|
120
|
+
# @param msg [String] The message
|
121
|
+
# @param reason [String, Symbol, Exception] Reason for the retry, included
|
122
|
+
# in the JSON we put on the error exchange.
|
123
|
+
def handle_retry(hdr, props, msg, reason)
|
124
|
+
# +1 for the current attempt
|
125
|
+
num_attempts = failure_count(props[:headers]) + 1
|
126
|
+
if num_attempts <= @max_retries
|
127
|
+
# We call reject which will route the message to the
|
128
|
+
# x-dead-letter-exchange (ie. retry exchange) on the queue
|
129
|
+
Sneakers.logger.info do
|
130
|
+
"#{log_prefix} msg=retrying, count=#{num_attempts}, headers=#{props[:headers]}"
|
131
|
+
end
|
132
|
+
@channel.reject(hdr.delivery_tag, false)
|
133
|
+
# TODO: metrics
|
134
|
+
else
|
135
|
+
# Retried more than the max times
|
136
|
+
# Publish the original message with the routing_key to the error exchange
|
137
|
+
Sneakers.logger.info do
|
138
|
+
"#{log_prefix} msg=failing, retry_count=#{num_attempts}, reason=#{reason}"
|
139
|
+
end
|
140
|
+
data = {
|
141
|
+
error: reason.to_s,
|
142
|
+
num_attempts: num_attempts,
|
143
|
+
failed_at: Time.now.iso8601,
|
144
|
+
properties: props.to_hash
|
145
|
+
}.tap do |hash|
|
146
|
+
if reason.is_a?(Exception)
|
147
|
+
hash[:error_class] = reason.class.to_s
|
148
|
+
hash[:error_message] = "#{reason}"
|
149
|
+
if reason.backtrace
|
150
|
+
hash[:backtrace] = reason.backtrace.take(10)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Preserve retry log in a list
|
156
|
+
if retry_info = props[:headers]['retry_info']
|
157
|
+
old_retry0 = JSON.parse(retry_info) rescue {error: "Failed to parse retry info"}
|
158
|
+
old_retry = Array(old_retry0)
|
159
|
+
# Prevent old retry from nesting
|
160
|
+
data[:properties][:headers].delete('retry_info')
|
161
|
+
data = old_retry.unshift(data)
|
162
|
+
end
|
163
|
+
|
164
|
+
@error_exchange.publish(msg, {
|
165
|
+
routing_key: hdr.routing_key,
|
166
|
+
headers: {
|
167
|
+
retry_info: data.to_json
|
168
|
+
}
|
169
|
+
})
|
170
|
+
@channel.acknowledge(hdr.delivery_tag, false)
|
171
|
+
# TODO: metrics
|
172
|
+
end
|
173
|
+
end
|
174
|
+
private :handle_retry
|
175
|
+
|
176
|
+
# Uses the x-death header to determine the number of failures this job has
|
177
|
+
# seen in the past. This does not count the current failure. So for
|
178
|
+
# instance, the first time the job fails, this will return 0, the second
|
179
|
+
# time, 1, etc.
|
180
|
+
# @param headers [Hash] Hash of headers that Rabbit delivers as part of
|
181
|
+
# the message
|
182
|
+
# @return [Integer] Count of number of failures.
|
183
|
+
def failure_count(headers)
|
184
|
+
if headers.nil? || headers['x-death'].nil?
|
185
|
+
0
|
186
|
+
else
|
187
|
+
x_death_array = headers['x-death'].select do |x_death|
|
188
|
+
x_death['queue'] == @worker_queue_name
|
189
|
+
end
|
190
|
+
if x_death_array.count > 0 && x_death_array.first['count']
|
191
|
+
# Newer versions of RabbitMQ return headers with a count key
|
192
|
+
x_death_array.inject(0) {|sum, x_death| sum + x_death['count']}
|
193
|
+
else
|
194
|
+
# Older versions return a separate x-death header for each failure
|
195
|
+
x_death_array.count
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
private :failure_count
|
200
|
+
|
201
|
+
# Prefix all of our log messages so they are easier to find. We don't have
|
202
|
+
# the worker, so the next best thing is the queue name.
|
203
|
+
def log_prefix
|
204
|
+
"Maxretry handler [queue=#{@worker_queue_name}]"
|
205
|
+
end
|
206
|
+
private :log_prefix
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def queue_durable?
|
211
|
+
@opts.fetch(:queue_options, {}).fetch(:durable, false)
|
212
|
+
end
|
213
|
+
|
214
|
+
def exchange_durable?
|
215
|
+
queue_durable?
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module Handlers
|
3
|
+
class Oneshot
|
4
|
+
def initialize(channel, queue, opts)
|
5
|
+
@channel = channel
|
6
|
+
@opts = opts
|
7
|
+
end
|
8
|
+
|
9
|
+
def acknowledge(hdr, props, msg)
|
10
|
+
@channel.acknowledge(hdr.delivery_tag, false)
|
11
|
+
end
|
12
|
+
|
13
|
+
def reject(hdr, props, msg, requeue=false)
|
14
|
+
@channel.reject(hdr.delivery_tag, requeue)
|
15
|
+
end
|
16
|
+
|
17
|
+
def error(hdr, props, msg, err)
|
18
|
+
reject(hdr, props, msg)
|
19
|
+
end
|
20
|
+
|
21
|
+
def noop(hdr, props, msg)
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module Metrics
|
3
|
+
class LoggingMetrics
|
4
|
+
def increment(metric)
|
5
|
+
Sneakers.logger.info("INC: #{metric}")
|
6
|
+
end
|
7
|
+
|
8
|
+
def timing(metric, &block)
|
9
|
+
start = Time.now
|
10
|
+
block.call
|
11
|
+
Sneakers.logger.info("TIME: #{metric} #{Time.now - start}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module Metrics
|
3
|
+
class NewrelicMetrics
|
4
|
+
|
5
|
+
def self.eagent(eagent = nil)
|
6
|
+
@eagent = eagent || @eagent
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize()
|
10
|
+
#@connection = conn
|
11
|
+
end
|
12
|
+
|
13
|
+
def increment(metric)
|
14
|
+
record_stat metric, 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def record_stat(metric, num)
|
18
|
+
metric_name = "Custom/#{metric.gsub("\.", "\/")}"
|
19
|
+
NewrelicMetrics.eagent::Agent.record_metric(metric_name, num)
|
20
|
+
rescue Exception => e
|
21
|
+
puts "NewrelicMetrics#record_stat: #{e}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def timing(metric, &block)
|
25
|
+
start = Time.now
|
26
|
+
block.call
|
27
|
+
record_stat(metric, ((Time.now - start)*1000).floor)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module Metrics
|
3
|
+
class StatsdMetrics
|
4
|
+
def initialize(conn)
|
5
|
+
@connection = conn
|
6
|
+
end
|
7
|
+
|
8
|
+
def increment(metric)
|
9
|
+
@connection.increment(metric)
|
10
|
+
end
|
11
|
+
|
12
|
+
def timing(metric, &block)
|
13
|
+
start = Time.now
|
14
|
+
block.call
|
15
|
+
@connection.timing(metric, ((Time.now - start)*1000).floor)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Sneakers
|
2
|
+
module Middleware
|
3
|
+
class Config
|
4
|
+
def self.use(klass, args)
|
5
|
+
middlewares << { class: klass, args: args }
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.delete(klass)
|
9
|
+
middlewares.reject! { |el| el[:class] == klass }
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.to_a
|
13
|
+
middlewares
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.middlewares
|
17
|
+
@middlewares ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
private_class_method :middlewares
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Sneakers
|
2
|
+
class Publisher
|
3
|
+
|
4
|
+
attr_reader :exchange, :channel
|
5
|
+
|
6
|
+
def initialize(opts = {})
|
7
|
+
@mutex = Mutex.new
|
8
|
+
@opts = Sneakers::CONFIG.merge(opts)
|
9
|
+
# If we've already got a bunny object, use it. This allows people to
|
10
|
+
# specify all kinds of options we don't need to know about (e.g. for ssl).
|
11
|
+
@bunny = @opts[:connection]
|
12
|
+
end
|
13
|
+
|
14
|
+
def publish(msg, options = {})
|
15
|
+
ensure_connection!
|
16
|
+
to_queue = options.delete(:to_queue)
|
17
|
+
options[:routing_key] ||= to_queue
|
18
|
+
Sneakers.logger.info {"publishing <#{msg}> to [#{options[:routing_key]}]"}
|
19
|
+
serialized_msg = Sneakers::ContentType.serialize(msg, options[:content_type])
|
20
|
+
encoded_msg = Sneakers::ContentEncoding.encode(serialized_msg, options[:content_encoding])
|
21
|
+
@exchange.publish(encoded_msg, options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def ensure_connection!
|
25
|
+
@mutex.synchronize do
|
26
|
+
connect! unless connected?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def connect!
|
32
|
+
@bunny ||= create_bunny_connection
|
33
|
+
@bunny.start
|
34
|
+
@channel = @bunny.create_channel
|
35
|
+
@exchange = @channel.exchange(@opts[:exchange], **@opts[:exchange_options])
|
36
|
+
end
|
37
|
+
|
38
|
+
def connected?
|
39
|
+
@bunny && @bunny.connected? && channel
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_bunny_connection
|
43
|
+
Bunny.new(@opts[:amqp], :vhost => @opts[:vhost],
|
44
|
+
:heartbeat => @opts[:heartbeat],
|
45
|
+
:properties => @opts.fetch(:properties, {}),
|
46
|
+
:logger => Sneakers::logger)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
|
2
|
+
class Sneakers::Queue
|
3
|
+
attr_reader :name, :opts, :exchange, :channel
|
4
|
+
|
5
|
+
def initialize(name, opts)
|
6
|
+
@name = name
|
7
|
+
@opts = opts
|
8
|
+
@handler_klass = Sneakers::CONFIG[:handler]
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# :exchange
|
13
|
+
# :heartbeat_interval
|
14
|
+
# :prefetch
|
15
|
+
# :durable
|
16
|
+
# :ack
|
17
|
+
#
|
18
|
+
def subscribe(worker)
|
19
|
+
# If we've already got a bunny object, use it. This allows people to
|
20
|
+
# specify all kinds of options we don't need to know about (e.g. for ssl).
|
21
|
+
@bunny = @opts[:connection]
|
22
|
+
@bunny ||= create_bunny_connection
|
23
|
+
@bunny.start
|
24
|
+
|
25
|
+
@channel = @bunny.create_channel
|
26
|
+
@channel.prefetch(@opts[:prefetch])
|
27
|
+
|
28
|
+
exchange_name = @opts[:exchange]
|
29
|
+
@exchange = @channel.exchange(exchange_name, **@opts[:exchange_options])
|
30
|
+
|
31
|
+
routing_key = @opts[:routing_key] || @name
|
32
|
+
routing_keys = [*routing_key]
|
33
|
+
|
34
|
+
handler_klass = worker.opts[:handler] || Sneakers::CONFIG.fetch(:handler)
|
35
|
+
# Configure options if needed
|
36
|
+
if handler_klass.respond_to?(:configure_queue)
|
37
|
+
@opts[:queue_options] = handler_klass.configure_queue(@name, @opts)
|
38
|
+
end
|
39
|
+
|
40
|
+
queue = @channel.queue(@name, **@opts[:queue_options])
|
41
|
+
|
42
|
+
if exchange_name.length > 0
|
43
|
+
routing_keys.each do |key|
|
44
|
+
if @opts[:bind_arguments]
|
45
|
+
queue.bind(@exchange, routing_key: key, arguments: @opts[:bind_arguments])
|
46
|
+
else
|
47
|
+
queue.bind(@exchange, routing_key: key)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# NOTE: we are using the worker's options. This is necessary so the handler
|
53
|
+
# has the same configuration as the worker. Also pass along the exchange and
|
54
|
+
# queue in case the handler requires access to them (for things like binding
|
55
|
+
# retry queues, etc).
|
56
|
+
handler = handler_klass.new(@channel, queue, worker.opts)
|
57
|
+
|
58
|
+
@consumer = queue.subscribe(block: false, manual_ack: @opts[:ack]) do | delivery_info, metadata, msg |
|
59
|
+
worker.do_work(delivery_info, metadata, msg, handler)
|
60
|
+
end
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def unsubscribe
|
65
|
+
return unless @consumer
|
66
|
+
|
67
|
+
# TODO: should we simply close the channel here?
|
68
|
+
Sneakers.logger.info("Queue: will try to cancel consumer #{@consumer.inspect}")
|
69
|
+
cancel_ok = @consumer.cancel
|
70
|
+
if cancel_ok
|
71
|
+
Sneakers.logger.info "Queue: consumer #{cancel_ok.consumer_tag} cancelled"
|
72
|
+
@consumer = nil
|
73
|
+
else
|
74
|
+
Sneakers.logger.warn "Queue: could not cancel consumer #{@consumer.inspect}"
|
75
|
+
sleep(1)
|
76
|
+
unsubscribe
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def create_bunny_connection
|
81
|
+
Bunny.new(@opts[:amqp], { vhost: @opts[:vhost],
|
82
|
+
heartbeat: @opts[:heartbeat],
|
83
|
+
properties: @opts.fetch(:properties, {}),
|
84
|
+
logger: Sneakers::logger })
|
85
|
+
end
|
86
|
+
private :create_bunny_connection
|
87
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'serverengine'
|
2
|
+
require 'sneakers/workergroup'
|
3
|
+
|
4
|
+
module Sneakers
|
5
|
+
class Runner
|
6
|
+
def initialize(worker_classes, opts={})
|
7
|
+
@runnerconfig = RunnerConfig.new(worker_classes, opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
@se = ServerEngine.create(nil, WorkerGroup) { @runnerconfig.reload_config! }
|
12
|
+
@se.run
|
13
|
+
end
|
14
|
+
|
15
|
+
def stop(stop_graceful=true)
|
16
|
+
@se.stop(stop_graceful)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
class RunnerConfig
|
22
|
+
def method_missing(meth, *args, &block)
|
23
|
+
if %w{ before_fork after_fork }.include? meth.to_s
|
24
|
+
@conf[meth] = block
|
25
|
+
elsif %w{ workers start_worker_delay amqp }.include? meth.to_s
|
26
|
+
@conf[meth] = args.first
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(worker_classes, opts)
|
33
|
+
@worker_classes = worker_classes
|
34
|
+
@conf = opts
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_h
|
38
|
+
@conf
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def reload_config!
|
43
|
+
Sneakers.logger.info("Loading runner configuration...")
|
44
|
+
config_file = Sneakers::CONFIG[:runner_config_file]
|
45
|
+
|
46
|
+
if config_file
|
47
|
+
begin
|
48
|
+
instance_eval(File.read(config_file), config_file)
|
49
|
+
Sneakers.logger.info("Loading config with file: #{config_file}")
|
50
|
+
rescue
|
51
|
+
Sneakers.logger.error("Cannot load from file '#{config_file}', #{$!}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
config = make_serverengine_config
|
56
|
+
|
57
|
+
[:before_fork, :after_fork].each do | hook |
|
58
|
+
Sneakers::CONFIG[:hooks][hook] = config.delete(hook) if config[hook]
|
59
|
+
end
|
60
|
+
|
61
|
+
Sneakers.logger.debug("New configuration: #{config.inspect}")
|
62
|
+
config
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def make_serverengine_config
|
68
|
+
# From Sneakers#setup_general_logger, there's support for a Logger object
|
69
|
+
# in CONFIG[:log]. However, serverengine takes an object in :logger.
|
70
|
+
# Pass our logger object so there's no issue about sometimes passing a
|
71
|
+
# file and sometimes an object.
|
72
|
+
serverengine_config = Sneakers::CONFIG.merge(@conf)
|
73
|
+
serverengine_config.merge!(
|
74
|
+
:logger => Sneakers.logger,
|
75
|
+
:log_level => Sneakers.logger.level,
|
76
|
+
:worker_type => 'process',
|
77
|
+
:worker_classes => @worker_classes,
|
78
|
+
|
79
|
+
# Turning off serverengine internal logging infra, causes
|
80
|
+
# livelock and hang.
|
81
|
+
# see https://github.com/jondot/sneakers/issues/153
|
82
|
+
:log_stdout => false,
|
83
|
+
:log_stderr => false
|
84
|
+
)
|
85
|
+
serverengine_config.delete(:log)
|
86
|
+
|
87
|
+
serverengine_config
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
module Sneakers
|
5
|
+
class Spawner
|
6
|
+
def self.spawn
|
7
|
+
worker_group_config_file = ENV['WORKER_GROUP_CONFIG'] || './config/sneaker_worker_groups.yml'
|
8
|
+
unless File.exist?(worker_group_config_file)
|
9
|
+
puts 'No worker group file found.'
|
10
|
+
puts "Specify via ENV 'WORKER_GROUP_CONFIG' or by convention ./config/sneaker_worker_groups.yml"
|
11
|
+
Kernel.exit(1)
|
12
|
+
end
|
13
|
+
@pids = []
|
14
|
+
@exec_string = 'bundle exec rake sneakers:run'
|
15
|
+
worker_config = YAML.load(ERB.new(File.read(worker_group_config_file)).result)
|
16
|
+
worker_config.keys.each do |group_name|
|
17
|
+
workers = worker_config[group_name]['classes']
|
18
|
+
workers = workers.join ',' if workers.is_a?(Array)
|
19
|
+
@pids << fork do
|
20
|
+
@exec_hash = { 'WORKERS' => workers, 'WORKER_COUNT' => worker_config[group_name]['workers'].to_s }
|
21
|
+
Kernel.exec(@exec_hash, @exec_string)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
%w[TERM USR1 HUP USR2].each do |signal|
|
25
|
+
Signal.trap(signal) { @pids.each { |pid| Process.kill(signal, pid) } }
|
26
|
+
end
|
27
|
+
Process.waitall
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'time'
|
2
|
+
module Sneakers
|
3
|
+
module Support
|
4
|
+
class ProductionFormatter < Logger::Formatter
|
5
|
+
def self.call(severity, time, program_name, message)
|
6
|
+
"#{time.utc.iso8601} p-#{Process.pid} t-#{Thread.current.object_id.to_s(36)} #{severity}: #{message}\n"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Sneakers::Utils
|
2
|
+
def self.make_worker_id(namespace)
|
3
|
+
"worker-#{namespace}:#{'1'}:#{rand(36**6).floor.to_s(36)}" # jid, worker id. include date.
|
4
|
+
end
|
5
|
+
def self.parse_workers(workerstring)
|
6
|
+
missing_workers = []
|
7
|
+
workers = (workerstring || '').split(',').map do |k|
|
8
|
+
begin
|
9
|
+
w = Kernel.const_get(k)
|
10
|
+
rescue
|
11
|
+
missing_workers << k
|
12
|
+
end
|
13
|
+
w
|
14
|
+
end.compact
|
15
|
+
|
16
|
+
[workers, missing_workers]
|
17
|
+
end
|
18
|
+
end
|