delayed_job_worker_pool 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f7e84e64a69a710244b682df783ea1f7f96ce72a
4
- data.tar.gz: 75e5bbb9ffe986ded38517b4bb0fb72e7e5049c0
3
+ metadata.gz: b57aa82422ba29a08e43da4d5f3b46e5841d2ea0
4
+ data.tar.gz: 342039b94feef2d751f62470642ce7b584b6f7e2
5
5
  SHA512:
6
- metadata.gz: 071b9234bfd7e3904bc44eb0957508a31bdd9975d644d7de77d06e3f9b0e8af5ccb0ca185c4bd9e754fde55c52698e4add16ebf873964d31d1af3101cb52d163
7
- data.tar.gz: d98c55b40a03b8156d96ebfb870a56e75472101f61806dbab4b52300a6082808e225a4d4a738a689e53d0175d73688a7905e46974c9846080d05358176ca374a
6
+ metadata.gz: cfed1c0e3a15bed2524aebad47ff861a1130617397aa64ae4fa533d106926c1f3dd0f02a3654576a97d06430c5909ec612d8ea4151d312872bda5b8be706ad40
7
+ data.tar.gz: 9cd73cb2cc25a036c14559f3e1bb61e76262d09346fe1f2713c6a05b656fe95db6e3064bad9eaecdaa4e110b57119c1a71e6b36885f81f1710966a3aa0a53718
@@ -1,4 +1,4 @@
1
1
  # Changelog
2
2
 
3
- ### 0.2.0 (unreleased)
4
- *
3
+ ### 0.2.1 (unreleased)
4
+ * Fix race condition in signal handler
data/README.md CHANGED
@@ -8,9 +8,11 @@
8
8
  [travis]: http://travis-ci.org/salsify/delayed_job_worker_pool
9
9
  [codeclimate]: https://codeclimate.com/github/salsify/delayed_job_worker_pool
10
10
 
