postburner 1.0.0.pre.3 → 1.0.0.pre.4

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,170 @@
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
+ # Enqueues the job to Beanstalkd for processing.
28
+ #
29
+ # Sets queued_at timestamp and optionally run_at for scheduled execution.
30
+ # Triggers enqueue callbacks and inserts job into Beanstalkd via after_save_commit hook.
31
+ # In test mode, executes immediately instead of queueing to Beanstalkd.
32
+ #
33
+ # @param options [Hash] Queue options
34
+ # @option options [Time, ActiveSupport::Duration] :at Absolute time to run the job
35
+ # @option options [Integer, ActiveSupport::Duration] :delay Seconds to delay execution
36
+ # @option options [Integer] :pri Beanstalkd priority (lower = higher priority)
37
+ # @option options [Integer] :ttr Time-to-run in seconds before job times out
38
+ #
39
+ # @return [void]
40
+ #
41
+ # @raise [ActiveRecord::RecordInvalid] if job is not valid
42
+ # @raise [AlreadyProcessed] if job was already processed
43
+ #
44
+ # @example Queue immediately
45
+ # job.queue!
46
+ #
47
+ # @example Queue with delay
48
+ # job.queue!(delay: 1.hour)
49
+ #
50
+ # @example Queue at specific time
51
+ # job.queue!(at: Time.zone.now + 2.days)
52
+ #
53
+ # @example Queue with priority
54
+ # job.queue!(pri: 0, delay: 30.minutes)
55
+ #
56
+ # @see #requeue!
57
+ # @see Postburner.queue_strategy
58
+ #
59
+ def queue!(options={})
60
+ return if self.queued_at.present? && self.bkid.present?
61
+ raise ActiveRecord::RecordInvalid, "Can't queue unless valid." unless self.valid?
62
+ raise AlreadyProcessed, "Processed at #{self.processed_at}" if self.processed_at
63
+
64
+ at = options.delete(:at)
65
+ now = Time.zone.now
66
+
67
+ self.queued_at = now
68
+ self.run_at = case
69
+ when at.present?
70
+ # this is rudimentary, add error handling
71
+ options[:delay] ||= at.to_i - now.to_i
72
+ at
73
+ when options[:delay].present?
74
+ now + options[:delay].seconds
75
+ end
76
+
77
+ @_insert_options = options
78
+
79
+ run_callbacks :enqueue do
80
+ self.save!
81
+ end
82
+ end
83
+
84
+ # Re-queues an existing job by removing it from Beanstalkd and queueing again.
85
+ #
86
+ # Calls {#delete!} to remove from Beanstalkd, resets queuing metadata,
87
+ # then calls {#queue!} with new options.
88
+ #
89
+ # @param options [Hash] Queue options (same as {#queue!})
90
+ # @option options [Time, ActiveSupport::Duration] :at Absolute time to run the job
91
+ # @option options [Integer, ActiveSupport::Duration] :delay Seconds to delay execution
92
+ # @option options [Integer] :pri Beanstalkd priority
93
+ # @option options [Integer] :ttr Time-to-run in seconds
94
+ #
95
+ # @return [void]
96
+ #
97
+ # @raise [ActiveRecord::RecordInvalid] if job is not valid
98
+ # @raise [Beaneater::NotConnected] if Beanstalkd connection fails
99
+ #
100
+ # @example Requeue with different delay
101
+ # job.requeue!(delay: 5.minutes)
102
+ #
103
+ # @see #queue!
104
+ # @see #delete!
105
+ #
106
+ def requeue!(options={})
107
+ self.delete!
108
+ self.bkid, self.queued_at = nil, nil
109
+
110
+ self.queue! options
111
+ end
112
+
113
+ # Checks if job is flagged for insertion into Beanstalkd.
114
+ #
115
+ # Set internally by {#queue!} to trigger insertion via after_save_commit hook.
116
+ #
117
+ # @return [Boolean] true if job will be inserted on save
118
+ # @api private
119
+ #
120
+ def will_insert?
121
+ @_insert_options.is_a? Hash
122
+ end
123
+
124
+ private
125
+
126
+ # After-save-commit callback that inserts job into queue if flagged.
127
+ #
128
+ # Checks {#will_insert?} and calls {#insert!} if true. Triggered by
129
+ # after_save_commit callback after {#queue!} saves the record.
130
+ #
131
+ # @return [void]
132
+ #
133
+ # @api private
134
+ #
135
+ def insert_if_queued!
136
+ #debugger
137
+ return unless self.will_insert?
138
+ insert!(@_insert_options)
139
+ end
140
+
141
+ # Inserts job into queue via current queue strategy.
142
+ #
143
+ # Delegates to Postburner.queue_strategy.insert which handles actual
144
+ # queueing (Beanstalkd in production, inline execution in test mode).
145
+ # Updates bkid if the strategy returns a Beanstalkd job ID.
146
+ #
147
+ # @param options [Hash] Queue options (delay, pri, ttr, etc.)
148
+ #
149
+ # @return [Hash, nil] Queue strategy response
150
+ #
151
+ # @api private
152
+ #
153
+ def insert!(options={})
154
+ response = Postburner.queue_strategy.insert(self, options)
155
+ #debugger
156
+
157
+ # Response must be a hash with an :id key (value can be nil)
158
+ # Backburner returns symbol keys
159
+ unless response.is_a?(Hash) && response.key?(:id)
160
+ raise MalformedResponse, "Missing :id key in response: #{response.inspect}"
161
+ end
162
+
163
+ persist_metadata!(bkid: response[:id])
164
+
165
+ self.log("QUEUED: #{response}") if response
166
+
167
+ response
168
+ end
169
+ end
170
+ 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,64 @@ 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 priority for this job instance.
174
+ #
175
+ # Checks instance-level override first, then falls back to class-level configuration.
176
+ #
177
+ # @return [Integer, nil] Priority (lower = higher priority)
178
+ #
179
+ # @example Instance-level override
180
+ # job = MyJob.create!(args: {}, priority: 1500)
181
+ # job.priority # => 1500
182
+ #
183
+ def priority
184
+ @priority || self.class.priority
185
+ end
186
+
187
+ # Returns the TTR (time-to-run) for this job instance.
188
+ #
189
+ # Checks instance-level override first, then falls back to class-level configuration.
190
+ #
191
+ # @return [Integer, nil] TTR in seconds
192
+ #
193
+ # @example Instance-level override
194
+ # job = MyJob.create!(args: {}, ttr: 600)
195
+ # job.ttr # => 600
196
+ #
197
+ def ttr
198
+ @ttr || self.class.ttr
199
+ end
200
+
201
+ # Returns the full tube name with environment prefix.
202
+ #
203
+ # Expands the queue name to include the environment prefix
204
+ # (e.g., 'critical' becomes 'postburner.development.critical').
205
+ #
206
+ # @return [String] Full tube name with environment prefix
207
+ #
208
+ # @example
209
+ # job.expanded_tube_name # => 'postburner.development.critical'
210
+ #
211
+ def expanded_tube_name
212
+ Postburner.configuration.expand_tube_name(queue_name)
213
+ end
153
214
  end
154
215
  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