resque-pool-vinted 0.4.0.rc1

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,76 @@
1
+ require 'aruba/cucumber'
2
+ require 'aruba/api'
3
+ require 'aruba/process'
4
+
5
+ module Aruba
6
+
7
+ module Api
8
+
9
+ # this is a horrible hack, to make sure that it's done what it needs to do
10
+ # before we do our next step
11
+ def keep_trying(timeout=10, tries=0)
12
+ puts "Try: #{tries}" if @announce_env
13
+ yield
14
+ rescue RSpec::Expectations::ExpectationNotMetError
15
+ if tries < timeout
16
+ sleep 1
17
+ tries += 1
18
+ retry
19
+ else
20
+ raise
21
+ end
22
+ end
23
+
24
+ def run_background(cmd)
25
+ @background = run(cmd)
26
+ end
27
+
28
+ def send_signal(cmd, signal)
29
+ announce_or_puts "$ kill -#{signal} #{processes[cmd].pid}" if @announce_env
30
+ processes[cmd].send_signal signal
31
+ end
32
+
33
+ def background_pid
34
+ @pid_from_pidfile || @background.pid
35
+ end
36
+
37
+ # like all_stdout, but doesn't stop processes first
38
+ def interactive_stdout
39
+ only_processes.inject("") { |out, ps| out << ps.stdout(@aruba_keep_ansi) }
40
+ end
41
+
42
+ # like all_stderr, but doesn't stop processes first
43
+ def interactive_stderr
44
+ only_processes.inject("") { |out, ps| out << ps.stderr(@aruba_keep_ansi) }
45
+ end
46
+
47
+ # like all_output, but doesn't stop processes first
48
+ def interactive_output
49
+ interactive_stdout << interactive_stderr
50
+ end
51
+
52
+ def interpolate_background_pid(string)
53
+ interpolated = string.gsub('$PID', background_pid.to_s)
54
+ announce_or_puts interpolated if @announce_env
55
+ interpolated
56
+ end
57
+
58
+ def kill_all_processes!
59
+ # stop_processes!
60
+ #rescue
61
+ # processes.each {|cmd,process| send_signal(cmd, 'KILL') }
62
+ # raise
63
+ end
64
+
65
+ end
66
+
67
+ class Process
68
+ def pid
69
+ @process.pid
70
+ end
71
+ def send_signal signal
72
+ @process.send :send_signal, signal
73
+ end
74
+ end
75
+
76
+ end
@@ -0,0 +1 @@
1
+ ENV["RAILS_ENV"] = "test"
@@ -0,0 +1,426 @@
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 'erb'
8
+ require 'fcntl'
9
+ require 'yaml'
10
+
11
+ module Resque
12
+ class Pool
13
+ SIG_QUEUE_MAX_SIZE = 5
14
+ DEFAULT_WORKER_INTERVAL = 5
15
+ QUEUE_SIGS = [ :QUIT, :INT, :TERM, :USR1, :USR2, :CONT, :HUP, :WINCH, ]
16
+ CHUNK_SIZE = (16 * 1024)
17
+
18
+ include Logging
19
+ extend Logging
20
+ attr_reader :config
21
+ attr_reader :workers
22
+
23
+ def initialize(config)
24
+ init_config(config)
25
+ @workers = Hash.new { |workers, queues| workers[queues] = {} }
26
+ procline "(initialized)"
27
+ end
28
+
29
+ # Config: after_prefork {{{
30
+
31
+ # The `after_prefork` hooks will be run in workers if you are using the
32
+ # preforking master worker to save memory. Use these hooks to reload
33
+ # database connections and so forth to ensure that they're not shared
34
+ # among workers.
35
+ #
36
+ # Call with a block to set a hook.
37
+ # Call with no arguments to return all registered hooks.
38
+ #
39
+ def self.after_prefork(&block)
40
+ @after_prefork ||= []
41
+ block ? (@after_prefork << block) : @after_prefork
42
+ end
43
+
44
+ # Sets the after_prefork proc, clearing all pre-existing hooks.
45
+ # Warning: you probably don't want to clear out the other hooks.
46
+ # You can use `Resque::Pool.after_prefork << my_hook` instead.
47
+ #
48
+ def self.after_prefork=(after_prefork)
49
+ @after_prefork = [after_prefork]
50
+ end
51
+
52
+ def call_after_prefork!
53
+ self.class.after_prefork.each do |hook|
54
+ hook.call
55
+ end
56
+ end
57
+
58
+ # }}}
59
+ # Config: class methods to start up the pool using the default config {{{
60
+
61
+ @config_files = ["resque-pool.yml", "config/resque-pool.yml"]
62
+ class << self; attr_accessor :config_files, :app_name; end
63
+
64
+ def self.app_name
65
+ @app_name ||= File.basename(Dir.pwd)
66
+ end
67
+
68
+ def self.handle_winch?
69
+ @handle_winch ||= false
70
+ end
71
+ def self.handle_winch=(bool)
72
+ @handle_winch = bool
73
+ end
74
+
75
+ def self.single_process_group=(bool)
76
+ ENV["RESQUE_SINGLE_PGRP"] = !!bool ? "YES" : "NO"
77
+ end
78
+ def self.single_process_group
79
+ %w[yes y true t 1 okay sure please].include?(
80
+ ENV["RESQUE_SINGLE_PGRP"].to_s.downcase
81
+ )
82
+ end
83
+
84
+ def self.choose_config_file
85
+ if ENV["RESQUE_POOL_CONFIG"]
86
+ ENV["RESQUE_POOL_CONFIG"]
87
+ else
88
+ @config_files.detect { |f| File.exist?(f) }
89
+ end
90
+ end
91
+
92
+ def self.run
93
+ if GC.respond_to?(:copy_on_write_friendly=)
94
+ GC.copy_on_write_friendly = true
95
+ end
96
+ Resque::Pool.new(choose_config_file).start.join
97
+ end
98
+
99
+ # }}}
100
+ # Config: load config and config file {{{
101
+
102
+ def config_file
103
+ @config_file || (!@config && ::Resque::Pool.choose_config_file)
104
+ end
105
+
106
+ def init_config(config)
107
+ case config
108
+ when String, nil
109
+ @config_file = config
110
+ else
111
+ @config = config.dup
112
+ end
113
+ load_config
114
+ end
115
+
116
+ def load_config
117
+ if config_file
118
+ @config = YAML.load(ERB.new(IO.read(config_file)).result)
119
+ else
120
+ @config ||= {}
121
+ end
122
+ environment and @config[environment] and config.merge!(@config[environment])
123
+ config.delete_if {|key, value| value.is_a? Hash }
124
+ end
125
+
126
+ def environment
127
+ if defined? RAILS_ENV
128
+ RAILS_ENV
129
+ elsif defined?(Rails) && Rails.respond_to?(:env)
130
+ Rails.env
131
+ else
132
+ ENV['RACK_ENV'] || ENV['RAILS_ENV'] || ENV['RESQUE_ENV']
133
+ end
134
+ end
135
+
136
+ # }}}
137
+
138
+ # Sig handlers and self pipe management {{{
139
+
140
+ def self_pipe; @self_pipe ||= [] end
141
+ def sig_queue; @sig_queue ||= [] end
142
+ def term_child; @term_child ||= ENV['TERM_CHILD'] end
143
+
144
+
145
+ def init_self_pipe!
146
+ self_pipe.each { |io| io.close rescue nil }
147
+ self_pipe.replace(IO.pipe)
148
+ self_pipe.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
149
+ end
150
+
151
+ def init_sig_handlers!
152
+ QUEUE_SIGS.each { |sig| trap_deferred(sig) }
153
+ trap(:CHLD) { |_| awaken_master }
154
+ end
155
+
156
+ def awaken_master
157
+ begin
158
+ self_pipe.last.write_nonblock('.') # wakeup master process from select
159
+ rescue Errno::EAGAIN, Errno::EINTR
160
+ # pipe is full, master should wake up anyways
161
+ retry
162
+ end
163
+ end
164
+
165
+ class QuitNowException < Exception; end
166
+ # defer a signal for later processing in #join (master process)
167
+ def trap_deferred(signal)
168
+ trap(signal) do |sig_nr|
169
+ if @waiting_for_reaper && [:INT, :TERM].include?(signal)
170
+ log "Recieved #{signal}: short circuiting QUIT waitpid"
171
+ raise QuitNowException
172
+ end
173
+ if sig_queue.size < SIG_QUEUE_MAX_SIZE
174
+ sig_queue << signal
175
+ awaken_master
176
+ else
177
+ log "ignoring SIG#{signal}, queue=#{sig_queue.inspect}"
178
+ end
179
+ end
180
+ end
181
+
182
+ def reset_sig_handlers!
183
+ QUEUE_SIGS.each {|sig| trap(sig, "DEFAULT") }
184
+ end
185
+
186
+ def handle_sig_queue!
187
+ case signal = sig_queue.shift
188
+ when :USR1, :USR2, :CONT
189
+ log "#{signal}: sending to all workers"
190
+ signal_all_workers(signal)
191
+ when :HUP
192
+ log "HUP: reload config file and reload logfiles"
193
+ load_config
194
+ Logging.reopen_logs!
195
+ log "HUP: gracefully shutdown old children (which have old logfiles open)"
196
+ if term_child
197
+ signal_all_workers(:TERM)
198
+ else
199
+ signal_all_workers(:QUIT)
200
+ end
201
+ log "HUP: new children will inherit new logfiles"
202
+ maintain_worker_count
203
+ when :WINCH
204
+ if self.class.handle_winch?
205
+ log "WINCH: gracefully stopping all workers"
206
+ @config = {}
207
+ maintain_worker_count
208
+ end
209
+ when :QUIT
210
+ if term_child
211
+ shutdown_everything_now!(signal)
212
+ else
213
+ graceful_worker_shutdown_and_wait!(signal)
214
+ end
215
+ when :INT
216
+ graceful_worker_shutdown!(signal)
217
+ when :TERM
218
+ if term_child
219
+ graceful_worker_shutdown!(signal)
220
+ else
221
+ case self.class.term_behavior
222
+ when "graceful_worker_shutdown_and_wait"
223
+ graceful_worker_shutdown_and_wait!(signal)
224
+ when "graceful_worker_shutdown"
225
+ graceful_worker_shutdown!(signal)
226
+ else
227
+ shutdown_everything_now!(signal)
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ class << self
234
+ attr_accessor :term_behavior
235
+ end
236
+
237
+ def graceful_worker_shutdown_and_wait!(signal)
238
+ log "#{signal}: graceful shutdown, waiting for children"
239
+ if term_child
240
+ signal_all_workers(:TERM)
241
+ else
242
+ signal_all_workers(:QUIT)
243
+ end
244
+ reap_all_workers(0) # will hang until all workers are shutdown
245
+ :break
246
+ end
247
+
248
+ def graceful_worker_shutdown!(signal)
249
+ log "#{signal}: immediate shutdown (graceful worker shutdown)"
250
+ if term_child
251
+ signal_all_workers(:TERM)
252
+ else
253
+ signal_all_workers(:QUIT)
254
+ end
255
+ :break
256
+ end
257
+
258
+ def shutdown_everything_now!(signal)
259
+ log "#{signal}: immediate shutdown (and immediate worker shutdown)"
260
+ if term_child
261
+ signal_all_workers(:QUIT)
262
+ else
263
+ signal_all_workers(:TERM)
264
+ end
265
+ :break
266
+ end
267
+
268
+ # }}}
269
+ # start, join, and master sleep {{{
270
+
271
+ def start
272
+ procline("(starting)")
273
+ init_self_pipe!
274
+ init_sig_handlers!
275
+ maintain_worker_count
276
+ procline("(started)")
277
+ log "started manager"
278
+ report_worker_pool_pids
279
+ self
280
+ end
281
+
282
+ def report_worker_pool_pids
283
+ if workers.empty?
284
+ log "Pool is empty"
285
+ else
286
+ log "Pool contains worker PIDs: #{all_pids.inspect}"
287
+ end
288
+ end
289
+
290
+ def join
291
+ loop do
292
+ reap_all_workers
293
+ break if handle_sig_queue! == :break
294
+ if sig_queue.empty?
295
+ master_sleep
296
+ maintain_worker_count
297
+ end
298
+ procline("managing #{all_pids.inspect}")
299
+ end
300
+ procline("(shutting down)")
301
+ #stop # gracefully shutdown all workers on our way out
302
+ log "manager finished"
303
+ #unlink_pid_safe(pid) if pid
304
+ end
305
+
306
+ def master_sleep
307
+ begin
308
+ ready = IO.select([self_pipe.first], nil, nil, 1) or return
309
+ ready.first && ready.first.first or return
310
+ loop { self_pipe.first.read_nonblock(CHUNK_SIZE) }
311
+ rescue Errno::EAGAIN, Errno::EINTR
312
+ end
313
+ end
314
+
315
+ # }}}
316
+ # worker process management {{{
317
+
318
+ def reap_all_workers(waitpid_flags=Process::WNOHANG)
319
+ @waiting_for_reaper = waitpid_flags == 0
320
+ begin
321
+ loop do
322
+ # -1, wait for any child process
323
+ wpid, status = Process.waitpid2(-1, waitpid_flags)
324
+ break unless wpid
325
+
326
+ if worker = delete_worker(wpid)
327
+ log "Reaped resque worker[#{status.pid}] (status: #{status.exitstatus}) queues: #{worker.queues.join(",")}"
328
+ else
329
+ # this died before it could be killed, so it's not going to have any extra info
330
+ log "Tried to reap worker [#{status.pid}], but it had already died. (status: #{status.exitstatus})"
331
+ end
332
+ end
333
+ rescue Errno::ECHILD, QuitNowException
334
+ end
335
+ end
336
+
337
+ # TODO: close any file descriptors connected to worker, if any
338
+ def delete_worker(pid)
339
+ worker = nil
340
+ workers.detect do |queues, pid_to_worker|
341
+ worker = pid_to_worker.delete(pid)
342
+ end
343
+ worker
344
+ end
345
+
346
+ def all_pids
347
+ workers.map {|q,workers| workers.keys }.flatten
348
+ end
349
+
350
+ def signal_all_workers(signal)
351
+ all_pids.each do |pid|
352
+ Process.kill signal, pid
353
+ end
354
+ end
355
+
356
+ # }}}
357
+ # ???: maintain_worker_count, all_known_queues {{{
358
+
359
+ def maintain_worker_count
360
+ all_known_queues.each do |queues|
361
+ delta = worker_delta_for(queues)
362
+ spawn_missing_workers_for(queues) if delta > 0
363
+ quit_excess_workers_for(queues) if delta < 0
364
+ end
365
+ end
366
+
367
+ def all_known_queues
368
+ config.keys | workers.keys
369
+ end
370
+
371
+ # }}}
372
+ # methods that operate on a single grouping of queues {{{
373
+ # perhaps this means a class is waiting to be extracted
374
+
375
+ def spawn_missing_workers_for(queues)
376
+ worker_delta_for(queues).times do |nr|
377
+ spawn_worker!(queues)
378
+ end
379
+ end
380
+
381
+ def quit_excess_workers_for(queues)
382
+ delta = -worker_delta_for(queues)
383
+ pids_for(queues)[0...delta].each do |pid|
384
+ Process.kill("QUIT", pid)
385
+ end
386
+ end
387
+
388
+ def worker_delta_for(queues)
389
+ config.fetch(queues, 0) - workers.fetch(queues, []).size
390
+ end
391
+
392
+ def pids_for(queues)
393
+ workers[queues].keys
394
+ end
395
+
396
+ def spawn_worker!(queues)
397
+ worker = create_worker(queues)
398
+ pid = fork do
399
+ Process.setpgrp unless Resque::Pool.single_process_group
400
+ log_worker "Starting worker #{worker}"
401
+ call_after_prefork!
402
+ reset_sig_handlers!
403
+ #self_pipe.each {|io| io.close }
404
+ worker.work(ENV['INTERVAL'] || DEFAULT_WORKER_INTERVAL) # interval, will block
405
+ end
406
+ workers[queues][pid] = worker
407
+ end
408
+
409
+ def create_worker(queues)
410
+ queues = queues.to_s.split(',')
411
+ worker = ::Resque::Worker.new(*queues)
412
+ worker.term_timeout = ENV['RESQUE_TERM_TIMEOUT'] || 4.0
413
+ worker.term_child = ENV['TERM_CHILD']
414
+ if ENV['LOGGING'] || ENV['VERBOSE']
415
+ worker.verbose = ENV['LOGGING'] || ENV['VERBOSE']
416
+ end
417
+ if ENV['VVERBOSE']
418
+ worker.very_verbose = ENV['VVERBOSE']
419
+ end
420
+ worker
421
+ end
422
+
423
+ # }}}
424
+
425
+ end
426
+ end