11
- [Delayed Job's](https://github.com/collectiveidea/delayed_job) built-in worker pooling daemonizes all worker processes. This is great for certain environments but not so great for environments like Heroku that really want your processes to run in the foreground. Delayed Job Worker Pool runs a pool of Delayed Job workers **without** daemonizing them. [Salsify](http://salsify.com) is currently using this to run multiple Delayed Job workers on a single Heroku PX dyno.
11
+ [Delayed Job's](https://github.com/collectiveidea/delayed_job) built-in worker pooling daemonizes all worker processes. This is great for certain environments but not so great for environments like Heroku that really want your processes to run in the foreground. Delayed Job Worker Pool runs a pool of Delayed Job workers **without** daemonizing them.
12
+
13
+ [Salsify](http://salsify.com) is currently using Delayed Job Worker Pool to run multiple Delayed Job workers on a single Heroku PX dyno. Read more about our experience using this gem on our [blog](http://blog.salsify.com/engineering/delayed-job-worker-pooling).
12
14
 
13
- This gem only works with MRI on Linux/MacOS X.
15
+ **This gem only works with MRI on Linux/MacOS X.**
14
16
 
15
17
  ## Installation
16
18
 
@@ -1,3 +1,3 @@
1
1
  module DelayedJobWorkerPool
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.2.1'.freeze
3
3
  end
@@ -1,8 +1,13 @@
1
1
  module DelayedJobWorkerPool
2
2
  class WorkerPool
3
+ SIGNALS = ['TERM', 'INT'].map(&:freeze).freeze
4
+
3
5
  def initialize(options = {})
4
6
  @options = options
5
7
  @worker_pids = []
8
+ @pending_signals = []
9
+ @pending_signal_read_pipe, @pending_signal_write_pipe = create_pipe(inheritable: false)
10
+ @master_alive_read_pipe, @master_alive_write_pipe = create_pipe(inheritable: true)
6
11
  self.shutting_down = false
7
12
  end
8
13
 
@@ -18,7 +23,6 @@ module DelayedJobWorkerPool
18
23
 
19
24
  log_uninheritable_threads
20
25
 
21
- create_master_alive_pipe
22
26
  num_workers.times { fork_worker }
23
27
 
24
28
  monitor_workers
@@ -31,16 +35,22 @@ module DelayedJobWorkerPool
31
35
 
32
36
  private
33
37
 
34
- attr_reader :options, :worker_pids, :master_alive_read_pipe, :master_alive_write_pipe
38
+ attr_reader :options, :worker_pids, :master_alive_read_pipe, :master_alive_write_pipe,
39
+ :pending_signals, :pending_signal_read_pipe, :pending_signal_write_pipe
35
40
  attr_accessor :shutting_down
36
41
 
37
42
  def install_signal_handlers
38
- trap('TERM') do
39
- shutdown('TERM')
43
+ SIGNALS.each do |signal|
44
+ trap(signal) do
45
+ pending_signals << signal
46
+ pending_signal_write_pipe.write_nonblock('.')
47
+ end
40
48
  end
49
+ end
41
50
 
42
- trap('INT') do
43
- shutdown('INT')
51
+ def uninstall_signal_handlers
52
+ SIGNALS.each do |signal|
53
+ trap(signal, 'DEFAULT')
44
54
  end
45
55
  end
46
56
 
@@ -55,10 +65,6 @@ module DelayedJobWorkerPool
55
65
  end
56
66
  end
57
67
 
58
- def create_master_alive_pipe
59
- @master_alive_read_pipe, @master_alive_write_pipe = IO.pipe
60
- end
61
-
62
68
  def load_app
63
69
  DelayedJobWorkerPool::Application.load
64
70
  end
@@ -73,16 +79,32 @@ module DelayedJobWorkerPool
73
79
  end
74
80
 
75
81
  def monitor_workers
76
- until worker_pids.empty?
77
- worker_pid, status = Process.wait2
82
+ while has_workers?
83
+ if has_pending_signal?
84
+ shutdown(pending_signals.pop)
85
+ elsif (wait_result = Process.wait2(-1, Process::WNOHANG))
86
+ handle_dead_worker(wait_result.first, wait_result.last)
87
+ else
88
+ wait_for_signal(1)
89
+ end
90
+ end
91
+ end
78
92
 
79
- next unless worker_pids.include?(worker_pid)
93
+ def handle_dead_worker(worker_pid, status)
94
+ return unless worker_pids.include?(worker_pid)
80
95
 
81
- log("Worker #{worker_pid} exited with status #{status.to_i}")
82
- worker_pids.delete(worker_pid)
83
- invoke_callback(:after_worker_shutdown, worker_info(worker_pid))
84
- fork_worker unless shutting_down
85
- end
96
+ log("Worker #{worker_pid} exited with status #{status.to_i}")
97
+ worker_pids.delete(worker_pid)
98
+ invoke_callback(:after_worker_shutdown, worker_info(worker_pid))
99
+ fork_worker unless shutting_down
100
+ end
101
+
102
+ def has_workers?
103
+ !worker_pids.empty?
104
+ end
105
+
106
+ def has_pending_signal?
107
+ !pending_signals.empty?
86
108
  end
87
109
 
88
110
  def invoke_callback(callback_name, *args)
@@ -99,6 +121,8 @@ module DelayedJobWorkerPool
99
121
  def run_worker
100
122
  master_alive_write_pipe.close
101
123
 
124
+ uninstall_signal_handlers
125
+
102
126
  Thread.new do
103
127
  IO.select([master_alive_read_pipe])
104
128
  log('Detected dead master. Shutting down worker.')
@@ -135,6 +159,31 @@ module DelayedJobWorkerPool
135
159
  options.except(:workers, :preload_app, *DelayedJobWorkerPool::DSL::CALLBACK_SETTINGS).merge(name: worker_name(worker_pid))
136
160
  end
137
161
 
162
+ def create_pipe(inheritable: true)
163
+ read, write = IO.pipe
164
+ unless inheritable
165
+ make_file_descriptor_uninheritable(read)
166
+ make_file_descriptor_uninheritable(write)
167
+ end
168
+ [read, write]
169
+ end
170
+
171
+ def make_file_descriptor_uninheritable(io)
172
+ io.fcntl(Fcntl::F_SETFD)
173
+ end
174
+
175
+ def wait_for_signal(timeout)
176
+ if IO.select([pending_signal_read_pipe], [], [], timeout)
177
+ drain_pipe(pending_signal_read_pipe)
178
+ end
179
+ end
180
+
181
+ def drain_pipe(pipe)
182
+ loop { pipe.read_nonblock(16) }
183
+ rescue IO::WaitReadable
184
+ # We've drained the pipe
185
+ end
186
+
138
187
  def log(message)
139
188
  puts(message)
140
189
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: delayed_job_worker_pool
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Turkel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-19 00:00:00.000000000 Z
11
+ date: 2015-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: delayed_job