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
data/lib/postburner.rb CHANGED
@@ -7,8 +7,7 @@ require "postburner/tracked"
7
7
  require "postburner/active_job/payload"
8
8
  require "postburner/active_job/execution"
9
9
  require "postburner/active_job/adapter"
10
- require "postburner/workers/base"
11
- require "postburner/workers/worker"
10
+ require "postburner/worker"
12
11
  require "postburner/runner"
13
12
  require "postburner/engine"
14
13
  require "postburner/strategies/queue"
@@ -17,7 +16,7 @@ require "postburner/strategies/test_queue"
17
16
  require "postburner/strategies/immediate_test_queue"
18
17
  require "postburner/strategies/null_queue"
19
18
 
20
- # Postburner - PostgreSQL-backed job queue system built on Backburner and Beanstalkd.
19
+ # Postburner - PostgreSQL-backed job queue system built on Beanstalkd.
21
20
  #
22
21
  # Postburner is a Ruby on Rails Engine that provides a database-backed job queue with
23
22
  # full audit trails, inspection capabilities, and multiple execution strategies. Every
@@ -28,7 +27,7 @@ require "postburner/strategies/null_queue"
28
27
  #
29
28
  # - **Jobs:** Subclass {Postburner::Job} and implement `perform` method
30
29
  # - **Queue Strategies:** Control how jobs are executed (async, inline, test modes)
31
- # - **Beanstalkd Integration:** Production queuing via Backburner
30
+ # - **Beanstalkd Integration:** Production queuing via Beanstalkd
32
31
  # - **Database Persistence:** Full audit trail with timestamps, logs, and errors
33
32
  # - **Callbacks:** ActiveJob-style lifecycle hooks (enqueue, attempt, processing, processed)
34
33
  #
@@ -385,7 +384,7 @@ module Postburner
385
384
  # configuration. This prevents accidentally clearing tubes from other
386
385
  # applications or environments sharing the same Beanstalkd server.
387
386
  #
388
- # @param tube_names [Array<String>, nil] Array of tube names to clear, or nil to only show stats
387
+ # @param tube_names [Array<String>, String, nil] Tube name(s) to clear, or nil to only show stats
389
388
  # @param silent [Boolean] If true, suppress output to stdout (default: false)
390
389
  #
391
390
  # @return [Hash] Statistics and results (see Connection#clear_tubes!)
@@ -400,6 +399,10 @@ module Postburner
400
399
  # Postburner.clear_jobs!(Postburner.watched_tube_names)
401
400
  # # Only clears tubes defined in your config
402
401
  #
402
+ # @example Clear scheduler tube (single string)
403
+ # Postburner.clear_jobs!(Postburner.scheduler_tube_name)
404
+ # # Clears the scheduler watchdog tube
405
+ #
403
406
  # @example Trying to clear unconfigured tube - RAISES ERROR
404
407
  # Postburner.clear_jobs!(['some-other-app-tube'])
405
408
  # # => ArgumentError: Cannot clear tubes not in configuration
@@ -413,6 +416,7 @@ module Postburner
413
416
  def self.clear_jobs!(tube_names = nil, silent: false)
414
417
  require 'json'
415
418
 
419
+ tube_names = Array(tube_names) if tube_names
416
420
  result = connection.clear_tubes!(tube_names)
417
421
 
418
422
  unless silent
@@ -449,6 +453,21 @@ module Postburner
449
453
  @__watched_tubes ||= watched_tube_names.map { |tube_name| connection.tubes[tube_name] }
450
454
  end
451
455
 
456
+ # Returns the scheduler tube name with environment prefix.
457
+ #
458
+ # @return [String] Expanded scheduler tube name
459
+ #
460
+ # @example
461
+ # Postburner.scheduler_tube_name
462
+ # # => 'postburner.production.scheduler'
463
+ #
464
+ # @example Clear scheduler tube
465
+ # Postburner.clear_jobs!(Postburner.scheduler_tube_name)
466
+ #
467
+ def self.scheduler_tube_name
468
+ configuration.scheduler_tube_name
469
+ end
470
+
452
471
  # Returns the Beanstalkd tube name prefix for the given environment.
453
472
  #
454
473
  # Delegates to {Configuration#tube_prefix}. Postburner automatically
@@ -519,14 +538,14 @@ module Postburner
519
538
 
520
539
  tube_data = {
521
540
  name: tube.name,
522
- ready: stats_hash['current-jobs-ready'] || 0,
523
- delayed: stats_hash['current-jobs-delayed'] || 0,
524
- buried: stats_hash['current-jobs-buried'] || 0,
525
- reserved: stats_hash['current-jobs-reserved'] || 0,
526
- total: (stats_hash['current-jobs-ready'] || 0) +
527
- (stats_hash['current-jobs-delayed'] || 0) +
528
- (stats_hash['current-jobs-buried'] || 0) +
529
- (stats_hash['current-jobs-reserved'] || 0)
541
+ ready: stats_hash['current_jobs_ready'] || 0,
542
+ delayed: stats_hash['current_jobs_delayed'] || 0,
543
+ buried: stats_hash['current_jobs_buried'] || 0,
544
+ reserved: stats_hash['current_jobs_reserved'] || 0,
545
+ total: (stats_hash['current_jobs_ready'] || 0) +
546
+ (stats_hash['current_jobs_delayed'] || 0) +
547
+ (stats_hash['current_jobs_buried'] || 0) +
548
+ (stats_hash['current_jobs_reserved'] || 0)
530
549
  }
