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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +961 -555
  3. data/app/concerns/postburner/commands.rb +1 -1
  4. data/app/concerns/postburner/execution.rb +11 -11
  5. data/app/concerns/postburner/insertion.rb +1 -1
  6. data/app/concerns/postburner/logging.rb +2 -2
  7. data/app/concerns/postburner/statistics.rb +1 -1
  8. data/app/models/postburner/job.rb +27 -4
  9. data/app/models/postburner/mailer.rb +1 -1
  10. data/app/models/postburner/schedule.rb +703 -0
  11. data/app/models/postburner/schedule_execution.rb +353 -0
  12. data/app/views/postburner/jobs/show.html.haml +3 -3
  13. data/lib/generators/postburner/install/install_generator.rb +1 -0
  14. data/lib/generators/postburner/install/templates/config/postburner.yml +15 -6
  15. data/lib/generators/postburner/install/templates/migrations/create_postburner_schedules.rb.erb +71 -0
  16. data/lib/postburner/active_job/adapter.rb +3 -3
  17. data/lib/postburner/active_job/payload.rb +5 -0
  18. data/lib/postburner/advisory_lock.rb +123 -0
  19. data/lib/postburner/configuration.rb +43 -7
  20. data/lib/postburner/connection.rb +7 -6
  21. data/lib/postburner/runner.rb +26 -3
  22. data/lib/postburner/scheduler.rb +427 -0
  23. data/lib/postburner/strategies/immediate_test_queue.rb +24 -7
  24. data/lib/postburner/strategies/nice_queue.rb +1 -1
  25. data/lib/postburner/strategies/null_queue.rb +2 -2
  26. data/lib/postburner/strategies/test_queue.rb +2 -2
  27. data/lib/postburner/time_helpers.rb +4 -2
  28. data/lib/postburner/tube.rb +9 -1
  29. data/lib/postburner/version.rb +1 -1
  30. data/lib/postburner/worker.rb +684 -0
  31. data/lib/postburner.rb +32 -13
  32. metadata +7 -3
  33. data/lib/postburner/workers/base.rb +0 -205
  34. 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