heroku-resque-pool 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ebae8d33e9ac962bd6900f82137f77316081223b
4
+ data.tar.gz: 5d45c9065c5c0d2e9bb1a0932db1b5ecaf56ccaa
5
+ SHA512:
6
+ metadata.gz: 595892e90edfd1fe375555c6b72f027e94c207338815643d1c87972b8e51e2158aec290f134b558d480d45950788a82c0cecd7f2121a938f766d58534781cb5a
7
+ data.tar.gz: e8d5a3dd5f1fcbe54aab6a304149eb15698e0a8ef2b235cbfa874b2fee0e754ad90ecf52f723fdf435bb01e9142807f53d54d280e1602595e829ca66df022353
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2010 by Nicholas Evans <nick@ekenosen.net>, et al.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
@@ -0,0 +1,2 @@
1
+ Resque-pool is a great gem, but is no longer actively maintained. This patch is necessary if you want to take advantage of Resque's updated term signal handling. Set TERM_CHILD and RESQUE_TERM_TIMEOUT to gracefully shutdown workers on redeploy and dyno re-scale.
2
+
@@ -0,0 +1,37 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+
4
+ # for loading the example config file in config/resque-pool.yml
5
+ require 'resque/pool/tasks'
6
+
7
+ require 'rspec/core/rake_task'
8
+
9
+ desc "Run fast RSpec code examples"
10
+ RSpec::Core::RakeTask.new(:spec) do |t|
11
+ t.rspec_opts = ["-c", "-f progress", "--tag ~slow"]
12
+ end
13
+
14
+ desc "Run all RSpec code examples"
15
+ RSpec::Core::RakeTask.new("spec:ci") do |t|
16
+ t.rspec_opts = ["-c", "-f progress"]
17
+ end
18
+
19
+ require 'cucumber/rake/task'
20
+ Cucumber::Rake::Task.new(:features) do |c|
21
+ c.profile = "rake"
22
+ end
23
+
24
+ task :default => ["spec:ci", :features]
25
+
26
+ rule(/\.[1-9]$/ => [proc { |tn| "#{tn}.ronn" }]) do |t|
27
+ name = Resque::Pool.name.sub('::','-').upcase
28
+ version = "%s %s" % [name, Resque::Pool::VERSION.upcase]
29
+
30
+ manual = '--manual "%s"' % name
31
+ organization = '--organization "%s"' % version
32
+ sh "ronn #{manual} #{organization} <#{t.source} >#{t.name}"
33
+ end
34
+
35
+ file 'man/resque-pool.1'
36
+ file 'man/resque-pool.yml.5'
37
+ task :manpages => ['man/resque-pool.1','man/resque-pool.yml.5']
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
5
+
6
+ require 'resque/pool/cli'
7
+ Resque::Pool::CLI.run
@@ -0,0 +1,446 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'resque'
3
+ require 'resque/worker'
4
+ require 'resque/pool/version'
5
+ require 'resque/pool/logging'
6
+ require 'resque/pool/pooled_worker'
7
+ require 'resque/pool/file_or_hash_loader'
8
+ require 'erb'
9
+ require 'fcntl'
10
+ require 'yaml'
11
+
12
+ module Resque
13
+ class Pool
14
+ SIG_QUEUE_MAX_SIZE = 5
15
+ DEFAULT_WORKER_INTERVAL = 5
16
+ QUEUE_SIGS = [ :QUIT, :INT, :TERM, :USR1, :USR2, :CONT, :HUP, :WINCH, ]
17
+ CHUNK_SIZE = (16 * 1024)
18
+
19
+ include Logging
20
+ extend Logging
21
+ attr_reader :config
22
+ attr_reader :config_loader
23
+ attr_reader :workers
24
+
25
+ def initialize(config_loader=nil)
26
+ init_config(config_loader)
27
+ @workers = Hash.new { |workers, queues| workers[queues] = {} }
28
+ procline "(initialized)"
29
+ end
30
+
31
+ # Config: hooks {{{
32
+
33
+ def self.hook(name) # :nodoc:
34
+ class_eval <<-CODE
35
+ def self.#{name}(&block)
36
+ @#{name} ||= []
37
+ block ? (@#{name} << block) : @#{name}
38
+ end
39
+
40
+ def self.#{name}=(block)
41
+ @#{name} = [block]
42
+ end
43
+
44
+ def call_#{name}!(*args)
45
+ self.class.#{name}.each do |hook|
46
+ hook.call(*args)
47
+ end
48
+ end
49
+ CODE
50
+ end
51
+
52
+ ##
53
+ # :call-seq:
54
+ # after_prefork do |worker| ... end => add a hook
55
+ # after_prefork << hook => add a hook
56
+ #
57
+ # +after_prefork+ will run in workers before any jobs. Use these hooks e.g.
58
+ # to reload database connections to ensure that they're not shared among
59
+ # workers.
60
+ #
61
+ # :yields: worker
62
+ hook :after_prefork
63
+
64
+ ##
65
+ # :call-seq:
66
+ # after_prefork do |worker, pid, workers| ... end => add a hook
67
+ # after_prefork << hook => add a hook
68
+ #
69
+ # The `after_spawn` hooks will run in the master after spawning a new
70
+ # worker.
71
+ #
72
+ # :yields: worker, pid, workers
73
+ hook :after_spawn
74
+
75
+ # }}}
76
+ # Config: class methods to start up the pool using the config loader {{{
77
+
78
+ class << self; attr_accessor :config_loader, :app_name, :spawn_delay; end
79
+
80
+ def self.app_name
81
+ @app_name ||= File.basename(Dir.pwd)
82
+ end
83
+
84
+ def self.handle_winch?
85
+ @handle_winch ||= false
86
+ end
87
+ def self.handle_winch=(bool)
88
+ @handle_winch = bool
89
+ end
90
+
91
+ def self.kill_other_pools!
92
+ require 'resque/pool/killer'
93
+ Resque::Pool::Killer.run
94
+ end
95
+
96
+ def self.single_process_group=(bool)
97
+ ENV["RESQUE_SINGLE_PGRP"] = !!bool ? "YES" : "NO"
98
+ end
99
+ def self.single_process_group
100
+ %w[yes y true t 1 okay sure please].include?(
101
+ ENV["RESQUE_SINGLE_PGRP"].to_s.downcase
102
+ )
103
+ end
104
+
105
+ def self.run
106
+ if GC.respond_to?(:copy_on_write_friendly=)
107
+ GC.copy_on_write_friendly = true
108
+ end
109
+ create_configured.start.join
110
+ end
111
+
112
+ def self.create_configured
113
+ Resque::Pool.new(config_loader)
114
+ end
115
+
116
+ # }}}
117
+ # Config: store loader and load config {{{
118
+
119
+ def init_config(loader)
120
+ case loader
121
+ when String, Hash, nil
122
+ @config_loader = FileOrHashLoader.new(loader)
123
+ else
124
+ @config_loader = loader
125
+ end
126
+ load_config
127
+ end
128
+
129
+ def load_config
130
+ @config = config_loader.call(environment)
131
+ end
132
+
133
+ def reset_config
134
+ config_loader.reset! if config_loader.respond_to?(:reset!)
135
+ load_config
136
+ end
137
+
138
+ def environment
139
+ if defined? RAILS_ENV
140
+ RAILS_ENV
141
+ elsif defined?(Rails) && Rails.respond_to?(:env)
142
+ Rails.env
143
+ else
144
+ ENV['RACK_ENV'] || ENV['RAILS_ENV'] || ENV['RESQUE_ENV']
145
+ end
146
+ end
147
+
148
+ # }}}
149
+
150
+ # Sig handlers and self pipe management {{{
151
+
152
+ def self_pipe; @self_pipe ||= [] end
153
+ def sig_queue; @sig_queue ||= [] end
154
+ def term_child; @term_child ||= ENV['TERM_CHILD'] end
155
+
156
+
157
+ def init_self_pipe!
158
+ self_pipe.each { |io| io.close rescue nil }
159
+ self_pipe.replace(IO.pipe)
160
+ self_pipe.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
161
+ end
162
+
163
+ def init_sig_handlers!
164
+ QUEUE_SIGS.each { |sig| trap_deferred(sig) }
165
+ trap(:CHLD) { |_| awaken_master }
166
+ end
167
+
168
+ def awaken_master
169
+ begin
170
+ self_pipe.last.write_nonblock('.') # wakeup master process from select
171
+ rescue Errno::EAGAIN, Errno::EINTR
172
+ # pipe is full, master should wake up anyways
173
+ retry
174
+ end
175
+ end
176
+
177
+ class QuitNowException < Exception; end
178
+ # defer a signal for later processing in #join (master process)
179
+ def trap_deferred(signal)
180
+ trap(signal) do |sig_nr|
181
+ if @waiting_for_reaper && [:INT, :TERM].include?(signal)
182
+ log "Recieved #{signal}: short circuiting QUIT waitpid"
183
+ raise QuitNowException
184
+ end
185
+ if sig_queue.size < SIG_QUEUE_MAX_SIZE
186
+ sig_queue << signal
187
+ awaken_master
188
+ else
189
+ log "ignoring SIG#{signal}, queue=#{sig_queue.inspect}"
190
+ end
191
+ end
192
+ end
193
+
194
+ def reset_sig_handlers!
195
+ QUEUE_SIGS.each {|sig| trap(sig, "DEFAULT") }
196
+ end
197
+
198
+ def handle_sig_queue!
199
+ case signal = sig_queue.shift
200
+ when :USR1, :USR2, :CONT
201
+ log "#{signal}: sending to all workers"
202
+ signal_all_workers(signal)
203
+ when :HUP
204
+ log "HUP: reset configuration and reload logfiles"
205
+ reset_config
206
+ Logging.reopen_logs!
207
+ log "HUP: gracefully shutdown old children (which have old logfiles open)"
208
+ if term_child
209
+ signal_all_workers(:TERM)
210
+ else
211
+ signal_all_workers(:QUIT)
212
+ end
213
+ log "HUP: new children will inherit new logfiles"
214
+ maintain_worker_count
215
+ when :WINCH
216
+ if self.class.handle_winch?
217
+ log "WINCH: gracefully stopping all workers"
218
+ @config = {}
219
+ maintain_worker_count
220
+ end
221
+ when :QUIT
222
+ if term_child
223
+ shutdown_everything_now!(signal)
224
+ else
225
+ graceful_worker_shutdown_and_wait!(signal)
226
+ end
227
+ when :INT
228
+ graceful_worker_shutdown!(signal)
229
+ when :TERM
230
+ case self.class.term_behavior
231
+ when "graceful_worker_shutdown_and_wait"
232
+ graceful_worker_shutdown_and_wait!(signal)
233
+ when "graceful_worker_shutdown"
234
+ graceful_worker_shutdown!(signal)
235
+ else
236
+ shutdown_everything_now!(signal)
237
+ end
238
+ end
239
+ end
240
+
241
+ class << self
242
+ attr_accessor :term_behavior
243
+ attr_accessor :kill_other_pools
244
+ end
245
+
246
+ def graceful_worker_shutdown_and_wait!(signal)
247
+ log "#{signal}: graceful shutdown, waiting for children"
248
+ if term_child
249
+ signal_all_workers(:TERM)
250
+ else
251
+ signal_all_workers(:QUIT)
252
+ end
253
+ reap_all_workers(0) # will hang until all workers are shutdown
254
+ :break
255
+ end
256
+
257
+ def graceful_worker_shutdown!(signal)
258
+ log "#{signal}: immediate shutdown (graceful worker shutdown)"
259
+ if term_child
260
+ signal_all_workers(:TERM)
261
+ else
262
+ signal_all_workers(:QUIT)
263
+ end
264
+ :break
265
+ end
266
+
267
+ def shutdown_everything_now!(signal)
268
+ log "#{signal}: immediate shutdown (and immediate worker shutdown)"
269
+ if term_child
270
+ signal_all_workers(:QUIT)
271
+ else
272
+ signal_all_workers(:TERM)
273
+ end
274
+ :break
275
+ end
276
+
277
+ # }}}
278
+ # start, join, and master sleep {{{
279
+
280
+ def start
281
+ procline("(starting)")
282
+ init_self_pipe!
283
+ init_sig_handlers!
284
+ maintain_worker_count
285
+ procline("(started)")
286
+ log "started manager"
287
+ report_worker_pool_pids
288
+ self.class.kill_other_pools! if self.class.kill_other_pools
289
+ self
290
+ end
291
+
292
+ def report_worker_pool_pids
293
+ if workers.empty?
294
+ log "Pool is empty"
295
+ else
296
+ log "Pool contains worker PIDs: #{all_pids.inspect}"
297
+ end
298
+ end
299
+
300
+ def join
301
+ loop do
302
+ reap_all_workers
303
+ break if handle_sig_queue! == :break
304
+ if sig_queue.empty?
305
+ master_sleep
306
+ load_config
307
+ maintain_worker_count
308
+ end
309
+ procline("managing #{all_pids.inspect}")
310
+ end
311
+ procline("(shutting down)")
312
+ #stop # gracefully shutdown all workers on our way out
313
+ log "manager finished"
314
+ #unlink_pid_safe(pid) if pid
315
+ end
316
+
317
+ def master_sleep
318
+ begin
319
+ ready = IO.select([self_pipe.first], nil, nil, 1) or return
320
+ ready.first && ready.first.first or return
321
+ loop { self_pipe.first.read_nonblock(CHUNK_SIZE) }
322
+ rescue Errno::EAGAIN, Errno::EINTR
323
+ end
324
+ end
325
+
326
+ # }}}
327
+ # worker process management {{{
328
+
329
+ def reap_all_workers(waitpid_flags=Process::WNOHANG)
330
+ @waiting_for_reaper = waitpid_flags == 0
331
+ begin
332
+ loop do
333
+ # -1, wait for any child process
334
+ wpid, status = Process.waitpid2(-1, waitpid_flags)
335
+ break unless wpid
336
+
337
+ if worker = delete_worker(wpid)
338
+ log "Reaped resque worker[#{status.pid}] (status: #{status.exitstatus}) queues: #{worker.queues.join(",")}"
339
+ else
340
+ # this died before it could be killed, so it's not going to have any extra info
341
+ log "Tried to reap worker [#{status.pid}], but it had already died. (status: #{status.exitstatus})"
342
+ end
343
+ end
344
+ rescue Errno::ECHILD, QuitNowException
345
+ end
346
+ end
347
+
348
+ # TODO: close any file descriptors connected to worker, if any
349
+ def delete_worker(pid)
350
+ worker = nil
351
+ workers.detect do |queues, pid_to_worker|
352
+ worker = pid_to_worker.delete(pid)
353
+ end
354
+ worker
355
+ end
356
+
357
+ def all_pids
358
+ workers.map {|q,workers| workers.keys }.flatten
359
+ end
360
+
361
+ def signal_all_workers(signal)
362
+ log "Sending #{signal} to all workers"
363
+ all_pids.each do |pid|
364
+ Process.kill signal, pid
365
+ end
366
+ end
367
+
368
+ # }}}
369
+ # ???: maintain_worker_count, all_known_queues {{{
370
+
371
+ def maintain_worker_count
372
+ all_known_queues.each do |queues|
373
+ delta = worker_delta_for(queues)
374
+ spawn_missing_workers_for(queues) if delta > 0
375
+ quit_excess_workers_for(queues) if delta < 0
376
+ end
377
+ end
378
+
379
+ def all_known_queues
380
+ config.keys | workers.keys
381
+ end
382
+
383
+ # }}}
384
+ # methods that operate on a single grouping of queues {{{
385
+ # perhaps this means a class is waiting to be extracted
386
+
387
+ def spawn_missing_workers_for(queues)
388
+ worker_delta_for(queues).times do |nr|
389
+ spawn_worker!(queues)
390
+ sleep Resque::Pool.spawn_delay if Resque::Pool.spawn_delay
391
+ end
392
+ end
393
+
394
+ def quit_excess_workers_for(queues)
395
+ delta = -worker_delta_for(queues)
396
+ pids_for(queues)[0...delta].each do |pid|
397
+ Process.kill("QUIT", pid)
398
+ end
399
+ end
400
+
401
+ def worker_delta_for(queues)
402
+ config.fetch(queues, 0) - workers.fetch(queues, []).size
403
+ end
404
+
405
+ def pids_for(queues)
406
+ workers[queues].keys
407
+ end
408
+
409
+ def spawn_worker!(queues)
410
+ worker = create_worker(queues)
411
+ pid = fork do
412
+ Process.setpgrp unless Resque::Pool.single_process_group
413
+ worker.worker_parent_pid = Process.pid
414
+ log_worker "Starting worker #{worker}"
415
+ call_after_prefork!(worker)
416
+ reset_sig_handlers!
417
+ #self_pipe.each {|io| io.close }
418
+ worker.work(ENV['INTERVAL'] || DEFAULT_WORKER_INTERVAL) # interval, will block
419
+ end
420
+ workers[queues][pid] = worker
421
+ call_after_spawn!(worker, pid, workers)
422
+ end
423
+
424
+ def create_worker(queues)
425
+ queues = queues.to_s.split(',')
426
+ worker = ::Resque::Worker.new(*queues)
427
+ worker.pool_master_pid = Process.pid
428
+ worker.term_timeout = (ENV['RESQUE_TERM_TIMEOUT'] || 4.0).to_f
429
+ worker.term_child = ENV['TERM_CHILD']
430
+ if worker.respond_to?(:run_at_exit_hooks=)
431
+ # resque doesn't support this until 1.24, but we support 1.22
432
+ worker.run_at_exit_hooks = ENV['RUN_AT_EXIT_HOOKS'] || false
433
+ end
434
+ if ENV['LOGGING'] || ENV['VERBOSE']
435
+ worker.verbose = ENV['LOGGING'] || ENV['VERBOSE']
436
+ end
437
+ if ENV['VVERBOSE']
438
+ worker.very_verbose = ENV['VVERBOSE']
439
+ end
440
+ worker
441
+ end
442
+
443
+ # }}}
444
+
445
+ end
446
+ end