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.
@@ -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 QueueConfig
74
+ include Properties
75
75
  include Callbacks
76
-
77
- # Instance-level queue configuration (overrides class-level defaults)
78
- attr_writer :priority, :ttr
79
-
80
- LOG_LEVELS = [
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 #beanstalk_job!
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 @_beanstalk_job if @_beanstalk_job
507
-
508
- @_beanstalk_job = Postburner.connection.beanstalk.jobs.find(self.bkid)
165
+ return @__job if @__job
509
166
 
510
- @_beanstalk_job
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 {#beanstalk_job} but clears the cached instance variable first,
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.beanstalk_job! # Forces fresh lookup
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 extend!
676
- return unless self.bk
677
- begin
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
- private
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
- # Checks {#will_insert?} and calls {#insert!} if true. Triggered by
915
- # after_save_commit callback after {#queue!} saves the record.
916
- #
917
- # @return [void]
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
- # @api private
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
- def insert_if_queued!
922
- #debugger
923
- return unless self.will_insert?
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
- # Delegates to Postburner.queue_strategy.insert which handles actual
930
- # queueing (Beanstalkd in production, inline execution in test mode).
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
- # @param options [Hash] Queue options (delay, pri, ttr, etc.)
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
- # @return [Hash, nil] Queue strategy response
266
+ # @note The error message includes full job parameters for debugging
267
+ # @note Access original Beanstalkd error via exception.cause
936
268
  #
937
- # @api private
269
+ # @see Postburner::Queue#insert
938
270
  #
939
- def insert!(options={})
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
- self.log("QUEUED: #{response}") if response
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