heroku-resque-pool 0.0.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.
@@ -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