531
550
  rescue Beaneater::NotFoundError
532
551
  # Tube doesn't exist yet, skip it
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postburner
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.11
4
+ version: 1.0.0.pre.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Smith
@@ -124,6 +124,8 @@ files:
124
124
  - app/models/postburner/application_record.rb
125
125
  - app/models/postburner/job.rb
126
126
  - app/models/postburner/mailer.rb
127
+ - app/models/postburner/schedule.rb
128
+ - app/models/postburner/schedule_execution.rb
127
129
  - app/models/postburner/tracked_job.rb
128
130
  - app/views/layouts/postburner/application.html.haml
129
131
  - app/views/postburner/jobs/index.html.haml
@@ -138,15 +140,18 @@ files:
138
140
  - lib/generators/postburner/install/install_generator.rb
139
141
  - lib/generators/postburner/install/templates/config/postburner.yml
140
142
  - lib/generators/postburner/install/templates/migrations/create_postburner_jobs.rb.erb
143
+ - lib/generators/postburner/install/templates/migrations/create_postburner_schedules.rb.erb
141
144
  - lib/postburner.rb
142
145
  - lib/postburner/active_job/adapter.rb
143
146
  - lib/postburner/active_job/execution.rb
144
147
  - lib/postburner/active_job/payload.rb
148
+ - lib/postburner/advisory_lock.rb
145
149
  - lib/postburner/beanstalkd.rb
146
150
  - lib/postburner/configuration.rb
147
151
  - lib/postburner/connection.rb
148
152
  - lib/postburner/engine.rb
149
153
  - lib/postburner/runner.rb
154
+ - lib/postburner/scheduler.rb
150
155
  - lib/postburner/strategies/immediate_test_queue.rb
151
156
  - lib/postburner/strategies/nice_queue.rb
152
157
  - lib/postburner/strategies/null_queue.rb
@@ -156,8 +161,7 @@ files:
156
161
  - lib/postburner/tracked.rb
157
162
  - lib/postburner/tube.rb
158
163
  - lib/postburner/version.rb
159
- - lib/postburner/workers/base.rb
160
- - lib/postburner/workers/worker.rb
164
+ - lib/postburner/worker.rb
161
165
  - lib/tasks/postburner.rake
162
166
  - lib/tasks/postburner_tasks.rake
163
167
  homepage: https://gitlab.nearapogee.com/opensource/postburner
