symphony 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/ChangeLog +91 -3
- data/History.rdoc +13 -0
- data/Manifest.txt +10 -1
- data/README.rdoc +1 -1
- data/Rakefile +5 -5
- data/UPGRADING.md +38 -0
- data/USAGE.rdoc +1 -1
- data/lib/symphony.rb +86 -5
- data/lib/symphony/daemon.rb +94 -147
- data/lib/symphony/queue.rb +11 -4
- data/lib/symphony/routing.rb +5 -4
- data/lib/symphony/signal_handling.rb +1 -2
- data/lib/symphony/statistics.rb +96 -0
- data/lib/symphony/task.rb +71 -11
- data/lib/symphony/task_group.rb +165 -0
- data/lib/symphony/task_group/longlived.rb +98 -0
- data/lib/symphony/task_group/oneshot.rb +25 -0
- data/lib/symphony/tasks/oneshot_simulator.rb +61 -0
- data/lib/symphony/tasks/simulator.rb +22 -18
- data/spec/helpers.rb +67 -1
- data/spec/symphony/daemon_spec.rb +83 -31
- data/spec/symphony/queue_spec.rb +2 -2
- data/spec/symphony/routing_spec.rb +297 -0
- data/spec/symphony/statistics_spec.rb +71 -0
- data/spec/symphony/task_group/longlived_spec.rb +219 -0
- data/spec/symphony/task_group/oneshot_spec.rb +56 -0
- data/spec/symphony/task_group_spec.rb +93 -0
- data/spec/symphony_spec.rb +6 -0
- metadata +41 -18
- metadata.gz.sig +0 -0
- data/TODO.md +0 -3
data/lib/symphony/queue.rb
CHANGED
@@ -8,6 +8,10 @@ require 'symphony' unless defined?( Symphony )
|
|
8
8
|
require 'symphony/mixins'
|
9
9
|
|
10
10
|
|
11
|
+
Bunny.extend( Loggability )
|
12
|
+
Bunny.log_as( :amqp )
|
13
|
+
|
14
|
+
|
11
15
|
# An object class that encapsulates queueing logic for Symphony jobs.
|
12
16
|
class Symphony::Queue
|
13
17
|
extend Loggability,
|
@@ -66,7 +70,7 @@ class Symphony::Queue
|
|
66
70
|
### Fetch a Hash of AMQP options.
|
67
71
|
def self::amqp_session_options
|
68
72
|
opts = self.session_opts.merge({
|
69
|
-
logger: Loggability[
|
73
|
+
logger: Loggability[ Bunny ],
|
70
74
|
})
|
71
75
|
opts[:heartbeat] = opts[:heartbeat].to_sym if opts[:heartbeat].is_a?( String )
|
72
76
|
|
@@ -131,6 +135,7 @@ class Symphony::Queue
|
|
131
135
|
### Fetch the configured AMQP exchange interface object.
|
132
136
|
def self::amqp_exchange
|
133
137
|
unless self.amqp[:exchange]
|
138
|
+
self.log.info "Getting a reference to the %s topic exchange" % [ self.exchange ]
|
134
139
|
self.amqp[:exchange] = self.amqp_channel.topic( self.exchange, passive: true )
|
135
140
|
end
|
136
141
|
return self.amqp[:exchange]
|
@@ -281,12 +286,14 @@ class Symphony::Queue
|
|
281
286
|
|
282
287
|
# Re-raise errors from AMQP
|
283
288
|
rescue Bunny::Exception => err
|
284
|
-
self.log.error "%p while handling a message: %s" %
|
289
|
+
self.log.error "%p while handling a message: %s %s" %
|
290
|
+
[ err.class, err.message, err.backtrace.first ]
|
285
291
|
self.log.debug " " + err.backtrace.join( "\n " )
|
286
292
|
raise
|
287
293
|
|
288
294
|
rescue => err
|
289
|
-
self.log.error "%p while handling a message: %s" %
|
295
|
+
self.log.error "%p while handling a message: %s %s" %
|
296
|
+
[ err.class, err.message, err.backtrace.first ]
|
290
297
|
self.log.debug " " + err.backtrace.join( "\n " )
|
291
298
|
return self.ack_message( delivery_info.delivery_tag, false, false )
|
292
299
|
end
|
@@ -313,7 +320,7 @@ class Symphony::Queue
|
|
313
320
|
### Close the AMQP session associated with this queue.
|
314
321
|
def shutdown
|
315
322
|
self.shutting_down = true
|
316
|
-
self.consumer.cancel
|
323
|
+
self.consumer.cancel if self.consumer
|
317
324
|
end
|
318
325
|
|
319
326
|
|
data/lib/symphony/routing.rb
CHANGED
@@ -70,11 +70,12 @@ module Symphony::Routing
|
|
70
70
|
### Return a regular expression that will match messages matching the given
|
71
71
|
### +routing_key+.
|
72
72
|
def make_routing_pattern( routing_key )
|
73
|
-
re_string =
|
74
|
-
|
75
|
-
|
73
|
+
re_string = Regexp.escape( routing_key ).
|
74
|
+
gsub( /\\\*/, '[^\.]*' ).
|
75
|
+
gsub( /\\.\\#/, '(\..*)?' ).
|
76
|
+
gsub( /\\#\\./, '(.*\.)?' )
|
76
77
|
|
77
|
-
return Regexp.compile( re_string )
|
78
|
+
return Regexp.compile( '^' + re_string + '$' )
|
78
79
|
end
|
79
80
|
|
80
81
|
end # module ClassMethods
|
@@ -45,13 +45,12 @@ module Symphony::SignalHandling
|
|
45
45
|
def wait_for_signals( timeout=nil )
|
46
46
|
|
47
47
|
# Wait on the selfpipe for signals
|
48
|
-
self.log.debug " waiting for the selfpipe"
|
48
|
+
# self.log.debug " waiting for the selfpipe"
|
49
49
|
fds = IO.select( [@selfpipe[:reader]], [], [], timeout )
|
50
50
|
begin
|
51
51
|
rval = @selfpipe[:reader].read_nonblock( 11 )
|
52
52
|
self.log.debug " read from the selfpipe: %p" % [ rval ]
|
53
53
|
rescue Errno::EAGAIN, Errno::EINTR => err
|
54
|
-
self.log.debug " %p: %s!" % [ err.class, err.message ]
|
55
54
|
# ignore
|
56
55
|
end
|
57
56
|
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'loggability'
|
5
|
+
require 'symphony' unless defined?( Symphony )
|
6
|
+
|
7
|
+
|
8
|
+
# A collection of statistics functions for various Symphony systems. Special
|
9
|
+
# thanks to Justin Z. Smith <justin@statisticool.com> for the maths. Good luck
|
10
|
+
# with your search for economic collapse in applesauce!
|
11
|
+
module Symphony::Statistics
|
12
|
+
|
13
|
+
# The default number of samples to keep
|
14
|
+
DEFAULT_SAMPLE_SIZE = 100
|
15
|
+
|
16
|
+
|
17
|
+
### Set up some instance variables for tracking statistical info when the object
|
18
|
+
### is created.
|
19
|
+
def initialize( * )
|
20
|
+
super
|
21
|
+
@samples = []
|
22
|
+
@sample_size = DEFAULT_SAMPLE_SIZE
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Samples of the number of pending jobs
|
27
|
+
attr_reader :samples
|
28
|
+
|
29
|
+
##
|
30
|
+
# The number of samples to keep for analysis, and required
|
31
|
+
# before trending is performed.
|
32
|
+
attr_accessor :sample_size
|
33
|
+
|
34
|
+
|
35
|
+
### Add the specified +value+ as a sample for the current time.
|
36
|
+
def add_sample( value )
|
37
|
+
@samples << [ Time.now.to_f, value ]
|
38
|
+
@samples.pop( @samples.size - self.sample_size ) if @samples.size > self.sample_size
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
### Returns +true+ if the samples gathered so far indicate an upwards trend.
|
43
|
+
def sample_values_increasing?
|
44
|
+
return self.calculate_trend > 3
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
### Returns +true+ if the samples gathered so far indicate a downwards trend.
|
49
|
+
def sample_values_decreasing?
|
50
|
+
return self.calculate_trend < -3
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
### Predict the likelihood that an upward trend will continue based on linear regression
|
55
|
+
### analysis of the given samples. If the value returned is >= 3.0, the values are
|
56
|
+
### statistically trending upwards, which in Symphony's case means that the workers are
|
57
|
+
### not handling the incoming work.
|
58
|
+
def calculate_trend
|
59
|
+
return 0 unless self.samples.size >= self.sample_size
|
60
|
+
# Loggability[ Symphony ].debug "%d samples of required %d" % [ self.samples.size, self.sample_size ]
|
61
|
+
|
62
|
+
x_vec, y_vec = self.samples.transpose
|
63
|
+
|
64
|
+
y_avg = y_vec.inject( :+ ).to_f / y_vec.size
|
65
|
+
x_avg = x_vec.inject( :+ ).to_f / x_vec.size
|
66
|
+
|
67
|
+
# Find slope and y-intercept.
|
68
|
+
#
|
69
|
+
n = d = 0
|
70
|
+
samples.each do |x, y_val|
|
71
|
+
xv = x - x_avg
|
72
|
+
n = n + ( xv * ( y_val - y_avg ) )
|
73
|
+
d = d + ( xv ** 2 )
|
74
|
+
end
|
75
|
+
|
76
|
+
slope = n/d
|
77
|
+
y_intercept = y_avg - ( slope * x_avg )
|
78
|
+
|
79
|
+
# Find stderr.
|
80
|
+
#
|
81
|
+
r = s = 0
|
82
|
+
samples.each do |x, y_val|
|
83
|
+
yv = ( slope * x ) + y_intercept
|
84
|
+
r = r + ( (y_val - yv) ** 2 )
|
85
|
+
s = s + ( (x - x_avg) ** 2 )
|
86
|
+
end
|
87
|
+
|
88
|
+
stde = Math.sqrt( (r / ( samples.size - 2 )) / s )
|
89
|
+
|
90
|
+
# Loggability[ Symphony ].debug " job sampling trend is: %f" % [ slope / stde ]
|
91
|
+
return slope / stde
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
end # module Symphony::Statistics
|
96
|
+
|
data/lib/symphony/task.rb
CHANGED
@@ -30,6 +30,14 @@ class Symphony::Task
|
|
30
30
|
# Valid work model types
|
31
31
|
WORK_MODELS = %i[ longlived oneshot ]
|
32
32
|
|
33
|
+
# The number of seconds between checks to see if the worker is idle. Note that
|
34
|
+
# this is not the same as the idle timeout -- it's how often to check to see if
|
35
|
+
# the task has been idle for too long.
|
36
|
+
IDLE_CHECK_INTERVAL = 5 # seconds
|
37
|
+
|
38
|
+
# The default number of seconds to wait for work
|
39
|
+
DEFAULT_IDLE_TIMEOUT = IDLE_CHECK_INTERVAL * 2
|
40
|
+
|
33
41
|
|
34
42
|
# Loggability API -- log to symphony's logger
|
35
43
|
log_to :symphony
|
@@ -41,20 +49,20 @@ class Symphony::Task
|
|
41
49
|
|
42
50
|
### Create a new Task object and listen for work. Exits with the code returned
|
43
51
|
### by #start when it's done.
|
44
|
-
def self::run
|
52
|
+
def self::run( exit_on_idle=false )
|
45
53
|
if self.subscribe_to.empty?
|
46
54
|
raise ScriptError,
|
47
55
|
"No subscriptions defined. Add one or more patterns using subscribe_to."
|
48
56
|
end
|
49
57
|
|
50
|
-
exit self.new( self.queue ).start
|
58
|
+
exit self.new( self.queue, exit_on_idle ).start
|
51
59
|
end
|
52
60
|
|
53
61
|
|
54
62
|
### Prepare the process to be forked.
|
55
63
|
def self::before_fork
|
56
64
|
self.log.debug "Before fork [%d]: Threads: %p" % [ Process.pid, ThreadGroup::Default.list ]
|
57
|
-
|
65
|
+
Symphony::Queue.reset
|
58
66
|
end
|
59
67
|
|
60
68
|
|
@@ -76,6 +84,7 @@ class Symphony::Task
|
|
76
84
|
subclass.instance_variable_set( :@prefetch, 10 )
|
77
85
|
subclass.instance_variable_set( :@timeout_action, :reject )
|
78
86
|
subclass.instance_variable_set( :@persistent, false )
|
87
|
+
subclass.instance_variable_set( :@idle_timeout, DEFAULT_IDLE_TIMEOUT )
|
79
88
|
end
|
80
89
|
|
81
90
|
|
@@ -209,16 +218,33 @@ class Symphony::Task
|
|
209
218
|
end
|
210
219
|
|
211
220
|
|
221
|
+
### Get/set the maximum number of seconds the worker should wait for events to
|
222
|
+
### arrive before exiting.
|
223
|
+
def self::idle_timeout( seconds=nil, options={} )
|
224
|
+
unless seconds.nil?
|
225
|
+
self.log.info "Setting the idle timeout to %0.2fs." % [ seconds.to_f ]
|
226
|
+
@idle_timeout = seconds.to_f
|
227
|
+
end
|
228
|
+
|
229
|
+
return @idle_timeout
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
|
234
|
+
|
212
235
|
#
|
213
236
|
# Instance Methods
|
214
237
|
#
|
215
238
|
|
216
239
|
### Create a worker that will listen on the specified +queue+ for a job.
|
217
|
-
def initialize( queue )
|
240
|
+
def initialize( queue, exit_on_idle=false )
|
218
241
|
@queue = queue
|
219
242
|
@signal_handler = nil
|
220
243
|
@shutting_down = false
|
221
244
|
@restarting = false
|
245
|
+
@idle_checker = nil
|
246
|
+
@last_worked = Time.now
|
247
|
+
@exit_on_idle = exit_on_idle
|
222
248
|
end
|
223
249
|
|
224
250
|
|
@@ -239,6 +265,15 @@ class Symphony::Task
|
|
239
265
|
# Is the task in the process of restarting?
|
240
266
|
attr_predicate_accessor :restarting
|
241
267
|
|
268
|
+
# The Thread that checks for idle timeout
|
269
|
+
attr_accessor :idle_checker
|
270
|
+
|
271
|
+
# The Time the task was last running
|
272
|
+
attr_accessor :last_worked
|
273
|
+
|
274
|
+
# Flag that determines whether this task instance exits when it doesn't have any work.
|
275
|
+
attr_predicate :exit_on_idle
|
276
|
+
|
242
277
|
|
243
278
|
### Set up the task and start handling messages.
|
244
279
|
def start
|
@@ -254,7 +289,7 @@ class Symphony::Task
|
|
254
289
|
return rval ? 0 : 1
|
255
290
|
|
256
291
|
rescue Exception => err
|
257
|
-
self.log.fatal "%p in %p: %s" % [ err.class, self.class, err.message ]
|
292
|
+
self.log.fatal "%p in %p: %s: %s" % [ err.class, self.class, err.message, err.backtrace.first ]
|
258
293
|
self.log.debug { ' ' + err.backtrace.join(" \n") }
|
259
294
|
|
260
295
|
return :software
|
@@ -299,27 +334,52 @@ class Symphony::Task
|
|
299
334
|
def start_handling_messages
|
300
335
|
oneshot = self.class.work_model == :oneshot
|
301
336
|
|
302
|
-
|
337
|
+
rval = nil
|
338
|
+
self.queue.wait_for_message( oneshot ) do |payload, metadata|
|
339
|
+
self.last_worked = nil
|
303
340
|
work_payload = self.preprocess_payload( payload, metadata )
|
304
341
|
|
305
|
-
if self.class.timeout
|
342
|
+
rval = if self.class.timeout
|
306
343
|
self.work_with_timeout( work_payload, metadata )
|
307
344
|
else
|
308
345
|
self.work( work_payload, metadata )
|
309
346
|
end
|
310
|
-
|
347
|
+
|
348
|
+
self.last_worked = Time.now
|
349
|
+
end
|
350
|
+
|
351
|
+
return rval
|
311
352
|
end
|
312
353
|
|
313
354
|
|
314
|
-
### Start the thread that will deliver signals once they're put on the queue
|
355
|
+
### Start the thread that will deliver signals once they're put on the queue, and
|
356
|
+
### check for the last time a job was handled by this task process.
|
315
357
|
def start_signal_handler
|
316
358
|
@signal_handler = Thread.new do
|
317
359
|
Thread.current.abort_on_exception = true
|
318
360
|
loop do
|
319
|
-
self.log.debug "Signal handler: waiting for new signals in the queue."
|
320
|
-
self.wait_for_signals
|
361
|
+
# self.log.debug "Signal handler: waiting for new signals in the queue."
|
362
|
+
self.wait_for_signals( IDLE_CHECK_INTERVAL )
|
363
|
+
self.check_for_idle_timeout
|
321
364
|
end
|
322
365
|
end
|
366
|
+
rescue => err
|
367
|
+
self.log.fatal "Signal handler thread crashed: %p: %s" % [ err.class, err.message ]
|
368
|
+
self.stop_immediately
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
### Check to see if the last run was more than #idle_timeout seconds ago,
|
373
|
+
### and cancelling the task's consumer if so.
|
374
|
+
def check_for_idle_timeout
|
375
|
+
|
376
|
+
# If it's unset, it means it's running now
|
377
|
+
return unless self.last_worked && self.exit_on_idle?
|
378
|
+
|
379
|
+
if (Time.now - self.last_worked) > self.class.idle_timeout
|
380
|
+
self.log.debug "Sending stop signal due to idle timeout"
|
381
|
+
self.stop_gracefully
|
382
|
+
end
|
323
383
|
end
|
324
384
|
|
325
385
|
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'set'
|
5
|
+
require 'pluggability'
|
6
|
+
|
7
|
+
require 'symphony' unless defined?( Symphony )
|
8
|
+
|
9
|
+
# A group for managing groups of tasks.
|
10
|
+
class Symphony::TaskGroup
|
11
|
+
extend Pluggability,
|
12
|
+
Loggability
|
13
|
+
|
14
|
+
|
15
|
+
# Log to the Symphony logger
|
16
|
+
log_to :symphony
|
17
|
+
|
18
|
+
# Pluggability API -- set the directory/directories that will be search when trying to
|
19
|
+
# load tasks by name.
|
20
|
+
plugin_prefixes 'symphony/task_group'
|
21
|
+
|
22
|
+
|
23
|
+
### Set up a new task group.
|
24
|
+
def initialize( task_class, max_workers )
|
25
|
+
@task_class = task_class
|
26
|
+
@max_workers = max_workers
|
27
|
+
@workers = Set.new
|
28
|
+
@last_child_started = Time.now
|
29
|
+
@throttle = 0
|
30
|
+
@queue = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
######
|
35
|
+
public
|
36
|
+
######
|
37
|
+
|
38
|
+
##
|
39
|
+
# The set of worker PIDs
|
40
|
+
attr_reader :workers
|
41
|
+
|
42
|
+
##
|
43
|
+
# The maximum number of workers the group will keep running
|
44
|
+
attr_accessor :max_workers
|
45
|
+
|
46
|
+
##
|
47
|
+
# The Class of the task the worker runs
|
48
|
+
attr_reader :task_class
|
49
|
+
|
50
|
+
##
|
51
|
+
# The Time that the last child started
|
52
|
+
attr_reader :last_child_started
|
53
|
+
|
54
|
+
|
55
|
+
### Start a new Symphony::Task and return its PID.
|
56
|
+
def start_worker( exit_on_idle=false )
|
57
|
+
self.log.debug "Starting a %p." % [ task_class ]
|
58
|
+
task_class.before_fork
|
59
|
+
|
60
|
+
pid = Process.fork do
|
61
|
+
task_class.after_fork
|
62
|
+
task_class.run( exit_on_idle )
|
63
|
+
end or raise "No PID from forked %p worker?" % [ task_class ]
|
64
|
+
|
65
|
+
Process.setpgid( pid, 0 )
|
66
|
+
|
67
|
+
self.log.info "Adding worker %p" % [ pid ]
|
68
|
+
self.workers << pid
|
69
|
+
@last_child_started = Time.now
|
70
|
+
|
71
|
+
return pid
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
### Stop a worker from the task group.
|
76
|
+
def stop_worker
|
77
|
+
pid = self.workers.first
|
78
|
+
self.signal_processes( :TERM, pid )
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
### Stop all of the task group's workers.
|
83
|
+
def stop_all_workers
|
84
|
+
self.signal_processes( :TERM, *self.workers )
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
### Send a SIGHUP to all the group's workers.
|
89
|
+
def restart_workers
|
90
|
+
self.signal_processes( :HUP, *self.workers )
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
### Scale workers up or down based on the task group's work model.
|
95
|
+
### This method needs to return an array of pids that were started, otherwise
|
96
|
+
### nil.
|
97
|
+
def adjust_workers
|
98
|
+
raise NotImplementedError, "%p needs to provide an implementation of #adjust_workers" % [
|
99
|
+
self.class
|
100
|
+
]
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
### Handle the exit of the child with the specified +pid+. The +status+ is the
|
105
|
+
### Process::Status returned by waitpid.
|
106
|
+
def on_child_exit( pid, status )
|
107
|
+
self.workers.delete( pid )
|
108
|
+
self.adjust_throttle( status.success? ? -1 : 1 )
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
### Returns +true+ if the group of tasks is throttled (i.e., should wait to start any more
|
113
|
+
### children).
|
114
|
+
def throttled?
|
115
|
+
# Return unless the throttle period has lapsed
|
116
|
+
unless self.throttle_seconds < (Time.now - self.last_child_started)
|
117
|
+
self.log.warn "Not starting children: throttled for %0.2f seconds" %
|
118
|
+
[ self.throttle_seconds ]
|
119
|
+
return true
|
120
|
+
end
|
121
|
+
|
122
|
+
return false
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
### Return the number of seconds between child startup times.
|
127
|
+
def throttle_seconds
|
128
|
+
return 0 unless @throttle.nonzero?
|
129
|
+
return Math.log( @throttle ) * Symphony.throttle_factor
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
### Add +adjustment+ to the throttle value, ensuring that it doesn't go
|
134
|
+
### below zero.
|
135
|
+
def adjust_throttle( adjustment=1 )
|
136
|
+
self.log.debug "Adjusting worker throttle by %d" % [ adjustment ]
|
137
|
+
@throttle += adjustment
|
138
|
+
@throttle = 0 if @throttle < 0
|
139
|
+
@throttle = Symphony.throttle_max if @throttle > Symphony.throttle_max
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
#########
|
144
|
+
protected
|
145
|
+
#########
|
146
|
+
|
147
|
+
### Send the specified +signal+ to the process associated with +pid+, handling
|
148
|
+
### harmless errors.
|
149
|
+
def signal_processes( signal, *pids )
|
150
|
+
self.log.debug "Signalling processes: %p" % [ pids ]
|
151
|
+
|
152
|
+
# Do them one at a time, as Process.kill will abort on the first error if you
|
153
|
+
# pass more than one pid.
|
154
|
+
pids.each do |pid|
|
155
|
+
begin
|
156
|
+
Process.kill( signal, pid )
|
157
|
+
rescue Errno::ESRCH => err
|
158
|
+
self.log.error "%p when trying to %s worker %d: %s" %
|
159
|
+
[ err.class, signal, pid, err.message ]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end # class Symphony::TaskGroup
|
165
|
+
|