postburner 1.0.0.pre.11 → 1.0.0.pre.12
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/README.md +961 -555
- data/app/concerns/postburner/commands.rb +1 -1
- data/app/concerns/postburner/execution.rb +11 -11
- data/app/concerns/postburner/insertion.rb +1 -1
- data/app/concerns/postburner/logging.rb +2 -2
- data/app/concerns/postburner/statistics.rb +1 -1
- data/app/models/postburner/job.rb +27 -4
- data/app/models/postburner/mailer.rb +1 -1
- data/app/models/postburner/schedule.rb +703 -0
- data/app/models/postburner/schedule_execution.rb +353 -0
- data/app/views/postburner/jobs/show.html.haml +3 -3
- data/lib/generators/postburner/install/install_generator.rb +1 -0
- data/lib/generators/postburner/install/templates/config/postburner.yml +15 -6
- data/lib/generators/postburner/install/templates/migrations/create_postburner_schedules.rb.erb +71 -0
- data/lib/postburner/active_job/adapter.rb +3 -3
- data/lib/postburner/active_job/payload.rb +5 -0
- data/lib/postburner/advisory_lock.rb +123 -0
- data/lib/postburner/configuration.rb +43 -7
- data/lib/postburner/connection.rb +7 -6
- data/lib/postburner/runner.rb +26 -3
- data/lib/postburner/scheduler.rb +427 -0
- data/lib/postburner/strategies/immediate_test_queue.rb +24 -7
- data/lib/postburner/strategies/nice_queue.rb +1 -1
- data/lib/postburner/strategies/null_queue.rb +2 -2
- data/lib/postburner/strategies/test_queue.rb +2 -2
- data/lib/postburner/time_helpers.rb +4 -2
- data/lib/postburner/tube.rb +9 -1
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner/worker.rb +684 -0
- data/lib/postburner.rb +32 -13
- metadata +7 -3
- data/lib/postburner/workers/base.rb +0 -205
- data/lib/postburner/workers/worker.rb +0 -396
|
@@ -1,396 +0,0 @@
|
|
|
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.
|
|
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
|
-
# └─ Thread Pool (N threads watching all queues)
|
|
20
|
-
# ```
|
|
21
|
-
#
|
|
22
|
-
# ### Multi-Process Mode (forks: 1+)
|
|
23
|
-
# ```
|
|
24
|
-
# Parent Process
|
|
25
|
-
# ├─ Fork 0
|
|
26
|
-
# │ └─ Thread Pool (N threads watching all queues)
|
|
27
|
-
# ├─ Fork 1
|
|
28
|
-
# │ └─ Thread Pool (N threads watching all queues)
|
|
29
|
-
# └─ Fork 2
|
|
30
|
-
# └─ Thread Pool (N threads watching all queues)
|
|
31
|
-
# ```
|
|
32
|
-
#
|
|
33
|
-
# ## Scaling Strategy
|
|
34
|
-
#
|
|
35
|
-
# **Development:**
|
|
36
|
-
# ```yaml
|
|
37
|
-
# forks: 0
|
|
38
|
-
# threads: 1
|
|
39
|
-
# ```
|
|
40
|
-
# Single-threaded, single-process (simplest debugging)
|
|
41
|
-
#
|
|
42
|
-
# **Staging:**
|
|
43
|
-
# ```yaml
|
|
44
|
-
# forks: 0
|
|
45
|
-
# threads: 10
|
|
46
|
-
# ```
|
|
47
|
-
# Multi-threaded, single-process (moderate concurrency)
|
|
48
|
-
#
|
|
49
|
-
# **Production:**
|
|
50
|
-
# ```yaml
|
|
51
|
-
# forks: 4
|
|
52
|
-
# threads: 10
|
|
53
|
-
# ```
|
|
54
|
-
# 4 processes × 10 threads = 40 concurrent jobs
|
|
55
|
-
#
|
|
56
|
-
# ## Configuration
|
|
57
|
-
#
|
|
58
|
-
# @example Development (single-threaded)
|
|
59
|
-
# development:
|
|
60
|
-
# workers:
|
|
61
|
-
# default:
|
|
62
|
-
# forks: 0
|
|
63
|
-
# threads: 1
|
|
64
|
-
# queues:
|
|
65
|
-
# - default
|
|
66
|
-
# - mailers
|
|
67
|
-
#
|
|
68
|
-
# @example Production (Puma-style: forks × threads)
|
|
69
|
-
# production:
|
|
70
|
-
# default_forks: 2
|
|
71
|
-
# default_threads: 10
|
|
72
|
-
# workers:
|
|
73
|
-
# default:
|
|
74
|
-
# forks: 4 # Overrides default_forks
|
|
75
|
-
# threads: 10 # Overrides default_threads
|
|
76
|
-
# queues:
|
|
77
|
-
# - critical
|
|
78
|
-
# - default
|
|
79
|
-
# - mailers
|
|
80
|
-
#
|
|
81
|
-
class Worker < Base
|
|
82
|
-
# Starts the worker.
|
|
83
|
-
#
|
|
84
|
-
# Detects whether to run in single-process mode (forks: 0) or
|
|
85
|
-
# multi-process mode (forks: 1+) and starts accordingly.
|
|
86
|
-
#
|
|
87
|
-
# @return [void]
|
|
88
|
-
#
|
|
89
|
-
def start
|
|
90
|
-
logger.info "[Postburner::Worker] Starting worker '#{worker_config[:name]}'..."
|
|
91
|
-
logger.info "[Postburner::Worker] Queues: #{config.queue_names.join(', ')}"
|
|
92
|
-
logger.info "[Postburner::Worker] Config: #{worker_config[:forks]} forks, #{worker_config[:threads]} threads, gc_limit: #{worker_config[:gc_limit] || 'unlimited'}, timeout: #{worker_config[:timeout]}s"
|
|
93
|
-
logger.info "[Postburner] #{config.beanstalk_url} watching tubes: #{config.expanded_tube_names.join(', ')}"
|
|
94
|
-
|
|
95
|
-
# Detect mode based on fork configuration
|
|
96
|
-
if worker_config[:forks] > 0
|
|
97
|
-
start_forked_mode
|
|
98
|
-
else
|
|
99
|
-
start_single_process_mode
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
private
|
|
104
|
-
|
|
105
|
-
# Returns the worker configuration hash.
|
|
106
|
-
#
|
|
107
|
-
# @return [Hash] Worker config with :name, :queues, :forks, :threads, :gc_limit, :timeout
|
|
108
|
-
#
|
|
109
|
-
def worker_config
|
|
110
|
-
config.worker_config
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Starts worker in single-process mode (forks: 0).
|
|
114
|
-
#
|
|
115
|
-
# Creates a thread pool that watches all configured queues.
|
|
116
|
-
# Suitable for development and moderate concurrency needs.
|
|
117
|
-
#
|
|
118
|
-
# @return [void]
|
|
119
|
-
#
|
|
120
|
-
def start_single_process_mode
|
|
121
|
-
logger.info "[Postburner::Worker] Mode: Single process (forks: 0)"
|
|
122
|
-
|
|
123
|
-
# Track total jobs processed across all threads
|
|
124
|
-
@jobs_processed = Concurrent::AtomicFixnum.new(0)
|
|
125
|
-
@gc_limit = worker_config[:gc_limit]
|
|
126
|
-
|
|
127
|
-
# Create thread pool
|
|
128
|
-
thread_count = worker_config[:threads]
|
|
129
|
-
@pool = Concurrent::FixedThreadPool.new(thread_count)
|
|
130
|
-
|
|
131
|
-
# Spawn worker threads
|
|
132
|
-
thread_count.times do
|
|
133
|
-
@pool.post do
|
|
134
|
-
process_jobs
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Monitor for shutdown or GC limit
|
|
139
|
-
until shutdown? || (@gc_limit && @jobs_processed.value >= @gc_limit)
|
|
140
|
-
sleep 0.5
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Shutdown pool gracefully
|
|
144
|
-
logger.info "[Postburner::Worker] Shutting down..."
|
|
145
|
-
@pool.shutdown
|
|
146
|
-
@pool.wait_for_termination(30)
|
|
147
|
-
|
|
148
|
-
if @gc_limit && @jobs_processed.value >= @gc_limit
|
|
149
|
-
logger.info "[Postburner::Worker] Reached GC limit (#{@jobs_processed.value} jobs), exiting for restart..."
|
|
150
|
-
exit 99 # Special exit code for GC restart
|
|
151
|
-
else
|
|
152
|
-
logger.info "[Postburner::Worker] Shutdown complete"
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Processes jobs in a single thread.
|
|
157
|
-
#
|
|
158
|
-
# Each thread has its own Beanstalkd connection and reserves jobs
|
|
159
|
-
# from all configured queues.
|
|
160
|
-
#
|
|
161
|
-
# @return [void]
|
|
162
|
-
#
|
|
163
|
-
def process_jobs
|
|
164
|
-
connection = Postburner::Connection.new
|
|
165
|
-
timeout = worker_config[:timeout]
|
|
166
|
-
|
|
167
|
-
# Watch all configured queues
|
|
168
|
-
watch_queues(connection, config.queue_names)
|
|
169
|
-
|
|
170
|
-
until shutdown? || (@gc_limit && @jobs_processed.value >= @gc_limit)
|
|
171
|
-
begin
|
|
172
|
-
# Reserve with configured timeout
|
|
173
|
-
job = connection.beanstalk.tubes.reserve(timeout: timeout)
|
|
174
|
-
|
|
175
|
-
if job
|
|
176
|
-
logger.debug "[Postburner::Worker] Thread #{Thread.current.object_id} reserved job #{job.id}"
|
|
177
|
-
execute_job(job)
|
|
178
|
-
@jobs_processed.increment
|
|
179
|
-
end
|
|
180
|
-
rescue Beaneater::TimedOutError
|
|
181
|
-
# Normal timeout, continue
|
|
182
|
-
next
|
|
183
|
-
rescue Beaneater::NotConnected => e
|
|
184
|
-
logger.error "[Postburner::Worker] Thread disconnected: #{e.message}"
|
|
185
|
-
sleep 1
|
|
186
|
-
connection.reconnect!
|
|
187
|
-
rescue => e
|
|
188
|
-
logger.error "[Postburner::Worker] Thread error: #{e.class} - #{e.message}"
|
|
189
|
-
logger.error e.backtrace.join("\n")
|
|
190
|
-
sleep 1
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
ensure
|
|
194
|
-
connection&.close rescue nil
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Starts worker in forked mode (forks: 1+).
|
|
198
|
-
#
|
|
199
|
-
# Forks multiple child processes, each running a thread pool.
|
|
200
|
-
# Parent process monitors children and restarts them when they exit.
|
|
201
|
-
#
|
|
202
|
-
# @return [void]
|
|
203
|
-
#
|
|
204
|
-
def start_forked_mode
|
|
205
|
-
logger.info "[Postburner::Worker] Mode: Multi-process (#{worker_config[:forks]} forks)"
|
|
206
|
-
|
|
207
|
-
# Track children: { pid => fork_num }
|
|
208
|
-
@children = {}
|
|
209
|
-
|
|
210
|
-
# Spawn configured number of forks
|
|
211
|
-
worker_config[:forks].times do |fork_num|
|
|
212
|
-
spawn_fork(fork_num)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Parent process monitors children
|
|
216
|
-
until shutdown?
|
|
217
|
-
begin
|
|
218
|
-
pid, status = Process.wait2(-1, Process::WNOHANG)
|
|
219
|
-
|
|
220
|
-
if pid
|
|
221
|
-
fork_num = @children.delete(pid)
|
|
222
|
-
exit_code = status.exitstatus
|
|
223
|
-
|
|
224
|
-
if exit_code == 99
|
|
225
|
-
# GC restart - this is normal
|
|
226
|
-
logger.info "[Postburner::Worker] Fork #{fork_num} reached GC limit, restarting..."
|
|
227
|
-
spawn_fork(fork_num) unless shutdown?
|
|
228
|
-
else
|
|
229
|
-
logger.error "[Postburner::Worker] Fork #{fork_num} exited unexpectedly (code: #{exit_code})"
|
|
230
|
-
spawn_fork(fork_num) unless shutdown?
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
sleep 0.5
|
|
235
|
-
rescue Errno::ECHILD
|
|
236
|
-
# No children left
|
|
237
|
-
break
|
|
238
|
-
rescue => e
|
|
239
|
-
logger.error "[Postburner::Worker] Monitor error: #{e.message}"
|
|
240
|
-
sleep 1
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# Shutdown - wait for all children
|
|
245
|
-
logger.info "[Postburner::Worker] Shutting down, waiting for children..."
|
|
246
|
-
shutdown_children
|
|
247
|
-
logger.info "[Postburner::Worker] Shutdown complete"
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# Spawns a single forked worker process.
|
|
251
|
-
#
|
|
252
|
-
# @param fork_num [Integer] Fork number (0-indexed)
|
|
253
|
-
#
|
|
254
|
-
# @return [void]
|
|
255
|
-
#
|
|
256
|
-
def spawn_fork(fork_num)
|
|
257
|
-
pid = fork do
|
|
258
|
-
# Child process
|
|
259
|
-
run_fork(fork_num)
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
@children[pid] = fork_num
|
|
263
|
-
logger.info "[Postburner::Worker] Spawned fork #{fork_num} (pid: #{pid})"
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Runs the thread pool worker in a forked process.
|
|
267
|
-
#
|
|
268
|
-
# This runs in the child process. Creates a thread pool and processes
|
|
269
|
-
# jobs until GC limit is reached or shutdown is requested.
|
|
270
|
-
#
|
|
271
|
-
# @param fork_num [Integer] Fork number (for logging)
|
|
272
|
-
#
|
|
273
|
-
# @return [void]
|
|
274
|
-
#
|
|
275
|
-
def run_fork(fork_num)
|
|
276
|
-
thread_count = worker_config[:threads]
|
|
277
|
-
gc_limit = worker_config[:gc_limit]
|
|
278
|
-
|
|
279
|
-
logger.info "[Postburner::Worker] Fork #{fork_num}: #{thread_count} threads, GC limit #{gc_limit || 'unlimited'}"
|
|
280
|
-
|
|
281
|
-
# Track jobs processed in this fork
|
|
282
|
-
jobs_processed = Concurrent::AtomicFixnum.new(0)
|
|
283
|
-
|
|
284
|
-
# Create thread pool
|
|
285
|
-
pool = Concurrent::FixedThreadPool.new(thread_count)
|
|
286
|
-
|
|
287
|
-
# Each thread needs its own Beanstalkd connection
|
|
288
|
-
thread_count.times do
|
|
289
|
-
pool.post do
|
|
290
|
-
process_jobs_in_fork(fork_num, jobs_processed, gc_limit)
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# Wait for shutdown or GC limit
|
|
295
|
-
until shutdown? || (gc_limit && jobs_processed.value >= gc_limit)
|
|
296
|
-
sleep 0.5
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
# Shutdown pool gracefully
|
|
300
|
-
pool.shutdown
|
|
301
|
-
pool.wait_for_termination(30)
|
|
302
|
-
|
|
303
|
-
if gc_limit && jobs_processed.value >= gc_limit
|
|
304
|
-
logger.info "[Postburner::Worker] Fork #{fork_num} reached GC limit (#{jobs_processed.value} jobs), exiting for restart..."
|
|
305
|
-
exit 99 # Special exit code for GC restart
|
|
306
|
-
else
|
|
307
|
-
logger.info "[Postburner::Worker] Fork #{fork_num} shutting down gracefully..."
|
|
308
|
-
exit 0
|
|
309
|
-
end
|
|
310
|
-
rescue => e
|
|
311
|
-
logger.error "[Postburner::Worker] Fork #{fork_num} error: #{e.message}"
|
|
312
|
-
logger.error e.backtrace.join("\n")
|
|
313
|
-
exit 1
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
# Processes jobs in a single thread within a fork.
|
|
317
|
-
#
|
|
318
|
-
# Each thread has its own Beanstalkd connection and reserves jobs
|
|
319
|
-
# from all configured queues.
|
|
320
|
-
#
|
|
321
|
-
# @param fork_num [Integer] Fork number (for logging)
|
|
322
|
-
# @param jobs_processed [Concurrent::AtomicFixnum] Shared counter of jobs processed
|
|
323
|
-
# @param gc_limit [Integer, nil] Maximum jobs before triggering GC restart (nil = unlimited)
|
|
324
|
-
#
|
|
325
|
-
# @return [void]
|
|
326
|
-
#
|
|
327
|
-
def process_jobs_in_fork(fork_num, jobs_processed, gc_limit)
|
|
328
|
-
connection = Postburner::Connection.new
|
|
329
|
-
timeout = worker_config[:timeout]
|
|
330
|
-
|
|
331
|
-
# Watch all configured queues
|
|
332
|
-
watch_queues(connection, config.queue_names)
|
|
333
|
-
|
|
334
|
-
until shutdown? || (gc_limit && jobs_processed.value >= gc_limit)
|
|
335
|
-
begin
|
|
336
|
-
# Reserve with configured timeout
|
|
337
|
-
job = connection.beanstalk.tubes.reserve(timeout: timeout)
|
|
338
|
-
|
|
339
|
-
if job
|
|
340
|
-
logger.debug "[Postburner::Worker] Fork #{fork_num} thread #{Thread.current.object_id} reserved job #{job.id}"
|
|
341
|
-
execute_job(job)
|
|
342
|
-
jobs_processed.increment
|
|
343
|
-
end
|
|
344
|
-
rescue Beaneater::TimedOutError
|
|
345
|
-
# Normal timeout, continue
|
|
346
|
-
next
|
|
347
|
-
rescue Beaneater::NotConnected => e
|
|
348
|
-
logger.error "[Postburner::Worker] Thread disconnected: #{e.message}"
|
|
349
|
-
sleep 1
|
|
350
|
-
connection.reconnect!
|
|
351
|
-
rescue => e
|
|
352
|
-
logger.error "[Postburner::Worker] Thread error: #{e.class} - #{e.message}"
|
|
353
|
-
logger.error e.backtrace.join("\n")
|
|
354
|
-
sleep 1
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
ensure
|
|
358
|
-
connection&.close rescue nil
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
# Gracefully shuts down all child processes (forked mode).
|
|
362
|
-
#
|
|
363
|
-
# Sends TERM signal to all children and waits for them to exit.
|
|
364
|
-
#
|
|
365
|
-
# @return [void]
|
|
366
|
-
#
|
|
367
|
-
def shutdown_children
|
|
368
|
-
@children.keys.each do |pid|
|
|
369
|
-
begin
|
|
370
|
-
Process.kill('TERM', pid)
|
|
371
|
-
rescue Errno::ESRCH
|
|
372
|
-
# Process already exited
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Wait up to 30 seconds for children to exit
|
|
377
|
-
timeout = Time.zone.now + 30
|
|
378
|
-
until @children.empty? || Time.zone.now > timeout
|
|
379
|
-
pid, status = Process.wait2(-1, Process::WNOHANG)
|
|
380
|
-
@children.delete(pid) if pid
|
|
381
|
-
sleep 0.5
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
# Force kill any remaining children
|
|
385
|
-
@children.keys.each do |pid|
|
|
386
|
-
begin
|
|
387
|
-
Process.kill('KILL', pid)
|
|
388
|
-
Process.wait(pid)
|
|
389
|
-
rescue Errno::ESRCH
|
|
390
|
-
# Already exited
|
|
391
|
-
end
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
end
|
|
395
|
-
end
|
|
396
|
-
end
|