resque-pool-vinted 0.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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