postburner 0.8.0 → 1.0.0.pre.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +0 -22
- data/README.md +1219 -238
- data/Rakefile +1 -1
- data/app/concerns/postburner/callbacks.rb +286 -0
- data/app/models/postburner/job.rb +735 -46
- data/app/models/postburner/tracked_job.rb +83 -0
- data/bin/postburner +91 -0
- data/bin/rails +14 -0
- data/config/environment.rb +3 -0
- data/config/postburner.yml +22 -0
- data/config/postburner.yml.example +142 -0
- data/lib/postburner/active_job/adapter.rb +163 -0
- data/lib/postburner/active_job/execution.rb +109 -0
- data/lib/postburner/active_job/payload.rb +157 -0
- data/lib/postburner/beanstalkd.rb +97 -0
- data/lib/postburner/configuration.rb +202 -0
- data/lib/postburner/connection.rb +113 -0
- data/lib/postburner/engine.rb +1 -1
- data/lib/postburner/queue_config.rb +151 -0
- data/lib/postburner/strategies/immediate_test_queue.rb +133 -0
- data/lib/postburner/strategies/nice_queue.rb +85 -0
- data/lib/postburner/strategies/null_queue.rb +132 -0
- data/lib/postburner/strategies/queue.rb +119 -0
- data/lib/postburner/strategies/test_queue.rb +128 -0
- data/lib/postburner/time_helpers.rb +75 -0
- data/lib/postburner/tracked.rb +171 -0
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner/workers/base.rb +210 -0
- data/lib/postburner/workers/worker.rb +480 -0
- data/lib/postburner.rb +433 -4
- metadata +66 -17
- data/lib/postburner/tube.rb +0 -53
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
|
|
5
|
+
module Postburner
|
|
6
|
+
module Workers
|
|
7
|
+
# Puma-style worker with configurable forks and threads per queue.
|
|
8
|
+
#
|
|
9
|
+
# This is the universal Postburner worker that scales from development
|
|
10
|
+
# to production using forks and threads configuration. Just like Puma:
|
|
11
|
+
# - **0 forks** = Single process with thread pool
|
|
12
|
+
# - **1+ forks** = Multiple processes (forks) with thread pools
|
|
13
|
+
#
|
|
14
|
+
# ## Architecture
|
|
15
|
+
#
|
|
16
|
+
# ### Single Process Mode (forks: 0)
|
|
17
|
+
# ```
|
|
18
|
+
# Main Process
|
|
19
|
+
# ├─ Queue 'default' Thread Pool (10 threads)
|
|
20
|
+
# ├─ Queue 'critical' Thread Pool (1 thread)
|
|
21
|
+
# └─ Queue 'mailers' Thread Pool (5 threads)
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# ### Multi-Process Mode (forks: 1+)
|
|
25
|
+
# ```
|
|
26
|
+
# Parent Process
|
|
27
|
+
# ├─ Fork 0 (queue: critical)
|
|
28
|
+
# │ └─ Thread 1
|
|
29
|
+
# ├─ Fork 0 (queue: default)
|
|
30
|
+
# │ ├─ Thread 1-10
|
|
31
|
+
# ├─ Fork 1 (queue: default) # Puma-style: multiple forks of same queue
|
|
32
|
+
# │ ├─ Thread 1-10
|
|
33
|
+
# ├─ Fork 2 (queue: default)
|
|
34
|
+
# │ └─ Thread 1-10
|
|
35
|
+
# ├─ Fork 3 (queue: default)
|
|
36
|
+
# │ └─ Thread 1-10
|
|
37
|
+
# │ └─ Total: 40 concurrent jobs for 'default' (4 forks × 10 threads)
|
|
38
|
+
# └─ Fork 0 (queue: mailers)
|
|
39
|
+
# └─ Thread 1-5
|
|
40
|
+
# ```
|
|
41
|
+
#
|
|
42
|
+
# ## Scaling Strategy
|
|
43
|
+
#
|
|
44
|
+
# **Development:**
|
|
45
|
+
# ```yaml
|
|
46
|
+
# default_forks: 0
|
|
47
|
+
# default_threads: 1
|
|
48
|
+
# ```
|
|
49
|
+
# Single-threaded, single-process (simplest debugging)
|
|
50
|
+
#
|
|
51
|
+
# **Staging:**
|
|
52
|
+
# ```yaml
|
|
53
|
+
# default_forks: 0
|
|
54
|
+
# default_threads: 10
|
|
55
|
+
# ```
|
|
56
|
+
# Multi-threaded, single-process (moderate concurrency)
|
|
57
|
+
#
|
|
58
|
+
# **Production:**
|
|
59
|
+
# ```yaml
|
|
60
|
+
# default_forks: 4
|
|
61
|
+
# default_threads: 10
|
|
62
|
+
# ```
|
|
63
|
+
# 4 processes × 10 threads = 40 concurrent jobs per queue
|
|
64
|
+
#
|
|
65
|
+
# ## Configuration
|
|
66
|
+
#
|
|
67
|
+
# @example Development (single-threaded)
|
|
68
|
+
# development:
|
|
69
|
+
# default_forks: 0
|
|
70
|
+
# default_threads: 1
|
|
71
|
+
# queues:
|
|
72
|
+
# default: {}
|
|
73
|
+
# mailers: {}
|
|
74
|
+
#
|
|
75
|
+
# @example Staging (multi-threaded, single process)
|
|
76
|
+
# staging:
|
|
77
|
+
# default_forks: 0
|
|
78
|
+
# default_threads: 10
|
|
79
|
+
# default_gc_limit: 5000
|
|
80
|
+
# queues:
|
|
81
|
+
# critical:
|
|
82
|
+
# threads: 1 # Single-threaded for critical jobs
|
|
83
|
+
# default:
|
|
84
|
+
# threads: 10 # 10 concurrent threads
|
|
85
|
+
# mailers:
|
|
86
|
+
# threads: 5 # 5 concurrent emails
|
|
87
|
+
#
|
|
88
|
+
# @example Production (Puma-style: forks × threads)
|
|
89
|
+
# production:
|
|
90
|
+
# default_forks: 2
|
|
91
|
+
# default_threads: 10
|
|
92
|
+
# default_gc_limit: 5000
|
|
93
|
+
# queues:
|
|
94
|
+
# critical:
|
|
95
|
+
# forks: 1 # Single fork
|
|
96
|
+
# threads: 1 # Single thread = 1 concurrent job
|
|
97
|
+
# default:
|
|
98
|
+
# forks: 4 # 4 forks (Puma-style)
|
|
99
|
+
# threads: 10 # 10 threads per fork = 40 total concurrent jobs
|
|
100
|
+
# mailers:
|
|
101
|
+
# forks: 2
|
|
102
|
+
# threads: 5 # 2×5 = 10 total email senders
|
|
103
|
+
#
|
|
104
|
+
class Worker < Base
|
|
105
|
+
# Starts the worker.
|
|
106
|
+
#
|
|
107
|
+
# Detects whether to run in single-process mode (forks: 0) or
|
|
108
|
+
# multi-process mode (forks: 1+) and starts accordingly.
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
#
|
|
112
|
+
def start
|
|
113
|
+
logger.info "[Postburner::Worker] Starting..."
|
|
114
|
+
logger.info "[Postburner::Worker] Queues: #{config.queue_names.join(', ')}"
|
|
115
|
+
logger.info "[Postburner] #{config.beanstalk_url} watching tubes: #{config.expanded_tube_names.join(', ')}"
|
|
116
|
+
|
|
117
|
+
# Detect mode based on fork configuration
|
|
118
|
+
if using_forks?
|
|
119
|
+
start_forked_mode
|
|
120
|
+
else
|
|
121
|
+
start_single_process_mode
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Checks if any queue is configured to use forks.
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] true if any queue has forks > 0
|
|
130
|
+
#
|
|
131
|
+
def using_forks?
|
|
132
|
+
config.queue_names.any? do |queue_name|
|
|
133
|
+
queue_config = config.queue_config(queue_name)
|
|
134
|
+
fork_count = queue_config['forks'] || queue_config[:forks] || config.default_forks
|
|
135
|
+
fork_count > 0
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Starts worker in single-process mode (forks: 0).
|
|
140
|
+
#
|
|
141
|
+
# Creates thread pools for each queue in the main process.
|
|
142
|
+
# Suitable for development and moderate concurrency needs.
|
|
143
|
+
#
|
|
144
|
+
# @return [void]
|
|
145
|
+
#
|
|
146
|
+
def start_single_process_mode
|
|
147
|
+
logger.info "[Postburner::Worker] Mode: Single process (forks: 0)"
|
|
148
|
+
|
|
149
|
+
# Track total jobs processed across all threads
|
|
150
|
+
@jobs_processed = Concurrent::AtomicFixnum.new(0)
|
|
151
|
+
@gc_limit = config.default_gc_limit
|
|
152
|
+
|
|
153
|
+
# Create thread pools for each queue
|
|
154
|
+
@pools = {}
|
|
155
|
+
config.queue_names.each do |queue_name|
|
|
156
|
+
spawn_queue_threads(queue_name)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Monitor for shutdown or GC limit
|
|
160
|
+
until shutdown? || (@gc_limit && @jobs_processed.value >= @gc_limit)
|
|
161
|
+
sleep 0.5
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Shutdown pools gracefully
|
|
165
|
+
logger.info "[Postburner::Worker] Shutting down..."
|
|
166
|
+
shutdown_pools
|
|
167
|
+
|
|
168
|
+
if @gc_limit && @jobs_processed.value >= @gc_limit
|
|
169
|
+
logger.info "[Postburner::Worker] Reached GC limit (#{@jobs_processed.value} jobs), exiting for restart..."
|
|
170
|
+
exit 99 # Special exit code for GC restart
|
|
171
|
+
else
|
|
172
|
+
logger.info "[Postburner::Worker] Shutdown complete"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Spawns thread pool for a specific queue in single-process mode.
|
|
177
|
+
#
|
|
178
|
+
# @param queue_name [String] Name of the queue to process
|
|
179
|
+
#
|
|
180
|
+
# @return [void]
|
|
181
|
+
#
|
|
182
|
+
def spawn_queue_threads(queue_name)
|
|
183
|
+
queue_config = config.queue_config(queue_name)
|
|
184
|
+
thread_count = queue_config['threads'] || queue_config[:threads] || config.default_threads
|
|
185
|
+
|
|
186
|
+
logger.info "[Postburner::Worker] Queue '#{queue_name}': #{thread_count} threads"
|
|
187
|
+
|
|
188
|
+
# Create thread pool
|
|
189
|
+
pool = Concurrent::FixedThreadPool.new(thread_count)
|
|
190
|
+
@pools[queue_name] = pool
|
|
191
|
+
|
|
192
|
+
# Spawn worker threads
|
|
193
|
+
thread_count.times do
|
|
194
|
+
pool.post do
|
|
195
|
+
process_jobs_in_single_process(queue_name)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Processes jobs in a single thread (single-process mode).
|
|
201
|
+
#
|
|
202
|
+
# Each thread has its own Beanstalkd connection and reserves jobs
|
|
203
|
+
# from the specified queue.
|
|
204
|
+
#
|
|
205
|
+
# @param queue_name [String] Name of the queue to process
|
|
206
|
+
#
|
|
207
|
+
# @return [void]
|
|
208
|
+
#
|
|
209
|
+
def process_jobs_in_single_process(queue_name)
|
|
210
|
+
connection = Postburner::Connection.new
|
|
211
|
+
|
|
212
|
+
# Watch only this queue
|
|
213
|
+
watch_queues(connection, queue_name)
|
|
214
|
+
|
|
215
|
+
until shutdown? || (@gc_limit && @jobs_processed.value >= @gc_limit)
|
|
216
|
+
begin
|
|
217
|
+
# Reserve with short timeout
|
|
218
|
+
job = connection.beanstalk.tubes.reserve(timeout: 1)
|
|
219
|
+
|
|
220
|
+
if job
|
|
221
|
+
logger.debug "[Postburner::Worker] Thread #{Thread.current.object_id} reserved job #{job.id}"
|
|
222
|
+
execute_job(job)
|
|
223
|
+
@jobs_processed.increment
|
|
224
|
+
end
|
|
225
|
+
rescue Beaneater::TimedOutError
|
|
226
|
+
# Normal timeout, continue
|
|
227
|
+
next
|
|
228
|
+
rescue Beaneater::NotConnected => e
|
|
229
|
+
logger.error "[Postburner::Worker] Thread disconnected: #{e.message}"
|
|
230
|
+
sleep 1
|
|
231
|
+
connection.reconnect!
|
|
232
|
+
rescue => e
|
|
233
|
+
logger.error "[Postburner::Worker] Thread error: #{e.class} - #{e.message}"
|
|
234
|
+
logger.error e.backtrace.join("\n")
|
|
235
|
+
sleep 1
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
ensure
|
|
239
|
+
connection&.close rescue nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Gracefully shuts down all thread pools (single-process mode).
|
|
243
|
+
#
|
|
244
|
+
# @return [void]
|
|
245
|
+
#
|
|
246
|
+
def shutdown_pools
|
|
247
|
+
@pools.each do |queue_name, pool|
|
|
248
|
+
pool.shutdown
|
|
249
|
+
pool.wait_for_termination(30)
|
|
250
|
+
logger.info "[Postburner::Worker] Queue '#{queue_name}' shutdown complete"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Starts worker in forked mode (forks: 1+).
|
|
255
|
+
#
|
|
256
|
+
# Forks multiple child processes for each queue, each running
|
|
257
|
+
# a thread pool. Parent process monitors children and restarts them when they exit.
|
|
258
|
+
#
|
|
259
|
+
# @return [void]
|
|
260
|
+
#
|
|
261
|
+
def start_forked_mode
|
|
262
|
+
logger.info "[Postburner::Worker] Mode: Multi-process (forks: 1+)"
|
|
263
|
+
|
|
264
|
+
# Track children: { pid => { queue: 'name', fork_num: 0 } }
|
|
265
|
+
@children = {}
|
|
266
|
+
|
|
267
|
+
# Spawn configured number of forks for each queue
|
|
268
|
+
config.queue_names.each do |queue_name|
|
|
269
|
+
spawn_queue_workers(queue_name)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Parent process monitors children
|
|
273
|
+
until shutdown?
|
|
274
|
+
begin
|
|
275
|
+
pid, status = Process.wait2(-1, Process::WNOHANG)
|
|
276
|
+
|
|
277
|
+
if pid
|
|
278
|
+
child_info = @children.delete(pid)
|
|
279
|
+
exit_code = status.exitstatus
|
|
280
|
+
queue_name = child_info[:queue]
|
|
281
|
+
fork_num = child_info[:fork_num]
|
|
282
|
+
|
|
283
|
+
if exit_code == 99
|
|
284
|
+
# GC restart - this is normal
|
|
285
|
+
logger.info "[Postburner::Worker] Queue '#{queue_name}' fork #{fork_num} reached GC limit, restarting..."
|
|
286
|
+
spawn_queue_worker(queue_name, fork_num) unless shutdown?
|
|
287
|
+
else
|
|
288
|
+
logger.error "[Postburner::Worker] Queue '#{queue_name}' fork #{fork_num} exited unexpectedly (code: #{exit_code})"
|
|
289
|
+
spawn_queue_worker(queue_name, fork_num) unless shutdown?
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
sleep 0.5
|
|
294
|
+
rescue Errno::ECHILD
|
|
295
|
+
# No children left
|
|
296
|
+
break
|
|
297
|
+
rescue => e
|
|
298
|
+
logger.error "[Postburner::Worker] Monitor error: #{e.message}"
|
|
299
|
+
sleep 1
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Shutdown - wait for all children
|
|
304
|
+
logger.info "[Postburner::Worker] Shutting down, waiting for children..."
|
|
305
|
+
shutdown_children
|
|
306
|
+
logger.info "[Postburner::Worker] Shutdown complete"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Spawns all forked worker processes for a specific queue.
|
|
310
|
+
#
|
|
311
|
+
# @param queue_name [String] Name of the queue to process
|
|
312
|
+
#
|
|
313
|
+
# @return [void]
|
|
314
|
+
#
|
|
315
|
+
def spawn_queue_workers(queue_name)
|
|
316
|
+
queue_config = config.queue_config(queue_name)
|
|
317
|
+
fork_count = queue_config['forks'] || queue_config[:forks] || config.default_forks
|
|
318
|
+
thread_count = queue_config['threads'] || queue_config[:threads] || config.default_threads
|
|
319
|
+
|
|
320
|
+
# Skip if this queue has 0 forks (shouldn't happen in forked mode, but be defensive)
|
|
321
|
+
return if fork_count == 0
|
|
322
|
+
|
|
323
|
+
total_concurrency = fork_count * thread_count
|
|
324
|
+
logger.info "[Postburner::Worker] Queue '#{queue_name}': #{fork_count} forks × #{thread_count} threads = #{total_concurrency} total concurrency"
|
|
325
|
+
|
|
326
|
+
fork_count.times do |fork_num|
|
|
327
|
+
spawn_queue_worker(queue_name, fork_num)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Spawns a single forked worker process for a specific queue.
|
|
332
|
+
#
|
|
333
|
+
# @param queue_name [String] Name of the queue to process
|
|
334
|
+
# @param fork_num [Integer] Fork number (0-indexed)
|
|
335
|
+
#
|
|
336
|
+
# @return [void]
|
|
337
|
+
#
|
|
338
|
+
def spawn_queue_worker(queue_name, fork_num)
|
|
339
|
+
pid = fork do
|
|
340
|
+
# Child process
|
|
341
|
+
run_queue_worker(queue_name, fork_num)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
@children[pid] = { queue: queue_name, fork_num: fork_num }
|
|
345
|
+
logger.info "[Postburner::Worker] Spawned worker for queue '#{queue_name}' fork #{fork_num} (pid: #{pid})"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Runs the thread pool worker for a specific queue fork.
|
|
349
|
+
#
|
|
350
|
+
# This runs in the child process. Creates a thread pool and processes
|
|
351
|
+
# jobs until GC limit is reached or shutdown is requested.
|
|
352
|
+
#
|
|
353
|
+
# @param queue_name [String] Name of the queue to process
|
|
354
|
+
# @param fork_num [Integer] Fork number (for logging)
|
|
355
|
+
#
|
|
356
|
+
# @return [void]
|
|
357
|
+
#
|
|
358
|
+
def run_queue_worker(queue_name, fork_num)
|
|
359
|
+
queue_config = config.queue_config(queue_name)
|
|
360
|
+
thread_count = queue_config['threads'] || queue_config[:threads] || config.default_threads
|
|
361
|
+
gc_limit = queue_config['gc_limit'] || queue_config[:gc_limit] || config.default_gc_limit
|
|
362
|
+
|
|
363
|
+
logger.info "[Postburner::Worker] Queue '#{queue_name}' fork #{fork_num}: #{thread_count} threads, GC limit #{gc_limit || 'unlimited'}"
|
|
364
|
+
|
|
365
|
+
# Track jobs processed in this fork
|
|
366
|
+
jobs_processed = Concurrent::AtomicFixnum.new(0)
|
|
367
|
+
|
|
368
|
+
# Create thread pool
|
|
369
|
+
pool = Concurrent::FixedThreadPool.new(thread_count)
|
|
370
|
+
|
|
371
|
+
# Each thread needs its own Beanstalkd connection
|
|
372
|
+
thread_count.times do
|
|
373
|
+
pool.post do
|
|
374
|
+
process_jobs_in_fork(queue_name, fork_num, jobs_processed, gc_limit)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Wait for shutdown or GC limit
|
|
379
|
+
until shutdown? || (gc_limit && jobs_processed.value >= gc_limit)
|
|
380
|
+
sleep 0.5
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Shutdown pool gracefully
|
|
384
|
+
pool.shutdown
|
|
385
|
+
pool.wait_for_termination(30)
|
|
386
|
+
|
|
387
|
+
if gc_limit && jobs_processed.value >= gc_limit
|
|
388
|
+
logger.info "[Postburner::Worker] Queue '#{queue_name}' fork #{fork_num} reached GC limit (#{jobs_processed.value} jobs), exiting for restart..."
|
|
389
|
+
exit 99 # Special exit code for GC restart
|
|
390
|
+
else
|
|
391
|
+
logger.info "[Postburner::Worker] Queue '#{queue_name}' fork #{fork_num} shutting down gracefully..."
|
|
392
|
+
exit 0
|
|
393
|
+
end
|
|
394
|
+
rescue => e
|
|
395
|
+
logger.error "[Postburner::Worker] Queue '#{queue_name}' fork #{fork_num} error: #{e.message}"
|
|
396
|
+
logger.error e.backtrace.join("\n")
|
|
397
|
+
exit 1
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Processes jobs in a single thread within a fork (forked mode).
|
|
401
|
+
#
|
|
402
|
+
# Each thread has its own Beanstalkd connection and reserves jobs
|
|
403
|
+
# from the specified queue.
|
|
404
|
+
#
|
|
405
|
+
# @param queue_name [String] Name of the queue to process
|
|
406
|
+
# @param fork_num [Integer] Fork number (for logging)
|
|
407
|
+
# @param jobs_processed [Concurrent::AtomicFixnum] Shared counter of jobs processed
|
|
408
|
+
# @param gc_limit [Integer, nil] Maximum jobs before triggering GC restart (nil = unlimited)
|
|
409
|
+
#
|
|
410
|
+
# @return [void]
|
|
411
|
+
#
|
|
412
|
+
def process_jobs_in_fork(queue_name, fork_num, jobs_processed, gc_limit)
|
|
413
|
+
connection = Postburner::Connection.new
|
|
414
|
+
|
|
415
|
+
# Watch only this queue
|
|
416
|
+
watch_queues(connection, queue_name)
|
|
417
|
+
|
|
418
|
+
until shutdown? || (gc_limit && jobs_processed.value >= gc_limit)
|
|
419
|
+
begin
|
|
420
|
+
# Reserve with short timeout
|
|
421
|
+
job = connection.beanstalk.tubes.reserve(timeout: 1)
|
|
422
|
+
|
|
423
|
+
if job
|
|
424
|
+
logger.debug "[Postburner::Worker] Queue '#{queue_name}' fork #{fork_num} thread #{Thread.current.object_id} reserved job #{job.id}"
|
|
425
|
+
execute_job(job)
|
|
426
|
+
jobs_processed.increment
|
|
427
|
+
end
|
|
428
|
+
rescue Beaneater::TimedOutError
|
|
429
|
+
# Normal timeout, continue
|
|
430
|
+
next
|
|
431
|
+
rescue Beaneater::NotConnected => e
|
|
432
|
+
logger.error "[Postburner::Worker] Thread disconnected: #{e.message}"
|
|
433
|
+
sleep 1
|
|
434
|
+
connection.reconnect!
|
|
435
|
+
rescue => e
|
|
436
|
+
logger.error "[Postburner::Worker] Thread error: #{e.class} - #{e.message}"
|
|
437
|
+
logger.error e.backtrace.join("\n")
|
|
438
|
+
sleep 1
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
ensure
|
|
442
|
+
connection&.close rescue nil
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Gracefully shuts down all child processes (forked mode).
|
|
446
|
+
#
|
|
447
|
+
# Sends TERM signal to all children and waits for them to exit.
|
|
448
|
+
#
|
|
449
|
+
# @return [void]
|
|
450
|
+
#
|
|
451
|
+
def shutdown_children
|
|
452
|
+
@children.keys.each do |pid|
|
|
453
|
+
begin
|
|
454
|
+
Process.kill('TERM', pid)
|
|
455
|
+
rescue Errno::ESRCH
|
|
456
|
+
# Process already exited
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Wait up to 30 seconds for children to exit
|
|
461
|
+
timeout = Time.now + 30
|
|
462
|
+
until @children.empty? || Time.now > timeout
|
|
463
|
+
pid, status = Process.wait2(-1, Process::WNOHANG)
|
|
464
|
+
@children.delete(pid) if pid
|
|
465
|
+
sleep 0.5
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Force kill any remaining children
|
|
469
|
+
@children.keys.each do |pid|
|
|
470
|
+
begin
|
|
471
|
+
Process.kill('KILL', pid)
|
|
472
|
+
Process.wait(pid)
|
|
473
|
+
rescue Errno::ESRCH
|
|
474
|
+
# Already exited
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|