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.
- checksums.yaml +4 -4
- data/README.md +111 -67
- data/app/concerns/postburner/commands.rb +143 -0
- data/app/concerns/postburner/execution.rb +190 -0
- data/app/concerns/postburner/insertion.rb +174 -0
- data/app/concerns/postburner/logging.rb +181 -0
- data/{lib/postburner/queue_config.rb → app/concerns/postburner/properties.rb} +71 -4
- data/app/concerns/postburner/statistics.rb +125 -0
- data/app/models/postburner/job.rb +40 -749
- data/app/views/postburner/jobs/show.html.haml +2 -2
- data/bin/postburner +3 -34
- data/lib/postburner/runner.rb +126 -0
- data/lib/postburner/strategies/queue.rb +15 -8
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner.rb +1 -2
- data/lib/tasks/postburner.rake +18 -0
- metadata +9 -3
- data/MIT-LICENSE +0 -20
|
@@ -71,22 +71,16 @@ module Postburner
|
|
|
71
71
|
# @attr_reader [Array<Hash>] logs Array of log entries with timestamps
|
|
72
72
|
#
|
|
73
73
|
class Job < ApplicationRecord
|
|
74
|
-
include
|
|
74
|
+
include Properties
|
|
75
75
|
include Callbacks
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
:debug,
|
|
82
|
-
:info,
|
|
83
|
-
:warning,
|
|
84
|
-
:error
|
|
85
|
-
].freeze
|
|
76
|
+
include Logging
|
|
77
|
+
include Commands
|
|
78
|
+
include Insertion
|
|
79
|
+
include Execution
|
|
80
|
+
include Statistics
|
|
86
81
|
|
|
87
82
|
before_validation :ensure_sid!
|
|
88
83
|
before_destroy :delete!
|
|
89
|
-
after_save_commit :insert_if_queued!
|
|
90
84
|
|
|
91
85
|
validates :sid, presence: {strict: true}
|
|
92
86
|
|
|
@@ -149,341 +143,6 @@ module Postburner
|
|
|
149
143
|
# @see #remove!
|
|
150
144
|
# @note This is a standard Rails method with Beanstalkd integration via callback
|
|
151
145
|
|
|
152
|
-
# Enqueues the job to Beanstalkd for processing.
|
|
153
|
-
#
|
|
154
|
-
# Sets queued_at timestamp and optionally run_at for scheduled execution.
|
|
155
|
-
# Triggers enqueue callbacks and inserts job into Beanstalkd via after_save_commit hook.
|
|
156
|
-
# In test mode, executes immediately instead of queueing to Beanstalkd.
|
|
157
|
-
#
|
|
158
|
-
# @param options [Hash] Queue options
|
|
159
|
-
# @option options [Time, ActiveSupport::Duration] :at Absolute time to run the job
|
|
160
|
-
# @option options [Integer, ActiveSupport::Duration] :delay Seconds to delay execution
|
|
161
|
-
# @option options [Integer] :pri Beanstalkd priority (lower = higher priority)
|
|
162
|
-
# @option options [Integer] :ttr Time-to-run in seconds before job times out
|
|
163
|
-
#
|
|
164
|
-
# @return [void]
|
|
165
|
-
#
|
|
166
|
-
# @raise [ActiveRecord::RecordInvalid] if job is not valid
|
|
167
|
-
# @raise [AlreadyProcessed] if job was already processed
|
|
168
|
-
#
|
|
169
|
-
# @example Queue immediately
|
|
170
|
-
# job.queue!
|
|
171
|
-
#
|
|
172
|
-
# @example Queue with delay
|
|
173
|
-
# job.queue!(delay: 1.hour)
|
|
174
|
-
#
|
|
175
|
-
# @example Queue at specific time
|
|
176
|
-
# job.queue!(at: Time.zone.now + 2.days)
|
|
177
|
-
#
|
|
178
|
-
# @example Queue with priority
|
|
179
|
-
# job.queue!(pri: 0, delay: 30.minutes)
|
|
180
|
-
#
|
|
181
|
-
# @see #requeue!
|
|
182
|
-
# @see Postburner.queue_strategy
|
|
183
|
-
#
|
|
184
|
-
def queue!(options={})
|
|
185
|
-
return if self.queued_at.present? && self.bkid.present?
|
|
186
|
-
raise ActiveRecord::RecordInvalid, "Can't queue unless valid." unless self.valid?
|
|
187
|
-
raise AlreadyProcessed, "Processed at #{self.processed_at}" if self.processed_at
|
|
188
|
-
|
|
189
|
-
at = options.delete(:at)
|
|
190
|
-
now = Time.zone.now
|
|
191
|
-
|
|
192
|
-
self.queued_at = now
|
|
193
|
-
self.run_at = case
|
|
194
|
-
when at.present?
|
|
195
|
-
# this is rudimentary, add error handling
|
|
196
|
-
options[:delay] ||= at.to_i - now.to_i
|
|
197
|
-
at
|
|
198
|
-
when options[:delay].present?
|
|
199
|
-
now + options[:delay].seconds
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
@_insert_options = options
|
|
203
|
-
|
|
204
|
-
run_callbacks :enqueue do
|
|
205
|
-
self.save!
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Re-queues an existing job by removing it from Beanstalkd and queueing again.
|
|
210
|
-
#
|
|
211
|
-
# Calls {#delete!} to remove from Beanstalkd, resets queuing metadata,
|
|
212
|
-
# then calls {#queue!} with new options.
|
|
213
|
-
#
|
|
214
|
-
# @param options [Hash] Queue options (same as {#queue!})
|
|
215
|
-
# @option options [Time, ActiveSupport::Duration] :at Absolute time to run the job
|
|
216
|
-
# @option options [Integer, ActiveSupport::Duration] :delay Seconds to delay execution
|
|
217
|
-
# @option options [Integer] :pri Beanstalkd priority
|
|
218
|
-
# @option options [Integer] :ttr Time-to-run in seconds
|
|
219
|
-
#
|
|
220
|
-
# @return [void]
|
|
221
|
-
#
|
|
222
|
-
# @raise [ActiveRecord::RecordInvalid] if job is not valid
|
|
223
|
-
# @raise [Beaneater::NotConnected] if Beanstalkd connection fails
|
|
224
|
-
#
|
|
225
|
-
# @example Requeue with different delay
|
|
226
|
-
# job.requeue!(delay: 5.minutes)
|
|
227
|
-
#
|
|
228
|
-
# @see #queue!
|
|
229
|
-
# @see #delete!
|
|
230
|
-
#
|
|
231
|
-
def requeue!(options={})
|
|
232
|
-
self.delete!
|
|
233
|
-
self.bkid, self.queued_at = nil, nil
|
|
234
|
-
|
|
235
|
-
self.queue! options
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# Checks if job is flagged for insertion into Beanstalkd.
|
|
239
|
-
#
|
|
240
|
-
# Set internally by {#queue!} to trigger insertion via after_save_commit hook.
|
|
241
|
-
#
|
|
242
|
-
# @return [Boolean] true if job will be inserted on save
|
|
243
|
-
# @api private
|
|
244
|
-
#
|
|
245
|
-
def will_insert?
|
|
246
|
-
@_insert_options.is_a? Hash
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Executes a job by ID, delegating to the current queue strategy.
|
|
250
|
-
#
|
|
251
|
-
# Loads the job from database by ID and delegates execution to the current
|
|
252
|
-
# queue strategy's {handle_perform!} method. This provides a unified API
|
|
253
|
-
# for job execution regardless of strategy (async, test, or null).
|
|
254
|
-
#
|
|
255
|
-
# Called automatically by Backburner workers in production. Can also be
|
|
256
|
-
# called manually for test/null strategies to trigger execution.
|
|
257
|
-
#
|
|
258
|
-
# @param id [Integer] Job ID to execute
|
|
259
|
-
# @param _ [Hash] Unused Backburner metadata parameter
|
|
260
|
-
#
|
|
261
|
-
# @return [void]
|
|
262
|
-
#
|
|
263
|
-
# @example Backburner automatic execution (production)
|
|
264
|
-
# # Jobs execute in tube: backburner.worker.queue.backburner-jobs
|
|
265
|
-
# # Backburner calls: Postburner::Job.perform(job_id)
|
|
266
|
-
#
|
|
267
|
-
# @example Manual execution with NullQueue
|
|
268
|
-
# Postburner.null_strategy!
|
|
269
|
-
# job = MyJob.create!(args: {})
|
|
270
|
-
# job.queue!(delay: 1.hour)
|
|
271
|
-
# Postburner::Job.perform(job.id) # Time travels and executes
|
|
272
|
-
#
|
|
273
|
-
# @note Strategy-aware: delegates to Postburner.queue_strategy.handle_perform!
|
|
274
|
-
# @note For NullQueue, automatically handles time travel for scheduled jobs
|
|
275
|
-
#
|
|
276
|
-
# @see #perform!
|
|
277
|
-
# @see Queue.handle_perform!
|
|
278
|
-
# @see NullQueue.handle_perform!
|
|
279
|
-
#
|
|
280
|
-
def self.perform(id, _={})
|
|
281
|
-
job = nil
|
|
282
|
-
begin
|
|
283
|
-
job = self.find(id)
|
|
284
|
-
rescue ActiveRecord::RecordNotFound => e
|
|
285
|
-
Rails.logger.warn <<-MSG
|
|
286
|
-
[Postburner::Job] [#{id}] Not Found.
|
|
287
|
-
MSG
|
|
288
|
-
end
|
|
289
|
-
#job.perform!(job.args)
|
|
290
|
-
Postburner.queue_strategy.handle_perform!(job)
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
# Executes the job with full lifecycle management and error handling.
|
|
294
|
-
#
|
|
295
|
-
# This is the main execution method called by Backburner workers or test strategies.
|
|
296
|
-
# Performs validation checks, executes callbacks, calls the subclass {#perform} method,
|
|
297
|
-
# tracks timing and statistics, and handles errors.
|
|
298
|
-
#
|
|
299
|
-
# Execution flow:
|
|
300
|
-
# 1. Runs attempt callbacks (fires on every retry)
|
|
301
|
-
# 2. Updates attempting metadata (attempting_at, attempts, lag)
|
|
302
|
-
# 3. Validates job state (queued, not processed, not removed, not premature)
|
|
303
|
-
# 4. Runs processing callbacks
|
|
304
|
-
# 5. Calls subclass {#perform} method
|
|
305
|
-
# 6. Runs processed callbacks (only on success)
|
|
306
|
-
# 7. Updates completion metadata (processed_at, duration)
|
|
307
|
-
# 8. Logs and tracks any exceptions
|
|
308
|
-
#
|
|
309
|
-
# @param args [Hash] Arguments to pass to {#perform} method
|
|
310
|
-
#
|
|
311
|
-
# @return [void]
|
|
312
|
-
#
|
|
313
|
-
# @raise [Exception] Any exception raised by {#perform} is logged and re-raised
|
|
314
|
-
#
|
|
315
|
-
# @note Does not execute if queued_at is nil, in the future, already processed, or removed
|
|
316
|
-
# @note Premature execution (before run_at) is delegated to queue strategy
|
|
317
|
-
#
|
|
318
|
-
# @see #perform
|
|
319
|
-
# @see Postburner.queue_strategy
|
|
320
|
-
# @see Callbacks
|
|
321
|
-
#
|
|
322
|
-
def perform!(args={})
|
|
323
|
-
run_callbacks :attempt do
|
|
324
|
-
self.attempting
|
|
325
|
-
|
|
326
|
-
self.update_columns(
|
|
327
|
-
attempting_at: self.attempting_at,
|
|
328
|
-
attempts: self.attempts,
|
|
329
|
-
attempt_count: self.attempts.length,
|
|
330
|
-
lag: self.lag,
|
|
331
|
-
processing_at: Time.zone.now,
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
begin
|
|
335
|
-
if self.queued_at.nil?
|
|
336
|
-
self.log! "Not Queued", level: :error
|
|
337
|
-
return
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
if self.queued_at > Time.zone.now
|
|
341
|
-
self.log! "Future Queued", level: :error
|
|
342
|
-
return
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
if self.processed_at.present?
|
|
346
|
-
self.log! "Already Processed", level: :error
|
|
347
|
-
self.delete!
|
|
348
|
-
return
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
if self.removed_at.present?
|
|
352
|
-
self.log! "Removed", level: :error
|
|
353
|
-
return
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
if self.run_at && self.run_at.to_i > Time.zone.now.to_i
|
|
357
|
-
Postburner.queue_strategy.handle_premature_perform(self)
|
|
358
|
-
return
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
self.log!("START (bkid #{self.bkid})")
|
|
362
|
-
|
|
363
|
-
run_callbacks :processing do
|
|
364
|
-
begin
|
|
365
|
-
self.perform(args)
|
|
366
|
-
rescue Exception => exception
|
|
367
|
-
self.persist_metadata!
|
|
368
|
-
self.log! '[Postburner] Exception raised during perform prevented completion.'
|
|
369
|
-
raise exception
|
|
370
|
-
end
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
self.log!("DONE (bkid #{self.bkid})")
|
|
374
|
-
|
|
375
|
-
begin
|
|
376
|
-
now = Time.zone.now
|
|
377
|
-
_duration = (now - self.processing_at) * 1000 rescue nil
|
|
378
|
-
|
|
379
|
-
run_callbacks :processed do
|
|
380
|
-
persist_metadata!(
|
|
381
|
-
processed_at: now,
|
|
382
|
-
duration: _duration,
|
|
383
|
-
)
|
|
384
|
-
end
|
|
385
|
-
rescue Exception => e
|
|
386
|
-
self.log_exception!(e)
|
|
387
|
-
self.log! '[Postburner] Could not set data after processing.'
|
|
388
|
-
# TODO README doesn't retry if Postburner is to blame
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
rescue Exception => exception
|
|
392
|
-
self.log_exception!(exception)
|
|
393
|
-
raise exception
|
|
394
|
-
end
|
|
395
|
-
end # run_callbacks :attempt
|
|
396
|
-
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
# Deletes the job from the Beanstalkd queue.
|
|
400
|
-
#
|
|
401
|
-
# This is a Beanstalkd operation that removes the job from the queue but
|
|
402
|
-
# does NOT destroy the ActiveRecord model. Use {#destroy} or {#remove!} to
|
|
403
|
-
# also update the database record.
|
|
404
|
-
#
|
|
405
|
-
# Automatically retries with fresh connection if Beanstalkd connection is stale.
|
|
406
|
-
# Called automatically by before_destroy callback when using {#destroy}.
|
|
407
|
-
#
|
|
408
|
-
# @return [void]
|
|
409
|
-
#
|
|
410
|
-
# @raise [Beaneater::NotConnected] if Beanstalkd connection fails after retry
|
|
411
|
-
#
|
|
412
|
-
# @note Does nothing if job has no bkid (e.g., in test mode)
|
|
413
|
-
# @note Does not modify ActiveRecord model - only affects Beanstalkd
|
|
414
|
-
#
|
|
415
|
-
# @example
|
|
416
|
-
# job.delete! # Removes from Beanstalkd queue only
|
|
417
|
-
# job.reload # Job still exists in database
|
|
418
|
-
#
|
|
419
|
-
# @see #remove!
|
|
420
|
-
# @see #destroy
|
|
421
|
-
# @see #beanstalk_job
|
|
422
|
-
#
|
|
423
|
-
def delete!
|
|
424
|
-
return unless self.beanstalk_job
|
|
425
|
-
begin
|
|
426
|
-
self.beanstalk_job.delete
|
|
427
|
-
rescue Beaneater::NotConnected => e
|
|
428
|
-
self.beanstalk_job!.delete
|
|
429
|
-
end
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
# Kicks a buried job back into the ready queue in Beanstalkd.
|
|
433
|
-
#
|
|
434
|
-
# This is a Beanstalkd operation used to retry jobs that were buried due to
|
|
435
|
-
# repeated failures or explicit burial. Does not modify the ActiveRecord model.
|
|
436
|
-
#
|
|
437
|
-
# Automatically retries with fresh connection if Beanstalkd connection is stale.
|
|
438
|
-
#
|
|
439
|
-
# @return [void]
|
|
440
|
-
#
|
|
441
|
-
# @raise [Beaneater::NotConnected] if Beanstalkd connection fails after retry
|
|
442
|
-
#
|
|
443
|
-
# @note Does nothing if job has no bkid (e.g., in test mode)
|
|
444
|
-
# @note Only works on buried jobs - see Beanstalkd documentation
|
|
445
|
-
#
|
|
446
|
-
# @example
|
|
447
|
-
# job.kick! # Moves buried job back to ready queue
|
|
448
|
-
#
|
|
449
|
-
# @see #delete!
|
|
450
|
-
# @see #beanstalk_job
|
|
451
|
-
#
|
|
452
|
-
def kick!
|
|
453
|
-
return unless self.beanstalk_job
|
|
454
|
-
begin
|
|
455
|
-
self.beanstalk_job.kick
|
|
456
|
-
rescue Beaneater::NotConnected => e
|
|
457
|
-
self.beanstalk_job!.kick
|
|
458
|
-
end
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
# Soft-deletes the job by removing from Beanstalkd and setting removed_at timestamp.
|
|
462
|
-
#
|
|
463
|
-
# Unlike {#destroy}, this preserves the job record in the database for audit trails
|
|
464
|
-
# while removing it from the Beanstalkd queue and marking it as removed.
|
|
465
|
-
#
|
|
466
|
-
# @return [void]
|
|
467
|
-
#
|
|
468
|
-
# @raise [Beaneater::NotConnected] if Beanstalkd connection fails
|
|
469
|
-
#
|
|
470
|
-
# @note Idempotent - does nothing if already removed
|
|
471
|
-
# @note Does not destroy ActiveRecord model - only soft deletes
|
|
472
|
-
#
|
|
473
|
-
# @example
|
|
474
|
-
# job.remove!
|
|
475
|
-
# job.removed_at # => 2025-10-31 12:34:56 UTC
|
|
476
|
-
# job.persisted? # => true (still in database)
|
|
477
|
-
#
|
|
478
|
-
# @see #delete!
|
|
479
|
-
# @see #destroy
|
|
480
|
-
#
|
|
481
|
-
def remove!
|
|
482
|
-
return if self.removed_at
|
|
483
|
-
self.delete!
|
|
484
|
-
self.update_column(:removed_at, Time.zone.now)
|
|
485
|
-
end
|
|
486
|
-
|
|
487
146
|
# Returns the Beanstalkd job object for direct queue operations.
|
|
488
147
|
#
|
|
489
148
|
# Provides access to the underlying Beaneater job object for advanced
|
|
@@ -497,342 +156,32 @@ module Postburner
|
|
|
497
156
|
# bk_job.bury # Bury the job
|
|
498
157
|
# bk_job.release(pri: 0) # Release with priority
|
|
499
158
|
#
|
|
500
|
-
# @see #
|
|
159
|
+
# @see #bk!
|
|
501
160
|
# @see #delete!
|
|
502
161
|
# @see #kick!
|
|
503
162
|
#
|
|
504
163
|
def bk
|
|
505
164
|
return unless self.bkid.present?
|
|
506
|
-
return @
|
|
507
|
-
|
|
508
|
-
@_beanstalk_job = Postburner.connection.beanstalk.jobs.find(self.bkid)
|
|
165
|
+
return @__job if @__job
|
|
509
166
|
|
|
510
|
-
@
|
|
167
|
+
@__job = Postburner.connection.beanstalk.jobs.find(self.bkid)
|
|
511
168
|
end
|
|
512
169
|
|
|
513
|
-
alias_method :beanstalk_job, :bk
|
|
514
|
-
|
|
515
170
|
# Returns the Beanstalkd job object with cache invalidation.
|
|
516
171
|
#
|
|
517
|
-
# Same as {#
|
|
172
|
+
# Same as {#bk} but clears the cached instance variable first,
|
|
518
173
|
# forcing a fresh lookup from Beanstalkd. Use when job state may have changed.
|
|
519
174
|
#
|
|
520
175
|
# @return [Beaneater::Job, nil] Beanstalkd job object or nil if no bkid
|
|
521
176
|
#
|
|
522
177
|
# @example
|
|
523
|
-
# job.
|
|
524
|
-
#
|
|
525
|
-
# @see #beanstalk_job
|
|
526
|
-
#
|
|
527
|
-
def bk!
|
|
528
|
-
@_beanstalk_job = nil
|
|
529
|
-
self.beanstalk_job
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
alias_method :beanstalk_job!, :bk!
|
|
533
|
-
|
|
534
|
-
# Returns job statistics including Beanstalkd job state.
|
|
535
|
-
#
|
|
536
|
-
# Fetches current job state from Beanstalkd and returns combined statistics
|
|
537
|
-
# about the job's PostgreSQL record and its current Beanstalkd status.
|
|
538
|
-
#
|
|
539
|
-
# @return [Hash] Statistics hash with the following keys:
|
|
540
|
-
# - id: PostgreSQL job ID
|
|
541
|
-
# - bkid: Beanstalkd job ID
|
|
542
|
-
# - queue: Queue name configured for this job class (e.g., 'sleep-jobs')
|
|
543
|
-
# - tube: Derived tube name with environment prefix (e.g., 'postburner.development.sleep-jobs')
|
|
544
|
-
# - watched: Boolean indicating if tube is in configured watch list
|
|
545
|
-
# - beanstalk: Hash of Beanstalkd job statistics:
|
|
546
|
-
# - id: Beanstalkd job ID
|
|
547
|
-
# - tube: Tube name where job resides
|
|
548
|
-
# - state: Job state (ready, reserved, delayed, buried)
|
|
549
|
-
# - pri: Priority (0 = highest, 4294967295 = lowest)
|
|
550
|
-
# - age: Seconds since job was created
|
|
551
|
-
# - delay: Seconds remaining before job becomes ready (0 if ready now)
|
|
552
|
-
# - ttr: Time-to-run in seconds (time allowed for job processing)
|
|
553
|
-
# - time_left: Seconds remaining before job times out (0 if not reserved)
|
|
554
|
-
# - file: Binlog file number containing job
|
|
555
|
-
# - reserves: Number of times job has been reserved
|
|
556
|
-
# - timeouts: Number of times job has timed out during processing
|
|
557
|
-
# - releases: Number of times job has been released back to ready
|
|
558
|
-
# - buries: Number of times job has been buried
|
|
559
|
-
# - kicks: Number of times job has been kicked from buried/delayed
|
|
560
|
-
#
|
|
561
|
-
# @raise [Beaneater::NotFoundError] if job no longer exists in Beanstalkd
|
|
562
|
-
#
|
|
563
|
-
# @example
|
|
564
|
-
# job.stats
|
|
565
|
-
# # => {
|
|
566
|
-
# # id: 5,
|
|
567
|
-
# # bkid: 1,
|
|
568
|
-
# # queue: "sleep-jobs",
|
|
569
|
-
# # tube: "postburner.development.sleep-jobs",
|
|
570
|
-
# # watched: false,
|
|
571
|
-
# # beanstalk: {
|
|
572
|
-
# # id: 1,
|
|
573
|
-
# # tube: "postburner.development.sleep-jobs",
|
|
574
|
-
# # state: "ready",
|
|
575
|
-
# # pri: 50,
|
|
576
|
-
# # age: 1391,
|
|
577
|
-
# # delay: 0,
|
|
578
|
-
# # ttr: 120,
|
|
579
|
-
# # time_left: 0,
|
|
580
|
-
# # file: 0,
|
|
581
|
-
# # reserves: 0,
|
|
582
|
-
# # timeouts: 0,
|
|
583
|
-
# # releases: 0,
|
|
584
|
-
# # buries: 0,
|
|
585
|
-
# # kicks: 0
|
|
586
|
-
# # }
|
|
587
|
-
# # }
|
|
588
|
-
#
|
|
589
|
-
def stats
|
|
590
|
-
# Get configured watched tubes (expanded with environment prefix)
|
|
591
|
-
watched = Postburner.watched_tube_names.include?(self.tube_name)
|
|
592
|
-
|
|
593
|
-
{
|
|
594
|
-
id: self.id,
|
|
595
|
-
bkid: self.bkid,
|
|
596
|
-
queue: queue_name,
|
|
597
|
-
tube: tube_name,
|
|
598
|
-
watched: watched,
|
|
599
|
-
beanstalk: self.bk.stats.to_h.symbolize_keys,
|
|
600
|
-
}
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
alias_method :beanstalk_job_stats, :stats
|
|
604
|
-
|
|
605
|
-
# Returns the queue name for this job instance.
|
|
606
|
-
#
|
|
607
|
-
# Checks instance-level override first, then falls back to class-level configuration.
|
|
608
|
-
#
|
|
609
|
-
# @return [String] Queue name
|
|
610
|
-
#
|
|
611
|
-
# @example Class-level configuration
|
|
612
|
-
# class MyJob < Postburner::Job
|
|
613
|
-
# queue 'critical'
|
|
614
|
-
# end
|
|
615
|
-
# job = MyJob.create!(args: {})
|
|
616
|
-
# job.queue_name # => 'critical'
|
|
617
|
-
#
|
|
618
|
-
def queue_name
|
|
619
|
-
self.class.queue
|
|
620
|
-
end
|
|
621
|
-
|
|
622
|
-
# Returns the priority for this job instance.
|
|
623
|
-
#
|
|
624
|
-
# Checks instance-level override first, then falls back to class-level configuration.
|
|
625
|
-
#
|
|
626
|
-
# @return [Integer, nil] Priority (lower = higher priority)
|
|
627
|
-
#
|
|
628
|
-
# @example Instance-level override
|
|
629
|
-
# job = MyJob.create!(args: {}, priority: 1500)
|
|
630
|
-
# job.priority # => 1500
|
|
631
|
-
#
|
|
632
|
-
def priority
|
|
633
|
-
@priority || self.class.priority
|
|
634
|
-
end
|
|
635
|
-
|
|
636
|
-
# Returns the TTR (time-to-run) for this job instance.
|
|
637
|
-
#
|
|
638
|
-
# Checks instance-level override first, then falls back to class-level configuration.
|
|
639
|
-
#
|
|
640
|
-
# @return [Integer, nil] TTR in seconds
|
|
641
|
-
#
|
|
642
|
-
# @example Instance-level override
|
|
643
|
-
# job = MyJob.create!(args: {}, ttr: 600)
|
|
644
|
-
# job.ttr # => 600
|
|
645
|
-
#
|
|
646
|
-
def ttr
|
|
647
|
-
@ttr || self.class.ttr
|
|
648
|
-
end
|
|
649
|
-
|
|
650
|
-
def tube_name
|
|
651
|
-
Postburner.configuration.expand_tube_name(queue_name)
|
|
652
|
-
end
|
|
653
|
-
|
|
654
|
-
# Extends the job's time-to-run (TTR) in Beanstalkd.
|
|
655
|
-
#
|
|
656
|
-
# Calls touch on the Beanstalkd job, extending the TTR by the original
|
|
657
|
-
# TTR value. Use this during long-running operations to prevent the job
|
|
658
|
-
# from timing out.
|
|
659
|
-
#
|
|
660
|
-
# @return [void]
|
|
661
|
-
#
|
|
662
|
-
# @note Does nothing if job has no bkid (e.g., in test mode)
|
|
663
|
-
#
|
|
664
|
-
# @example Process large file line by line
|
|
665
|
-
# def perform(args)
|
|
666
|
-
# file = File.find(args['file_id'])
|
|
667
|
-
# file.each_line do |line|
|
|
668
|
-
# # ... process line ...
|
|
669
|
-
# extend! # Extend TTR to prevent timeout
|
|
670
|
-
# end
|
|
671
|
-
# end
|
|
178
|
+
# job.bk! # Forces fresh lookup
|
|
672
179
|
#
|
|
673
180
|
# @see #bk
|
|
674
181
|
#
|
|
675
|
-
def
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
self.bk.touch
|
|
679
|
-
rescue Beaneater::NotConnected => e
|
|
680
|
-
self.bk!.touch
|
|
681
|
-
end
|
|
682
|
-
end
|
|
683
|
-
|
|
684
|
-
# Tracks an exception in the job's errata array.
|
|
685
|
-
#
|
|
686
|
-
# Appends exception details to the in-memory errata array with timestamp,
|
|
687
|
-
# class, message, and backtrace. Does NOT persist to database immediately.
|
|
688
|
-
# Use {#log_exception!} to persist immediately.
|
|
689
|
-
#
|
|
690
|
-
# @param exception [Exception] The exception to track
|
|
691
|
-
#
|
|
692
|
-
# @return [Array<Array>] Updated errata array
|
|
693
|
-
#
|
|
694
|
-
# @example
|
|
695
|
-
# begin
|
|
696
|
-
# # ... risky operation ...
|
|
697
|
-
# rescue => e
|
|
698
|
-
# log_exception(e)
|
|
699
|
-
# raise
|
|
700
|
-
# end
|
|
701
|
-
#
|
|
702
|
-
# @see #log_exception!
|
|
703
|
-
#
|
|
704
|
-
def log_exception(exception)
|
|
705
|
-
self.errata << [
|
|
706
|
-
Time.zone.now,
|
|
707
|
-
{
|
|
708
|
-
bkid: self.bkid,
|
|
709
|
-
class: exception.class,
|
|
710
|
-
message: exception.message,
|
|
711
|
-
backtrace: exception.backtrace,
|
|
712
|
-
}
|
|
713
|
-
]
|
|
714
|
-
end
|
|
715
|
-
|
|
716
|
-
# Tracks an exception and immediately persists to database.
|
|
717
|
-
#
|
|
718
|
-
# Calls {#log_exception} to append exception details, then persists
|
|
719
|
-
# both errata and error_count to database via {#persist_metadata!}.
|
|
720
|
-
#
|
|
721
|
-
# @param exception [Exception] The exception to track
|
|
722
|
-
#
|
|
723
|
-
# @return [void]
|
|
724
|
-
#
|
|
725
|
-
# @example
|
|
726
|
-
# begin
|
|
727
|
-
# # ... risky operation ...
|
|
728
|
-
# rescue => e
|
|
729
|
-
# log_exception!(e)
|
|
730
|
-
# raise
|
|
731
|
-
# end
|
|
732
|
-
#
|
|
733
|
-
# @see #log_exception
|
|
734
|
-
#
|
|
735
|
-
def log_exception!(exception)
|
|
736
|
-
self.log_exception(exception)
|
|
737
|
-
self.persist_metadata!
|
|
738
|
-
end
|
|
739
|
-
|
|
740
|
-
# Appends a log message to the job's logs array.
|
|
741
|
-
#
|
|
742
|
-
# Adds timestamped log entry to in-memory logs array with bkid, level,
|
|
743
|
-
# message, and elapsed time. Does NOT persist to database immediately.
|
|
744
|
-
# Use {#log!} to persist immediately.
|
|
745
|
-
#
|
|
746
|
-
# @param message [String] Log message to append
|
|
747
|
-
# @param options [Hash] Log options
|
|
748
|
-
# @option options [Symbol] :level Log level (:debug, :info, :warning, :error) - defaults to :info
|
|
749
|
-
#
|
|
750
|
-
# @return [Array<Array>] Updated logs array
|
|
751
|
-
#
|
|
752
|
-
# @note Invalid log levels are coerced to :error
|
|
753
|
-
#
|
|
754
|
-
# @example
|
|
755
|
-
# log "Processing started"
|
|
756
|
-
# log "Warning: rate limit approaching", level: :warning
|
|
757
|
-
# log "Critical error occurred", level: :error
|
|
758
|
-
#
|
|
759
|
-
# @see #log!
|
|
760
|
-
# @see #elapsed_ms
|
|
761
|
-
# @see LOG_LEVELS
|
|
762
|
-
#
|
|
763
|
-
def log(message, options={})
|
|
764
|
-
options[:level] ||= :info
|
|
765
|
-
options[:level] = :error unless LOG_LEVELS.member?(options[:level])
|
|
766
|
-
|
|
767
|
-
self.logs << [
|
|
768
|
-
Time.zone.now, # time
|
|
769
|
-
{
|
|
770
|
-
bkid: self.bkid,
|
|
771
|
-
level: options[:level], # level
|
|
772
|
-
message: message, # message
|
|
773
|
-
elapsed: self.elapsed_ms, # ms from start
|
|
774
|
-
}
|
|
775
|
-
]
|
|
776
|
-
end
|
|
777
|
-
|
|
778
|
-
# Returns elapsed time in milliseconds since job execution started.
|
|
779
|
-
#
|
|
780
|
-
# Calculates milliseconds between attempting_at and current time.
|
|
781
|
-
# Used to track how long a job has been executing.
|
|
782
|
-
#
|
|
783
|
-
# @return [Float, nil] Milliseconds since attempting_at, or nil if not yet attempting
|
|
784
|
-
#
|
|
785
|
-
# @example
|
|
786
|
-
# elapsed_ms # => 1234.567 (job has been running for ~1.2 seconds)
|
|
787
|
-
#
|
|
788
|
-
# @see #log
|
|
789
|
-
#
|
|
790
|
-
def elapsed_ms
|
|
791
|
-
return unless self.attempting_at
|
|
792
|
-
(Time.zone.now - self.attempting_at) * 1000
|
|
793
|
-
end
|
|
794
|
-
|
|
795
|
-
# Appends a log message and immediately persists to database.
|
|
796
|
-
#
|
|
797
|
-
# Calls {#log} to append message, then persists logs array to database.
|
|
798
|
-
# Use this for important log messages that should be saved immediately.
|
|
799
|
-
#
|
|
800
|
-
# @param message [String] Log message to append
|
|
801
|
-
# @param options [Hash] Log options
|
|
802
|
-
# @option options [Symbol] :level Log level (:debug, :info, :warning, :error)
|
|
803
|
-
#
|
|
804
|
-
# @return [void]
|
|
805
|
-
#
|
|
806
|
-
# @example
|
|
807
|
-
# log! "Job started"
|
|
808
|
-
# log! "Payment processed successfully", level: :info
|
|
809
|
-
# log! "Retrying failed request", level: :warning
|
|
810
|
-
#
|
|
811
|
-
# @see #log
|
|
812
|
-
#
|
|
813
|
-
def log!(message, options={})
|
|
814
|
-
self.log(message, options)
|
|
815
|
-
self.update_column :logs, self.logs
|
|
816
|
-
end
|
|
817
|
-
|
|
818
|
-
# Returns the intended execution time for the job.
|
|
819
|
-
#
|
|
820
|
-
# Returns run_at if scheduled, otherwise returns queued_at.
|
|
821
|
-
# Used for calculating lag (delay between intended and actual execution).
|
|
822
|
-
#
|
|
823
|
-
# @return [Time] Intended execution timestamp
|
|
824
|
-
#
|
|
825
|
-
# @example
|
|
826
|
-
# job.queue!(delay: 1.hour)
|
|
827
|
-
# job.intended_at # => 1 hour from now (run_at)
|
|
828
|
-
#
|
|
829
|
-
# job.queue!
|
|
830
|
-
# job.intended_at # => now (queued_at)
|
|
831
|
-
#
|
|
832
|
-
# @see #lag
|
|
833
|
-
#
|
|
834
|
-
def intended_at
|
|
835
|
-
self.run_at ? self.run_at : self.queued_at
|
|
182
|
+
def bk!
|
|
183
|
+
@__job = nil
|
|
184
|
+
self.bk
|
|
836
185
|
end
|
|
837
186
|
|
|
838
187
|
# Exception raised when attempting to queue a job that has already been processed.
|
|
@@ -887,88 +236,42 @@ module Postburner
|
|
|
887
236
|
#
|
|
888
237
|
class MalformedResponse < StandardError; end
|
|
889
238
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
# Persists job metadata (errata, logs, and counts) to database.
|
|
893
|
-
#
|
|
894
|
-
# Updates errata, error_count, logs, and log_count columns, plus any
|
|
895
|
-
# additional data passed in. Used internally for atomic metadata updates.
|
|
896
|
-
#
|
|
897
|
-
# @param data [Hash] Additional columns to update
|
|
898
|
-
#
|
|
899
|
-
# @return [void]
|
|
900
|
-
#
|
|
901
|
-
# @api private
|
|
902
|
-
#
|
|
903
|
-
def persist_metadata!(data={})
|
|
904
|
-
self.update_columns({
|
|
905
|
-
errata: self.errata,
|
|
906
|
-
error_count: self.errata.length,
|
|
907
|
-
logs: self.logs,
|
|
908
|
-
log_count: self.logs.length,
|
|
909
|
-
}.merge(data))
|
|
910
|
-
end
|
|
911
|
-
|
|
912
|
-
# After-save-commit callback that inserts job into queue if flagged.
|
|
239
|
+
# Exception raised when Beanstalkd rejects a job with BAD_FORMAT error.
|
|
913
240
|
#
|
|
914
|
-
#
|
|
915
|
-
#
|
|
916
|
-
#
|
|
917
|
-
#
|
|
241
|
+
# Beanstalkd returns BAD_FORMAT when job parameters are invalid, such as:
|
|
242
|
+
# - Negative delay values
|
|
243
|
+
# - Invalid priority (outside 0-4294967295 range)
|
|
244
|
+
# - Invalid TTR (time-to-run)
|
|
245
|
+
# - Malformed job data
|
|
918
246
|
#
|
|
919
|
-
#
|
|
247
|
+
# This exception wraps the original Beaneater::BadFormatError and includes
|
|
248
|
+
# detailed debugging information about the parameters that caused the error.
|
|
249
|
+
# The original exception is preserved via the `cause` chain.
|
|
920
250
|
#
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
insert!(@_insert_options)
|
|
925
|
-
end
|
|
926
|
-
|
|
927
|
-
# Inserts job into queue via current queue strategy.
|
|
251
|
+
# @example Common causes
|
|
252
|
+
# # Negative delay (now prevented by fix in queue.rb)
|
|
253
|
+
# job.queue!(at: 1.hour.ago) # Previously caused BAD_FORMAT
|
|
928
254
|
#
|
|
929
|
-
#
|
|
930
|
-
#
|
|
931
|
-
# Updates bkid if the strategy returns a Beanstalkd job ID.
|
|
255
|
+
# # Invalid priority
|
|
256
|
+
# job.queue!(pri: -1) # Outside valid range
|
|
932
257
|
#
|
|
933
|
-
# @
|
|
258
|
+
# @example Accessing detailed error information
|
|
259
|
+
# begin
|
|
260
|
+
# job.queue!(at: past_time)
|
|
261
|
+
# rescue Postburner::Job::BadFormat => e
|
|
262
|
+
# puts e.message # Includes tube, data, pri, delay, ttr
|
|
263
|
+
# puts e.cause # Original Beaneater::BadFormatError
|
|
264
|
+
# end
|
|
934
265
|
#
|
|
935
|
-
# @
|
|
266
|
+
# @note The error message includes full job parameters for debugging
|
|
267
|
+
# @note Access original Beanstalkd error via exception.cause
|
|
936
268
|
#
|
|
937
|
-
# @
|
|
269
|
+
# @see Postburner::Queue#insert
|
|
938
270
|
#
|
|
939
|
-
|
|
940
|
-
response = Postburner.queue_strategy.insert(self, options)
|
|
941
|
-
#debugger
|
|
942
|
-
|
|
943
|
-
# Response must be a hash with an :id key (value can be nil)
|
|
944
|
-
# Backburner returns symbol keys
|
|
945
|
-
unless response.is_a?(Hash) && response.key?(:id)
|
|
946
|
-
raise MalformedResponse, "Missing :id key in response: #{response.inspect}"
|
|
947
|
-
end
|
|
948
|
-
|
|
949
|
-
persist_metadata!(bkid: response[:id])
|
|
271
|
+
class BadFormat < StandardError; end
|
|
950
272
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
response
|
|
954
|
-
end
|
|
273
|
+
private
|
|
955
274
|
|
|
956
|
-
# Records an attempt and calculates execution lag.
|
|
957
|
-
#
|
|
958
|
-
# Appends current time to attempts array, sets attempting_at on first attempt,
|
|
959
|
-
# and calculates lag (delay between intended and actual execution time).
|
|
960
|
-
#
|
|
961
|
-
# @return [Time] Current timestamp
|
|
962
|
-
#
|
|
963
|
-
# @api private
|
|
964
|
-
#
|
|
965
|
-
def attempting
|
|
966
|
-
now = Time.zone.now
|
|
967
|
-
self.attempts << now
|
|
968
|
-
self.attempting_at ||= now
|
|
969
|
-
self.lag ||= (self.attempting_at - self.intended_at) * 1000 rescue nil
|
|
970
|
-
now
|
|
971
|
-
end
|
|
972
275
|
|
|
973
276
|
# Before-validation callback that ensures job has a unique sid.
|
|
974
277
|
#
|
|
@@ -983,17 +286,5 @@ module Postburner
|
|
|
983
286
|
self.sid ||= SecureRandom.uuid
|
|
984
287
|
end
|
|
985
288
|
|
|
986
|
-
# Backburner message format for the job.
|
|
987
|
-
#
|
|
988
|
-
# Returns a simple string representation used by Backburner.
|
|
989
|
-
#
|
|
990
|
-
# @return [String] Message format
|
|
991
|
-
#
|
|
992
|
-
# @api private
|
|
993
|
-
#
|
|
994
|
-
def message
|
|
995
|
-
"Job: #{self.id}"
|
|
996
|
-
end
|
|
997
|
-
|
|
998
289
|
end
|
|
999
290
|
end
|