postburner 1.0.0.pre.3 → 1.0.0.pre.5

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.
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Concern providing queue insertion methods for Postburner jobs.
5
+ #
6
+ # Handles enqueuing jobs to Beanstalkd (or test strategies), including
7
+ # scheduling, re-queueing, and the internal insertion mechanics via
8
+ # after_save_commit callbacks.
9
+ #
10
+ # @example Basic queueing
11
+ # class MyJob < Postburner::Job
12
+ # def perform(args)
13
+ # # ... work ...
14
+ # end
15
+ # end
16
+ #
17
+ # job = MyJob.create!(args: { foo: 'bar' })
18
+ # job.queue!
19
+ #
20
+ # @example Scheduled queueing
21
+ # job.queue!(delay: 1.hour)
22
+ # job.queue!(at: 2.days.from_now)
23
+ #
24
+ module Insertion
25
+ extend ActiveSupport::Concern
26
+
27
+ included do
28
+ after_save_commit :insert_if_queued!
29
+ end
30
+
31
+ # Enqueues the job to Beanstalkd for processing.
32
+ #
33
+ # Sets queued_at timestamp and optionally run_at for scheduled execution.
34
+ # Triggers enqueue callbacks and inserts job into Beanstalkd via after_save_commit hook.
35
+ # In test mode, executes immediately instead of queueing to Beanstalkd.
36
+ #
37
+ # @param options [Hash] Queue options
38
+ # @option options [Time, ActiveSupport::Duration] :at Absolute time to run the job
39
+ # @option options [Integer, ActiveSupport::Duration] :delay Seconds to delay execution
40
+ # @option options [Integer] :pri Beanstalkd priority (lower = higher priority)
41
+ # @option options [Integer] :ttr Time-to-run in seconds before job times out
42
+ #
43
+ # @return [void]
44
+ #
45
+ # @raise [ActiveRecord::RecordInvalid] if job is not valid
46
+ # @raise [AlreadyProcessed] if job was already processed
47
+ #
48
+ # @example Queue immediately
49
+ # job.queue!
50
+ #
51
+ # @example Queue with delay
52
+ # job.queue!(delay: 1.hour)
53
+ #
54
+ # @example Queue at specific time
55
+ # job.queue!(at: Time.zone.now + 2.days)
56
+ #
57
+ # @example Queue with priority
58
+ # job.queue!(pri: 0, delay: 30.minutes)
59
+ #
60
+ # @see #requeue!
61
+ # @see Postburner.queue_strategy
62
+ #
63
+ def queue!(options={})
64
+ return if self.queued_at.present? && self.bkid.present?
65
+ raise ActiveRecord::RecordInvalid, "Can't queue unless valid." unless self.valid?
66
+ raise AlreadyProcessed, "Processed at #{self.processed_at}" if self.processed_at
67
+
68
+ at = options.delete(:at)
69
+ now = Time.zone.now
70
+
71
+ self.queued_at = now
72
+ self.run_at = case
73
+ when at.present?
74
+ # this is rudimentary, add error handling
75
+ options[:delay] ||= at.to_i - now.to_i
76
+ at
77
+ when options[:delay].present?
78
+ now + options[:delay].seconds
79
+ end
80
+
81
+ @_insert_options = options
82
+
83
+ run_callbacks :enqueue do
84
+ self.save!
85
+ end
86
+ end
87
+
88
+ # Re-queues an existing job by removing it from Beanstalkd and queueing again.
89
+ #
90
+ # Calls {#delete!} to remove from Beanstalkd, resets queuing metadata,
91
+ # then calls {#queue!} with new options.
92
+ #
93
+ # @param options [Hash] Queue options (same as {#queue!})
94
+ # @option options [Time, ActiveSupport::Duration] :at Absolute time to run the job
95
+ # @option options [Integer, ActiveSupport::Duration] :delay Seconds to delay execution
96
+ # @option options [Integer] :pri Beanstalkd priority
97
+ # @option options [Integer] :ttr Time-to-run in seconds
98
+ #
99
+ # @return [void]
100
+ #
101
+ # @raise [ActiveRecord::RecordInvalid] if job is not valid
102
+ # @raise [Beaneater::NotConnected] if Beanstalkd connection fails
103
+ #
104
+ # @example Requeue with different delay
105
+ # job.requeue!(delay: 5.minutes)
106
+ #
107
+ # @see #queue!
108
+ # @see #delete!
109
+ #
110
+ def requeue!(options={})
111
+ self.delete!
112
+ self.bkid, self.queued_at = nil, nil
113
+
114
+ self.queue! options
115
+ end
116
+
117
+ # Checks if job is flagged for insertion into Beanstalkd.
118
+ #
119
+ # Set internally by {#queue!} to trigger insertion via after_save_commit hook.
120
+ #
121
+ # @return [Boolean] true if job will be inserted on save
122
+ # @api private
123
+ #
124
+ def will_insert?
125
+ @_insert_options.is_a? Hash
126
+ end
127
+
128
+ private
129
+
130
+ # After-save-commit callback that inserts job into queue if flagged.
131
+ #
132
+ # Checks {#will_insert?} and calls {#insert!} if true. Triggered by
133
+ # after_save_commit callback after {#queue!} saves the record.
134
+ #
135
+ # @return [void]
136
+ #
137
+ # @api private
138
+ #
139
+ def insert_if_queued!
140
+ #debugger
141
+ return unless self.will_insert?
142
+ insert!(@_insert_options)
143
+ end
144
+
145
+ # Inserts job into queue via current queue strategy.
146
+ #
147
+ # Delegates to Postburner.queue_strategy.insert which handles actual
148
+ # queueing (Beanstalkd in production, inline execution in test mode).
149
+ # Updates bkid if the strategy returns a Beanstalkd job ID.
150
+ #
151
+ # @param options [Hash] Queue options (delay, pri, ttr, etc.)
152
+ #
153
+ # @return [Hash, nil] Queue strategy response
154
+ #
155
+ # @api private
156
+ #
157
+ def insert!(options={})
158
+ response = Postburner.queue_strategy.insert(self, options)
159
+ #debugger
160
+
161
+ # Response must be a hash with an :id key (value can be nil)
162
+ # Backburner returns symbol keys
163
+ unless response.is_a?(Hash) && response.key?(:id)
164
+ raise MalformedResponse, "Missing :id key in response: #{response.inspect}"
165
+ end
166
+
167
+ persist_metadata!(bkid: response[:id])
168
+
169
+ self.log("QUEUED: #{response}") if response
170
+
171
+ response
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Concern providing logging and exception tracking for Postburner jobs.
5
+ #
6
+ # Provides methods for adding timestamped log entries and tracking exceptions
7
+ # in the job's PostgreSQL audit trail. All logs are stored in JSONB arrays
8
+ # with timestamps, levels, and elapsed time tracking.
9
+ #
10
+ # @example Basic logging
11
+ # class MyJob < Postburner::Job
12
+ # def perform(args)
13
+ # log "Starting processing"
14
+ # # ... work ...
15
+ # log! "Critical checkpoint reached"
16
+ # end
17
+ # end
18
+ #
19
+ # @example Exception tracking
20
+ # class MyJob < Postburner::Job
21
+ # def perform(args)
22
+ # process_data(args)
23
+ # rescue => e
24
+ # log_exception!(e)
25
+ # raise
26
+ # end
27
+ # end
28
+ #
29
+ module Logging
30
+ extend ActiveSupport::Concern
31
+
32
+ included do
33
+ # Valid log levels in order of severity
34
+ LOG_LEVELS = [
35
+ :debug,
36
+ :info,
37
+ :warning,
38
+ :error
39
+ ].freeze
40
+ end
41
+
42
+ # Tracks an exception in the job's errata array.
43
+ #
44
+ # Appends exception details to the in-memory errata array with timestamp,
45
+ # class, message, and backtrace. Does NOT persist to database immediately.
46
+ # Use {#log_exception!} to persist immediately.
47
+ #
48
+ # @param exception [Exception] The exception to track
49
+ #
50
+ # @return [Array<Array>] Updated errata array
51
+ #
52
+ # @example
53
+ # begin
54
+ # # ... risky operation ...
55
+ # rescue => e
56
+ # log_exception(e)
57
+ # raise
58
+ # end
59
+ #
60
+ # @see #log_exception!
61
+ #
62
+ def log_exception(exception)
63
+ self.errata << [
64
+ Time.zone.now,
65
+ {
66
+ bkid: self.bkid,
67
+ class: exception.class,
68
+ message: exception.message,
69
+ backtrace: exception.backtrace,
70
+ }
71
+ ]
72
+ end
73
+
74
+ # Tracks an exception and immediately persists to database.
75
+ #
76
+ # Calls {#log_exception} to append exception details, then persists
77
+ # both errata and error_count to database via {#persist_metadata!}.
78
+ #
79
+ # @param exception [Exception] The exception to track
80
+ #
81
+ # @return [void]
82
+ #
83
+ # @example
84
+ # begin
85
+ # # ... risky operation ...
86
+ # rescue => e
87
+ # log_exception!(e)
88
+ # raise
89
+ # end
90
+ #
91
+ # @see #log_exception
92
+ #
93
+ def log_exception!(exception)
94
+ self.log_exception(exception)
95
+ self.persist_metadata!
96
+ end
97
+
98
+ # Appends a log message to the job's logs array.
99
+ #
100
+ # Adds timestamped log entry to in-memory logs array with bkid, level,
101
+ # message, and elapsed time. Does NOT persist to database immediately.
102
+ # Use {#log!} to persist immediately.
103
+ #
104
+ # @param message [String] Log message to append
105
+ # @param options [Hash] Log options
106
+ # @option options [Symbol] :level Log level (:debug, :info, :warning, :error) - defaults to :info
107
+ #
108
+ # @return [Array<Array>] Updated logs array
109
+ #
110
+ # @note Invalid log levels are coerced to :error
111
+ #
112
+ # @example
113
+ # log "Processing started"
114
+ # log "Warning: rate limit approaching", level: :warning
115
+ # log "Critical error occurred", level: :error
116
+ #
117
+ # @see #log!
118
+ # @see #elapsed_ms
119
+ # @see LOG_LEVELS
120
+ #
121
+ def log(message, options={})
122
+ options[:level] ||= :info
123
+ options[:level] = :error unless LOG_LEVELS.member?(options[:level])
124
+
125
+ self.logs << [
126
+ Time.zone.now, # time
127
+ {
128
+ bkid: self.bkid,
129
+ level: options[:level], # level
130
+ message: message, # message
131
+ elapsed: self.elapsed_ms, # ms from start
132
+ }
133
+ ]
134
+ end
135
+
136
+ # Appends a log message and immediately persists to database.
137
+ #
138
+ # Calls {#log} to append message, then persists logs array to database.
139
+ # Use this for important log messages that should be saved immediately.
140
+ #
141
+ # @param message [String] Log message to append
142
+ # @param options [Hash] Log options
143
+ # @option options [Symbol] :level Log level (:debug, :info, :warning, :error)
144
+ #
145
+ # @return [void]
146
+ #
147
+ # @example
148
+ # log! "Job started"
149
+ # log! "Payment processed successfully", level: :info
150
+ # log! "Retrying failed request", level: :warning
151
+ #
152
+ # @see #log
153
+ #
154
+ def log!(message, options={})
155
+ self.log(message, options)
156
+ self.update_column :logs, self.logs
157
+ end
158
+
159
+ private
160
+
161
+ # Persists job metadata (errata, logs, and counts) to database.
162
+ #
163
+ # Updates errata, error_count, logs, and log_count columns, plus any
164
+ # additional data passed in. Used internally for atomic metadata updates.
165
+ #
166
+ # @param data [Hash] Additional columns to update
167
+ #
168
+ # @return [void]
169
+ #
170
+ # @api private
171
+ #
172
+ def persist_metadata!(data={})
173
+ self.update_columns({
174
+ errata: self.errata,
175
+ error_count: self.errata.length,
176
+ logs: self.logs,
177
+ log_count: self.logs.length,
178
+ }.merge(data))
179
+ end
180
+ end
181
+ end
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Postburner
4
- # Queue configuration module for Postburner::Job classes.
4
+ # Queue properties module for Postburner::Job classes.
5
5
  #
