faktory_worker_ruby 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +25 -0
- data/LICENSE +165 -0
- data/README.md +91 -0
- data/Rakefile +10 -0
- data/bin/faktory-worker +18 -0
- data/faktory_worker_ruby.gemspec +24 -0
- data/lib/faktory.rb +166 -0
- data/lib/faktory/cli.rb +296 -0
- data/lib/faktory/client.rb +227 -0
- data/lib/faktory/connection.rb +15 -0
- data/lib/faktory/exception_handler.rb +31 -0
- data/lib/faktory/fetch.rb +44 -0
- data/lib/faktory/job.rb +180 -0
- data/lib/faktory/job_logger.rb +24 -0
- data/lib/faktory/launcher.rb +66 -0
- data/lib/faktory/logging.rb +72 -0
- data/lib/faktory/manager.rb +129 -0
- data/lib/faktory/middleware/chain.rb +150 -0
- data/lib/faktory/middleware/i18n.rb +43 -0
- data/lib/faktory/processor.rb +176 -0
- data/lib/faktory/rails.rb +33 -0
- data/lib/faktory/util.rb +62 -0
- data/lib/faktory/version.rb +4 -0
- metadata +132 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'connection_pool'
|
3
|
+
|
4
|
+
module Faktory
|
5
|
+
class Connection
|
6
|
+
class << self
|
7
|
+
def create(options={})
|
8
|
+
size = Faktory.worker? ? (Faktory.options[:concurrency] + 2) : 5
|
9
|
+
ConnectionPool.new(:timeout => options[:pool_timeout] || 1, :size => size) do
|
10
|
+
Faktory::Client.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'faktory'
|
3
|
+
|
4
|
+
module Faktory
|
5
|
+
module ExceptionHandler
|
6
|
+
|
7
|
+
class Logger
|
8
|
+
def call(ex, ctxHash)
|
9
|
+
Faktory.logger.warn(Faktory.dump_json(ctxHash)) if !ctxHash.empty?
|
10
|
+
Faktory.logger.warn "#{ex.class.name}: #{ex.message}"
|
11
|
+
Faktory.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
# Set up default handler which just logs the error
|
15
|
+
Faktory.error_handlers << Faktory::ExceptionHandler::Logger.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle_exception(ex, ctxHash={})
|
19
|
+
Faktory.error_handlers.each do |handler|
|
20
|
+
begin
|
21
|
+
handler.call(ex, ctxHash)
|
22
|
+
rescue => ex
|
23
|
+
Faktory.logger.error "!!! ERROR HANDLER THREW AN ERROR !!!"
|
24
|
+
Faktory.logger.error ex
|
25
|
+
Faktory.logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faktory
|
4
|
+
UnitOfWork = Struct.new(:job) do
|
5
|
+
def acknowledge
|
6
|
+
Faktory.server {|c| c.ack(jid) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def fail(ex)
|
10
|
+
Faktory.server {|c| c.fail(jid, ex) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def jid
|
14
|
+
job['jid']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Fetcher
|
19
|
+
def initialize(options)
|
20
|
+
@strictly_ordered_queues = !!options[:strict]
|
21
|
+
@queues = options[:queues]
|
22
|
+
@queues = @queues.uniq if @strictly_ordered_queues
|
23
|
+
end
|
24
|
+
|
25
|
+
def retrieve_work
|
26
|
+
work = Faktory.server { |conn| conn.fetch(*queues_cmd) }
|
27
|
+
UnitOfWork.new(work) if work
|
28
|
+
end
|
29
|
+
|
30
|
+
# Creating the pop command takes into account any
|
31
|
+
# configured queue weights. By default pop returns
|
32
|
+
# data from the first queue that has pending elements. We
|
33
|
+
# recreate the queue command each time we invoke pop
|
34
|
+
# to honor weights and avoid queue starvation.
|
35
|
+
def queues_cmd
|
36
|
+
if @strictly_ordered_queues
|
37
|
+
@queues
|
38
|
+
else
|
39
|
+
@queues.shuffle.uniq
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
data/lib/faktory/job.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Faktory
|
3
|
+
|
4
|
+
##
|
5
|
+
# Include this module in your Job class and you can easily create
|
6
|
+
# asynchronous jobs:
|
7
|
+
#
|
8
|
+
# class HardJob
|
9
|
+
# include Faktory::Job
|
10
|
+
#
|
11
|
+
# def perform(*args)
|
12
|
+
# # do some work
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Then in your Rails app, you can do this:
|
17
|
+
#
|
18
|
+
# HardJob.perform_async(1, 2, 3)
|
19
|
+
#
|
20
|
+
# Note that perform_async is a class method, perform is an instance method.
|
21
|
+
module Job
|
22
|
+
attr_accessor :jid
|
23
|
+
|
24
|
+
def self.included(base)
|
25
|
+
raise ArgumentError, "You cannot include Faktory::Job in an ActiveJob: #{base.name}" if base.ancestors.any? {|c| c.name == 'ActiveJob::Base' }
|
26
|
+
|
27
|
+
base.extend(ClassMethods)
|
28
|
+
base.faktory_class_attribute :faktory_options_hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def logger
|
32
|
+
Faktory.logger
|
33
|
+
end
|
34
|
+
|
35
|
+
# This helper class encapsulates the set options for `set`, e.g.
|
36
|
+
#
|
37
|
+
# SomeJob.set(queue: 'foo').perform_async(....)
|
38
|
+
#
|
39
|
+
class Setter
|
40
|
+
def initialize(opts)
|
41
|
+
@opts = opts
|
42
|
+
end
|
43
|
+
|
44
|
+
def perform_async(*args)
|
45
|
+
@opts['jobtype'.freeze].client_push(@opts.merge!('args'.freeze => args))
|
46
|
+
end
|
47
|
+
|
48
|
+
# +interval+ must be a timestamp, numeric or something that acts
|
49
|
+
# numeric (like an activesupport time interval).
|
50
|
+
def perform_in(interval, *args)
|
51
|
+
int = interval.to_f
|
52
|
+
now = Time.now.to_f
|
53
|
+
ts = (int < 1_000_000_000 ? now + int : int)
|
54
|
+
at = Time.at(ts).utc.to_datetime.rfc3339(9)
|
55
|
+
|
56
|
+
@opts.merge! 'args'.freeze => args, 'at'.freeze => at
|
57
|
+
# Optimization to enqueue something now that is scheduled to go out now or in the past
|
58
|
+
@opts.delete('at'.freeze) if ts <= now
|
59
|
+
@opts['jobtype'.freeze].client_push(@opts)
|
60
|
+
end
|
61
|
+
alias_method :perform_at, :perform_in
|
62
|
+
end
|
63
|
+
|
64
|
+
module ClassMethods
|
65
|
+
|
66
|
+
def set(options)
|
67
|
+
Setter.new(options.merge!('jobtype'.freeze => self))
|
68
|
+
end
|
69
|
+
|
70
|
+
def perform_async(*args)
|
71
|
+
client_push('jobtype'.freeze => self, 'args'.freeze => args)
|
72
|
+
end
|
73
|
+
|
74
|
+
# +interval+ must be a timestamp, numeric or something that acts
|
75
|
+
# numeric (like an activesupport time interval).
|
76
|
+
def perform_in(interval, *args)
|
77
|
+
int = interval.to_f
|
78
|
+
now = Time.now.to_f
|
79
|
+
ts = (int < 1_000_000_000 ? now + int : int)
|
80
|
+
item = { 'jobtype'.freeze => self, 'args'.freeze => args }
|
81
|
+
|
82
|
+
item['at'] = Time.at(ts).utc.to_datetime.rfc3339(9) if ts > now
|
83
|
+
client_push(item)
|
84
|
+
end
|
85
|
+
alias_method :perform_at, :perform_in
|
86
|
+
|
87
|
+
##
|
88
|
+
# Allows customization of Faktory features for this type of Job.
|
89
|
+
# Legal options:
|
90
|
+
#
|
91
|
+
# queue - use a named queue for this Job, default 'default'
|
92
|
+
# retry - enable automatic retry for this Job, *Integer* count, default 25
|
93
|
+
# backtrace - whether to save the error backtrace in the job payload to display in web UI,
|
94
|
+
# an integer number of lines to save, default *0*
|
95
|
+
#
|
96
|
+
def faktory_options(opts={})
|
97
|
+
# stringify
|
98
|
+
self.faktory_options_hash = get_faktory_options.merge(Hash[opts.map{|k, v| [k.to_s, v]}])
|
99
|
+
end
|
100
|
+
|
101
|
+
def get_faktory_options # :nodoc:
|
102
|
+
self.faktory_options_hash ||= Faktory.default_job_options
|
103
|
+
end
|
104
|
+
|
105
|
+
def client_push(item) # :nodoc:
|
106
|
+
pool = Thread.current[:faktory_via_pool] || get_faktory_options['pool'.freeze] || Faktory.server_pool
|
107
|
+
item = get_faktory_options.merge(item)
|
108
|
+
# stringify
|
109
|
+
item.keys.each do |key|
|
110
|
+
item[key.to_s] = item.delete(key)
|
111
|
+
end
|
112
|
+
item["jid"] ||= SecureRandom.hex(12)
|
113
|
+
item["queue"] ||= "default"
|
114
|
+
|
115
|
+
Faktory.client_middleware.invoke(item, pool) do
|
116
|
+
pool.with do |c|
|
117
|
+
c.push(item)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def faktory_class_attribute(*attrs)
|
123
|
+
instance_reader = true
|
124
|
+
instance_writer = true
|
125
|
+
|
126
|
+
attrs.each do |name|
|
127
|
+
singleton_class.instance_eval do
|
128
|
+
undef_method(name) if method_defined?(name) || private_method_defined?(name)
|
129
|
+
end
|
130
|
+
define_singleton_method(name) { nil }
|
131
|
+
|
132
|
+
ivar = "@#{name}"
|
133
|
+
|
134
|
+
singleton_class.instance_eval do
|
135
|
+
m = "#{name}="
|
136
|
+
undef_method(m) if method_defined?(m) || private_method_defined?(m)
|
137
|
+
end
|
138
|
+
define_singleton_method("#{name}=") do |val|
|
139
|
+
singleton_class.class_eval do
|
140
|
+
undef_method(name) if method_defined?(name) || private_method_defined?(name)
|
141
|
+
define_method(name) { val }
|
142
|
+
end
|
143
|
+
|
144
|
+
if singleton_class?
|
145
|
+
class_eval do
|
146
|
+
undef_method(name) if method_defined?(name) || private_method_defined?(name)
|
147
|
+
define_method(name) do
|
148
|
+
if instance_variable_defined? ivar
|
149
|
+
instance_variable_get ivar
|
150
|
+
else
|
151
|
+
singleton_class.send name
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
val
|
157
|
+
end
|
158
|
+
|
159
|
+
if instance_reader
|
160
|
+
undef_method(name) if method_defined?(name) || private_method_defined?(name)
|
161
|
+
define_method(name) do
|
162
|
+
if instance_variable_defined?(ivar)
|
163
|
+
instance_variable_get ivar
|
164
|
+
else
|
165
|
+
self.class.public_send name
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
if instance_writer
|
171
|
+
m = "#{name}="
|
172
|
+
undef_method(m) if method_defined?(m) || private_method_defined?(m)
|
173
|
+
attr_writer name
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Faktory
|
2
|
+
class JobLogger
|
3
|
+
|
4
|
+
def call(item)
|
5
|
+
start = Time.now
|
6
|
+
logger.info("start".freeze)
|
7
|
+
yield
|
8
|
+
logger.info("done: #{elapsed(start)} sec")
|
9
|
+
rescue Exception
|
10
|
+
logger.info("fail: #{elapsed(start)} sec")
|
11
|
+
raise
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def elapsed(start)
|
17
|
+
(Time.now - start).round(3)
|
18
|
+
end
|
19
|
+
|
20
|
+
def logger
|
21
|
+
Faktory.logger
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
require 'faktory/manager'
|
4
|
+
|
5
|
+
module Faktory
|
6
|
+
class Launcher
|
7
|
+
include Util
|
8
|
+
|
9
|
+
attr_accessor :manager
|
10
|
+
|
11
|
+
def initialize(options)
|
12
|
+
@manager = Faktory::Manager.new(options)
|
13
|
+
@done = false
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
@thread = safe_thread("heartbeat", &method(:heartbeat))
|
19
|
+
@manager.start
|
20
|
+
end
|
21
|
+
|
22
|
+
# Stops this instance from processing any more jobs,
|
23
|
+
def quiet
|
24
|
+
@done = true
|
25
|
+
@manager.quiet
|
26
|
+
end
|
27
|
+
|
28
|
+
# Shuts down the process. This method does not
|
29
|
+
# return until all work is complete and cleaned up.
|
30
|
+
# It can take up to the timeout to complete.
|
31
|
+
def stop
|
32
|
+
deadline = Time.now + @options[:timeout]
|
33
|
+
|
34
|
+
@done = true
|
35
|
+
@manager.quiet
|
36
|
+
@manager.stop(deadline)
|
37
|
+
end
|
38
|
+
|
39
|
+
def stopping?
|
40
|
+
@done
|
41
|
+
end
|
42
|
+
|
43
|
+
PROCTITLES = []
|
44
|
+
|
45
|
+
private unless $TESTING
|
46
|
+
|
47
|
+
def heartbeat
|
48
|
+
title = ['faktory-worker', Faktory::VERSION, @options[:tag]].compact.join(" ")
|
49
|
+
PROCTITLES << proc { title }
|
50
|
+
PROCTITLES << proc { "[#{Processor.busy_count} of #{@options[:concurrency]} busy]" }
|
51
|
+
PROCTITLES << proc { "stopping" if stopping? }
|
52
|
+
|
53
|
+
loop do
|
54
|
+
$0 = PROCTITLES.map {|p| p.call }.join(" ")
|
55
|
+
|
56
|
+
begin
|
57
|
+
Faktory.server {|c| c.beat }
|
58
|
+
rescue => ex
|
59
|
+
# best effort, try again in a few secs
|
60
|
+
end
|
61
|
+
sleep 10
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'time'
|
3
|
+
require 'logger'
|
4
|
+
require 'fcntl'
|
5
|
+
|
6
|
+
module Faktory
|
7
|
+
module Logging
|
8
|
+
|
9
|
+
class Pretty < Logger::Formatter
|
10
|
+
SPACE = " "
|
11
|
+
|
12
|
+
# Provide a call() method that returns the formatted message.
|
13
|
+
def call(severity, time, program_name, message)
|
14
|
+
"#{time.utc.iso8601(3)} #{::Process.pid} TID-#{Thread.current.object_id.to_s(36)}#{context} #{severity}: #{message}\n"
|
15
|
+
end
|
16
|
+
|
17
|
+
def context
|
18
|
+
c = Thread.current[:faktory_context]
|
19
|
+
" #{c.join(SPACE)}" if c && c.any?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class WithoutTimestamp < Pretty
|
24
|
+
def call(severity, time, program_name, message)
|
25
|
+
"#{::Process.pid} TID-#{Thread.current.object_id.to_s(36)}#{context} #{severity}: #{message}\n"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.job_hash_context(job_hash)
|
30
|
+
# If we're using a wrapper class, like ActiveJob, use the "wrapped"
|
31
|
+
# attribute to expose the underlying thing.
|
32
|
+
klass = job_hash['wrapped'.freeze] || job_hash["jobtype".freeze]
|
33
|
+
"#{klass} JID-#{job_hash['jid'.freeze]}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.with_job_hash_context(job_hash, &block)
|
37
|
+
with_context(job_hash_context(job_hash), &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.with_context(msg)
|
41
|
+
Thread.current[:faktory_context] ||= []
|
42
|
+
Thread.current[:faktory_context] << msg
|
43
|
+
yield
|
44
|
+
ensure
|
45
|
+
Thread.current[:faktory_context].pop
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.initialize_logger(log_target = STDOUT)
|
49
|
+
oldlogger = defined?(@logger) ? @logger : nil
|
50
|
+
@logger = Logger.new(log_target)
|
51
|
+
@logger.level = Logger::INFO
|
52
|
+
# We assume that any TTY is logging directly to a terminal and needs timestamps.
|
53
|
+
# We assume that any non-TTY is logging to Upstart/Systemd/syslog/Heroku/etc with a decent
|
54
|
+
# logging subsystem that provides a timestamp for each entry.
|
55
|
+
@logger.formatter = log_target.tty? ? Pretty.new : WithoutTimestamp.new
|
56
|
+
oldlogger.close if oldlogger && !$TESTING # don't want to close testing's STDOUT logging
|
57
|
+
@logger
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.logger
|
61
|
+
defined?(@logger) ? @logger : initialize_logger
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.logger=(log)
|
65
|
+
@logger = (log ? log : Logger.new(File::NULL))
|
66
|
+
end
|
67
|
+
|
68
|
+
def logger
|
69
|
+
Faktory::Logging.logger
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
require 'faktory/util'
|
4
|
+
require 'faktory/processor'
|
5
|
+
require 'faktory/fetch'
|
6
|
+
require 'thread'
|
7
|
+
require 'set'
|
8
|
+
|
9
|
+
module Faktory
|
10
|
+
|
11
|
+
##
|
12
|
+
# The Manager is the central coordination point in Faktory, controlling
|
13
|
+
# the lifecycle of the Processors.
|
14
|
+
#
|
15
|
+
# Tasks:
|
16
|
+
#
|
17
|
+
# 1. start: Spin up Processors.
|
18
|
+
# 3. processor_died: Handle job failure, throw away Processor, create new one.
|
19
|
+
# 4. quiet: shutdown idle Processors.
|
20
|
+
# 5. stop: hard stop the Processors by deadline.
|
21
|
+
#
|
22
|
+
# Note that only the last task requires its own Thread since it has to monitor
|
23
|
+
# the shutdown process. The other tasks are performed by other threads.
|
24
|
+
#
|
25
|
+
class Manager
|
26
|
+
include Util
|
27
|
+
|
28
|
+
attr_reader :threads
|
29
|
+
attr_reader :options
|
30
|
+
|
31
|
+
def initialize(options={})
|
32
|
+
logger.debug { options.inspect }
|
33
|
+
@options = options
|
34
|
+
@count = options[:concurrency] || 25
|
35
|
+
raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
|
36
|
+
|
37
|
+
@done = false
|
38
|
+
@threads = Set.new
|
39
|
+
@count.times do
|
40
|
+
@threads << Processor.new(self)
|
41
|
+
end
|
42
|
+
@plock = Mutex.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def start
|
46
|
+
@threads.each do |x|
|
47
|
+
x.start
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def quiet
|
52
|
+
return if @done
|
53
|
+
@done = true
|
54
|
+
|
55
|
+
logger.info { "Terminating quiet threads" }
|
56
|
+
@threads.each { |x| x.terminate }
|
57
|
+
fire_event(:quiet, true)
|
58
|
+
end
|
59
|
+
|
60
|
+
# hack for quicker development / testing environment
|
61
|
+
PAUSE_TIME = STDOUT.tty? ? 0.1 : 0.5
|
62
|
+
|
63
|
+
def stop(deadline)
|
64
|
+
quiet
|
65
|
+
fire_event(:shutdown, true)
|
66
|
+
|
67
|
+
# some of the shutdown events can be async,
|
68
|
+
# we don't have any way to know when they're done but
|
69
|
+
# give them a little time to take effect
|
70
|
+
sleep PAUSE_TIME
|
71
|
+
return if @threads.empty?
|
72
|
+
|
73
|
+
logger.info { "Pausing to allow threads to finish..." }
|
74
|
+
remaining = deadline - Time.now
|
75
|
+
while remaining > PAUSE_TIME
|
76
|
+
return if @threads.empty?
|
77
|
+
sleep PAUSE_TIME
|
78
|
+
remaining = deadline - Time.now
|
79
|
+
end
|
80
|
+
return if @threads.empty?
|
81
|
+
|
82
|
+
hard_shutdown
|
83
|
+
end
|
84
|
+
|
85
|
+
def processor_stopped(processor)
|
86
|
+
@plock.synchronize do
|
87
|
+
@threads.delete(processor)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def processor_died(processor, reason)
|
92
|
+
@plock.synchronize do
|
93
|
+
@threads.delete(processor)
|
94
|
+
unless @done
|
95
|
+
p = Processor.new(self)
|
96
|
+
@threads << p
|
97
|
+
p.start
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def stopped?
|
103
|
+
@done
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def hard_shutdown
|
109
|
+
# We've reached the timeout and we still have busy threads.
|
110
|
+
# They must die but their jobs shall live on.
|
111
|
+
cleanup = nil
|
112
|
+
@plock.synchronize do
|
113
|
+
cleanup = @threads.dup
|
114
|
+
end
|
115
|
+
|
116
|
+
if cleanup.size > 0
|
117
|
+
jobs = cleanup.map {|p| p.job }.compact
|
118
|
+
|
119
|
+
logger.warn { "Terminating #{cleanup.size} busy worker threads" }
|
120
|
+
logger.warn { "Work still in progress #{jobs.inspect}" }
|
121
|
+
end
|
122
|
+
|
123
|
+
cleanup.each do |processor|
|
124
|
+
processor.kill
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|