@@ -1,205 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Postburner
4
- module Workers
5
- # Base worker class with shared functionality for all worker types.
6
- #
7
- # Provides common methods for signal handling, job execution, error handling,
8
- # and retry logic. Subclasses implement the specific execution strategy
9
- # (simple, forking, threads_on_fork).
10
- #
11
- class Base
12
- attr_reader :config, :logger
13
-
14
- # @param config [Postburner::Configuration] Worker configuration
15
- #
16
- def initialize(config)
17
- @config = config
18
- @logger = config.logger
19
- @shutdown = false
20
- setup_signal_handlers
21
- end
22
-
23
- # Starts the worker loop.
24
- #
25
- # Subclasses must implement this method to define their execution strategy.
26
- #
27
- # @return [void]
28
- #
29
- # @raise [NotImplementedError] if not implemented by subclass
30
- #
31
- def start
32
- raise NotImplementedError, "Subclasses must implement #start"
33
- end
34
-
35
- # Initiates graceful shutdown.
36
- #
37
- # Sets shutdown flag to stop processing new jobs. Current jobs
38
- # are allowed to finish.
39
- #
40
- # @return [void]
41
- #
42
- def shutdown
43
- @shutdown = true
44
- end
45
-
46
- # Checks if shutdown has been requested.
47
- #
48
- # @return [Boolean] true if shutdown requested, false otherwise
49
- #
50
- def shutdown?
51
- @shutdown
52
- end
53
-
54
- protected
55
-
56
- # Sets up signal handlers for graceful shutdown.
57
- #
58
- # TERM and INT signals trigger graceful shutdown.
59
- #
60
- # @return [void]
61
- #
62
- def setup_signal_handlers
63
- Signal.trap('TERM') { shutdown }
64
- Signal.trap('INT') { shutdown }
65
- end
66
-
67
- # Expands queue name to full tube name with environment prefix.
68
- #
69
- # Delegates to Postburner::Configuration#expand_tube_name.
70
- #
71
- # @param queue_name [String] Base queue name
72
- #
73
- # @return [String] Full tube name (e.g., 'postburner.production.critical')
74
- #
75
- def expand_tube_name(queue_name)
76
- config.expand_tube_name(queue_name)
77
- end
78
-
79
- # Executes a job from Beanstalkd.
80
- #
81
- # Delegates to Postburner::ActiveJob::Execution to handle default,
82
- # tracked, and legacy job formats.
83
- #
84
- # @param beanstalk_job [Beaneater::Job] Job from Beanstalkd
85
- #
86
- # @return [void]
87
- #
88
- def execute_job(beanstalk_job)
89
- logger.info "[Postburner] Executing #{beanstalk_job.class.name} #{beanstalk_job.id}"
90
- Postburner::ActiveJob::Execution.execute(beanstalk_job.body)
91
- logger.info "[Postburner] Deleting #{beanstalk_job.class.name} #{beanstalk_job.id} (success)"
92
- beanstalk_job.delete
93
- rescue => e
94
- handle_error(beanstalk_job, e)
95
- end
96
-
97
- # Handles job execution errors with retry logic.
98
- #
99
- # Implements retry strategy:
100
- # - Parses payload to determine job type
101
- # - For default jobs: manages retry count, re-queues with backoff
102
- # - For tracked jobs: buries job (Postburner::Job handles retries)
103
- # - For legacy jobs: buries job
104
- #
105
- # @param beanstalk_job [Beaneater::Job] Job from Beanstalkd
106
- # @param error [Exception] The exception that was raised
107
- #
108
- # @return [void]
109
- #
110
- def handle_error(beanstalk_job, error)
111
- logger.error "[Postburner] Job failed: #{error.class} - #{error.message}"
112
- logger.error error.backtrace.join("\n")
113
-
114
- begin
115
- payload = JSON.parse(beanstalk_job.body)
116
-
117
- if payload['tracked'] || Postburner::ActiveJob::Payload.legacy_format?(payload)
118
- # Tracked and legacy jobs: bury for inspection
119
- # (Postburner::Job has its own retry logic)
120
- logger.info "[Postburner] Burying tracked/legacy job for inspection"
121
- beanstalk_job.bury
122
- else
123
- # Default job: handle retry logic
124
- handle_default_retry(beanstalk_job, payload, error)
125
- end
126
- rescue => retry_error
127
- logger.error "[Postburner] Error handling failure: #{retry_error.message}"
128
- beanstalk_job.bury rescue nil
129
- end
130
- end
131
-
132
- # Handles retry logic for default jobs.
133
- #
134
- # Checks ActiveJob's retry_on configuration, increments retry count,
135
- # and re-queues with exponential backoff if retries remaining.
136
- #
137
- # @param beanstalk_job [Beaneater::Job] Job from Beanstalkd
138
- # @param payload [Hash] Parsed job payload
139
- # @param error [Exception] The exception that was raised
140
- #
141
- # @return [void]
142
- #
143
- def handle_default_retry(beanstalk_job, payload, error)
144
- retry_count = payload['retry_count'] || 0
145
- job_class = payload['job_class'].constantize
146
-
147
- # Check if job class wants to retry this error
148
- # (This is simplified - full implementation would check retry_on config)
149
- max_retries = 5 # Default max retries
150
-
151
- if retry_count < max_retries
152
- # Increment retry count
153
- payload['retry_count'] = retry_count + 1
154
- payload['executions'] = (payload['executions'] || 0) + 1
155
-
156
- # Calculate backoff delay (exponential: 1s, 2s, 4s, 8s, 16s...)
157
- delay = calculate_backoff(retry_count)
158
-
159
- # Delete old job and insert new one with updated payload
160
- beanstalk_job.delete
161
-
162
- Postburner.connected do |conn|
163
- tube_name = expand_tube_name(payload['queue_name'])
164
- conn.tubes[tube_name].put(
165
- JSON.generate(payload),
166
- pri: payload['priority'] || 0,
167
- delay: delay,
168
- ttr: 120
169
- )
170
- end
171
-
172
- logger.info "[Postburner] Retrying default job #{payload['job_id']}, attempt #{retry_count + 1} in #{delay}s"
173
- else
174
- # Max retries exceeded
175
- logger.error "[Postburner] Discarding default job #{payload['job_id']} after #{retry_count} retries"
176
- beanstalk_job.delete
177
- # TODO: Call after_discard callback if configured
178
- end
179
- end
180
-
181
- # Calculates exponential backoff delay for retries.
182
- #
183
- # @param retry_count [Integer] Number of retries so far
184
- #
185
- # @return [Integer] Delay in seconds (capped at 1 hour)
186
- #
187
- def calculate_backoff(retry_count)
188
- # Exponential backoff: 2^retry_count, capped at 3600 seconds (1 hour)
189
- [2 ** retry_count, 3600].min
190
- end
191
-
192
- # Watches all configured queues in Beanstalkd.
193
- #
194
- # @param connection [Postburner::Connection] Beanstalkd connection
195
- #
196
- # @return [void]
197
- #
198
- def watch_queues(connection, queue_names=nil)
199
- tube_names = queue_names.map { |q| config.expand_tube_name(q) }
200
- tube_names = config.expanded_tube_names if tube_names.empty?
201
- connection.beanstalk.tubes.watch!(*tube_names)
202
- end
203
- end
204
- end
205
- end