6
6
  # Provides DSL methods for configuring queue behavior (name, priority, TTR, retries).
7
- # Replaces Backburner::Queue with cleaner implementation that doesn't interfere
8
- # with ActiveSupport::Callbacks.
7
+ # Defines configurable properties for job queue management.
9
8
  #
10
9
  # @example Basic usage
11
10
  # class ProcessPayment < Postburner::Job
@@ -19,10 +18,13 @@ module Postburner
19
18
  # end
20
19
  # end
21
20
  #
22
- module QueueConfig
21
+ module Properties
23
22
  extend ActiveSupport::Concern
24
23
 
25
24
  included do
25
+ # Instance-level queue configuration (overrides class-level defaults)
26
+ attr_writer :priority, :ttr
27
+
26
28
  class_attribute :postburner_queue_name, default: 'default'
27
29
  class_attribute :postburner_priority, default: nil
28
30
  class_attribute :postburner_ttr, default: nil
@@ -150,5 +152,70 @@ module Postburner
150
152
  end
151
153
  end
152
154
  end
155
+
156
+ # Returns the queue name for this job instance.
157
+ #
158
+ # Checks instance-level override first, then falls back to class-level configuration.
159
+ #
160
+ # @return [String] Queue name
161
+ #
162
+ # @example Class-level configuration
163
+ # class MyJob < Postburner::Job
164
+ # queue 'critical'
165
+ # end
166
+ # job = MyJob.create!(args: {})
167
+ # job.queue_name # => 'critical'
168
+ #
169
+ def queue_name
170
+ self.class.queue
171
+ end
172
+
173
+ # Returns the full tube name with environment prefix.
174
+ #
175
+ # Expands the queue name to include the environment prefix
176
+ # (e.g., 'critical' becomes 'postburner.development.critical').
177
+ #
178
+ # @return [String] Full tube name with environment prefix
179
+ #
180
+ # @example
181
+ # job.expanded_tube_name # => 'postburner.development.critical'
182
+ #
183
+ def expanded_tube_name
184
+ Postburner.configuration.expand_tube_name(queue_name)
185
+ end
186
+
187
+ # Returns the priority for this job instance.
188
+ #
189
+ # Checks instance-level override first, then falls back to class-level configuration.
190
+ #
191
+ # @return [Integer, nil] Priority (lower = higher priority)
192
+ #
193
+ # @example Instance-level override
194
+ # job = MyJob.create!(args: {}, priority: 1500)
195
+ # job.priority # => 1500
196
+ #
197
+ def priority
198
+ @priority || self.class.priority
199
+ end
200
+
201
+ # Alias for {#priority}.
202
+ #
203
+ # @see #priority
204
+ alias_method :pri, :priority
205
+
206
+ # Returns the TTR (time-to-run) for this job instance.
207
+ #
208
+ # Checks instance-level override first, then falls back to class-level configuration.
209
+ #
210
+ # @return [Integer, nil] TTR in seconds
211
+ #
212
+ # @example Instance-level override
213
+ # job = MyJob.create!(args: {}, ttr: 600)
214
+ # job.ttr # => 600
215
+ #
216
+ def ttr
217
+ @ttr || self.class.ttr
218
+ end
219
+
153
220
  end
