faktory_worker_ruby 0.5.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.
- 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
|