154
221
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Concern providing statistics and metrics methods for Postburner jobs.
5
+ #
6
+ # Provides methods for calculating timing metrics, retrieving job statistics,
7
+ # and determining intended execution times for scheduled jobs.
8
+ #
9
+ # @example Get job statistics
10
+ # job.stats
11
+ # # => { id: 5, bkid: 1, queue: "default", ... }
12
+ #
13
+ # @example Check elapsed time
14
+ # job.elapsed_ms # => 1234.567
15
+ #
16
+ module Statistics
17
+ extend ActiveSupport::Concern
18
+
19
+ # Returns job statistics including Beanstalkd job state.
20
+ #
21
+ # Fetches current job state from Beanstalkd and returns combined statistics
22
+ # about the job's PostgreSQL record and its current Beanstalkd status.
23
+ #
24
+ # @return [Hash] Statistics hash with the following keys:
25
+ # - id: PostgreSQL job ID
26
+ # - bkid: Beanstalkd job ID
27
+ # - queue: Queue name configured for this job class (e.g., 'sleep-jobs')
28
+ # - tube: Derived tube name with environment prefix (e.g., 'postburner.development.sleep-jobs')
29
+ # - watched: Boolean indicating if tube is in configured watch list
30
+ # - beanstalk: Hash of Beanstalkd job statistics:
31
+ # - id: Beanstalkd job ID
32
+ # - tube: Tube name where job resides
33
+ # - state: Job state (ready, reserved, delayed, buried)
34
+ # - pri: Priority (0 = highest, 4294967295 = lowest)
35
+ # - age: Seconds since job was created
36
+ # - delay: Seconds remaining before job becomes ready (0 if ready now)
37
+ # - ttr: Time-to-run in seconds (time allowed for job processing)
38
+ # - time_left: Seconds remaining before job times out (0 if not reserved)
39
+ # - file: Binlog file number containing job
40
+ # - reserves: Number of times job has been reserved
41
+ # - timeouts: Number of times job has timed out during processing
42
+ # - releases: Number of times job has been released back to ready
43
+ # - buries: Number of times job has been buried
44
+ # - kicks: Number of times job has been kicked from buried/delayed
45
+ #
46
+ # @raise [Beaneater::NotFoundError] if job no longer exists in Beanstalkd
47
+ #
48
+ # @example
49
+ # job.stats
50
+ # # => {
51
+ # # id: 5,
52
+ # # bkid: 1,
53
+ # # queue: "sleep-jobs",
54
+ # # tube: "postburner.development.sleep-jobs",
55
+ # # watched: false,
56
+ # # beanstalk: {
57
+ # # id: 1,
58
+ # # tube: "postburner.development.sleep-jobs",
59
+ # # state: "ready",
60
+ # # pri: 50,
61
+ # # age: 1391,
62
+ # # delay: 0,
63
+ # # ttr: 120,
64
+ # # time_left: 0,
65
+ # # file: 0,
66
+ # # reserves: 0,
67
+ # # timeouts: 0,
68
+ # # releases: 0,
69
+ # # buries: 0,
70
+ # # kicks: 0
71
+ # # }
72
+ # # }
73
+ #
74
+ def stats
75
+ # Get configured watched tubes (expanded with environment prefix)
76
+ watched = Postburner.watched_tube_names.include?(self.expanded_tube_name)
77
+
78
+ {
79
+ id: self.id,
80
+ bkid: self.bkid,
81
+ queue: queue_name,
82
+ tube: expanded_tube_name,
83
+ watched: watched,
84
+ beanstalk: self.bk.stats.to_h.symbolize_keys,
85
+ }
86
+ end
87
+
88
+ # Returns elapsed time in milliseconds since job execution started.
89
+ #
90
+ # Calculates milliseconds between attempting_at and current time.
91
+ # Used to track how long a job has been executing.
92
+ #
93
+ # @return [Float, nil] Milliseconds since attempting_at, or nil if not yet attempting
94
+ #
95
+ # @example
96
+ # elapsed_ms # => 1234.567 (job has been running for ~1.2 seconds)
97
+ #
98
+ # @see #log
99
+ #
100
+ def elapsed_ms
101
+ return unless self.attempting_at
102
+ (Time.zone.now - self.attempting_at) * 1000
103
+ end
104
+
105
+ # Returns the intended execution time for the job.
106
+ #
107
+ # Returns run_at if scheduled, otherwise returns queued_at.
108
+ # Used for calculating lag (delay between intended and actual execution).
109
+ #
110
+ # @return [Time] Intended execution timestamp
111
+ #
112
+ # @example
113
+ # job.queue!(delay: 1.hour)
114
+ # job.intended_at # => 1 hour from now (run_at)
115
+ #
116
+ # job.queue!
117
+ # job.intended_at # => now (queued_at)
118
+ #
119
+ # @see #lag
120
+ #
121
+ def intended_at
122
+ self.run_at ? self.run_at : self.queued_at
123
+ end
124
+ end
125
+ end