postburner 0.7.2 → 0.9.0.rc.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31329ed53b5479ef4b3963ce4f6faa7ae103986cb68af6777db9c5dbb46a5b28
4
- data.tar.gz: 2c115fcdea67de746f3544684377ff66f8f85ec48241b93a6c8f107e9c5bf052
3
+ metadata.gz: 5bef243523d66bb1c5d803e826f55fa047c20b7876b804cb82ecb6a82b235a38
4
+ data.tar.gz: f97a82f504e19e297fcb2f8b99b238b8804fc7e566fba23f082d092c79b88101
5
5
  SHA512:
6
- metadata.gz: 213abba743144280c99e4b65b0ad54fa26189a3c7f60c82ac9258642ca50803769b297abb53619f89bd88152e43ac6720881178b154ed45771c1fb252eeda99e
7
- data.tar.gz: c01eb3f31553159d442cf9c82e984ca084c83b301df918fe2cfedc4d6d2c508d15ae114ff5402361acf9a2bdeefe0db4ed508183e366ed144ae467aad422a0c3
6
+ metadata.gz: e990e976405963feeb3b5c8f9de98e1a2553f2820f363ef67ffc8c31cf54fa1e6cb6a9e9a8cb53b01ef9f8c644fe0fcc5a13fd7dc2677796401f66f2c7318f93
7
+ data.tar.gz: c2e829e9f8fc65020cfc080e19d4f26c4d5befa5c5855a73c4225cfaec557144a5237451d129a1e3cae554575bd3053401ead7bcc4c79c524282f6d3dfb4c370
data/CHANGELOG.md CHANGED
@@ -1,18 +1,5 @@
1
1
  # Changelog
2
2
 
3
- ## v0.7.1 - 2022-10-30
4
-
5
- ### Required Changes
6
- - rename `enqueue` callbacks to `insert`.
7
-
8
- ### Changed
9
- - convert to ActiveModel callbacks.
10
-
11
- ## v0.7.1 - 2022-10-30
12
-
13
- ### Changed
14
- - return true from insert_if_queued! and log response to Rails logger.
15
-
16
3
  ## v0.7.0 - 2022-10-24
17
4
 
18
5
  ### Added
data/README.md CHANGED
@@ -11,6 +11,27 @@ If you need something faster, check out Que - we love it! Postburner is built fo
11
11
  simple, safe, and inspectable. All of which Que has (or can be added), but are included by default with some architecture
12
12
  differences. See [Comparison to Que](#comparison-to-que) for more.
13
13
 
14
+ ## Queue Strategies
15
+
16
+ Postburner uses different strategies to control how jobs are queued and executed:
17
+
18
+ | Strategy | When to Use | Behavior | Requires Beanstalkd |
19
+ |----------|-------------|----------|---------------------|
20
+ | **NiceQueue** (default) | Production | Async via Beanstalkd, gracefully re-queues premature jobs | Yes |
21
+ | **Queue** (suggested) | Production | Async via Beanstalkd, raises error on premature execution | Yes |
22
+ | **TestQueue** | Testing with explicit time control | Inline execution, raises error for scheduled jobs | No |
23
+ | **ImmediateTestQueue** | Testing with automatic time travel | Inline execution, auto time-travels for scheduled jobs | No |
24
+ | **NullQueue** | Batch processing / deferred execution | Jobs created but not queued, manual execution via `Postburner::Job.perform(id)` | No |
25
+
26
+ ```ruby
27
+ # Switch strategies
28
+ Postburner.nice_async_strategy! # Default production (NiceQueue)
29
+ Postburner.async_strategy! # Strict production (Queue)
30
+ Postburner.inline_test_strategy! # Testing (TestQueue)
31
+ Postburner.inline_immediate_test_strategy! # Testing with time travel (ImmediateTestQueue)
32
+ Postburner.null_strategy! # Deferred execution (NullQueue)
33
+ ```
34
+
14
35
  ## Usage
15
36
 
16
37
  ```ruby
@@ -47,6 +68,57 @@ RunDonation.create!(args: {donation_id: 123}).queue! delay: 1.hour
47
68
  => {:status=>"INSERTED", :id=>"1141"}
48
69
  ```
49
70
 
71
+ ### Backburner Queue Configuration
72
+
73
+ Postburner jobs inherit from `Backburner::Queue`, giving you access to Backburner's queue configuration methods. These are **optional** - Backburner provides sensible defaults.
74
+
75
+ ```ruby
76
+ class CriticalJob < Postburner::Job
77
+ # Specify which Beanstalkd tube to use (default: 'backburner-jobs')
78
+ queue 'critical'
79
+
80
+ # Set job priority - lower numbers = higher priority (default: 65536)
81
+ # Most urgent priority is 0
82
+ queue_priority 0
83
+
84
+ # Set maximum retry attempts before burying (default: from Backburner config)
85
+ queue_max_job_retries 3
86
+
87
+ # Set job timeout in seconds (default: from Backburner config)
88
+ queue_respond_timeout 300
89
+
90
+ def perform(args)
91
+ # your job logic
92
+ end
93
+ end
94
+ ```
95
+
96
+ **Common configurations:**
97
+
98
+ ```ruby
99
+ # High priority job with no retries
100
+ class PaymentJob < Postburner::Job
101
+ queue 'payments'
102
+ queue_priority 0
103
+ queue_max_job_retries 0 # Don't retry - handle failures manually
104
+ end
105
+
106
+ # Background job with retries
107
+ class EmailJob < Postburner::Job
108
+ queue 'emails'
109
+ queue_priority 1000 # Lower priority
110
+ queue_max_job_retries 5 # Retry up to 5 times
111
+ end
112
+
113
+ # Long-running job
114
+ class DataExportJob < Postburner::Job
115
+ queue 'exports'
116
+ queue_respond_timeout 3600 # 1 hour timeout
117
+ end
118
+ ```
119
+
120
+ These methods come from [Backburner](https://github.com/nesquena/backburner#configure-backburner) and work seamlessly with Postburner. See Backburner's documentation for all available options.
121
+
50
122
  ### Mailers
51
123
 
52
124
  ```ruby
@@ -58,31 +130,96 @@ j.queue!
58
130
  => {:status=>"INSERTED", :id=>"1139"}
59
131
  ```
60
132
 
61
- ### [Beaneater](https://github.com/beanstalkd/beaneater) and [beanstalkd](https://raw.githubusercontent.com/beanstalkd/beanstalkd/master/doc/protocol.txt) attributes and methods
62
- ```ruby
63
- # get the beanstalkd job id
64
- job.bkid
65
- => 1104
133
+ ### Job Management Methods
66
134
 
67
- # get the beaneater job, call any beaneater methods on this object
68
- job.beanstalk_job
135
+ Postburner provides several methods for managing jobs in Beanstalkd and the database:
69
136
 
70
- # get the beanstald stats
71
- job.beanstalk_job.stats
137
+ #### delete! - Remove from Beanstalkd only
72
138
 
73
- # kick the beanstalk job
74
- job.kick!
139
+ Deletes the job from the Beanstalkd queue but **keeps the database record**. Use this when you want to cancel a queued job while maintaining its audit trail.
75
140
 
76
- # delete beankstalkd job, retain the job model
141
+ ```ruby
142
+ job = MyJob.create!(args: {})
143
+ job.queue!(delay: 1.hour)
144
+ job.bkid # => 12345
145
+
146
+ # Remove from Beanstalkd but keep in database
77
147
  job.delete!
78
148
 
79
- # delete beankstalkd job, retain the job model, but set `removed_at` on model.
149
+ job.reload
150
+ job.bkid # => 12345 (still has bkid)
151
+ job.persisted? # => true (still in database)
152
+ # Job won't execute, but you can still inspect it
153
+ ```
154
+
155
+ #### remove! - Soft delete (Beanstalkd + removed_at)
156
+
157
+ Removes the job from Beanstalkd **and** sets `removed_at` timestamp. The database record is preserved for audit trails. This is the recommended way to cancel jobs.
158
+
159
+ ```ruby
160
+ job = MyJob.create!(args: {})
161
+ job.queue!(delay: 1.hour)
162
+
163
+ # Soft delete - removes from queue and marks as removed
80
164
  job.remove!
81
165
 
82
- # or simply remove the model, which will clean up the beanstalkd job in a before_destroy hook
83
- job.destroy # OR job.destroy!
166
+ job.reload
167
+ job.removed_at # => 2025-11-01 12:34:56 UTC
168
+ job.persisted? # => true (still in database)
169
+ # Job won't execute and is marked as removed
170
+ ```
171
+
172
+ #### destroy / destroy! - Delete from both
173
+
174
+ Destroys the database record **and** removes from Beanstalkd via `before_destroy` callback. Use when you want to completely eliminate the job.
175
+
176
+ ```ruby
177
+ job = MyJob.create!(args: {})
178
+ job.queue!(delay: 1.hour)
179
+
180
+ # Completely remove job from database and Beanstalkd
181
+ job.destroy!
182
+
183
+ # Job no longer exists in database or Beanstalkd
184
+ ```
185
+
186
+ #### kick! - Retry buried jobs
187
+
188
+ Moves a buried job back into the ready queue. Buried jobs are those that failed too many times or were explicitly buried by Beanstalkd.
189
+
190
+ ```ruby
191
+ # Job failed and was buried
192
+ job.beanstalk_job.stats['state'] # => 'buried'
193
+
194
+ # Move it back to ready queue for retry
195
+ job.kick!
196
+
197
+ job.beanstalk_job.stats['state'] # => 'ready'
198
+ ```
84
199
 
85
- # get a cached Backburner connection and inspect it (or even use it directly)
200
+ **Summary:**
201
+ - **`delete!`** - Removes from Beanstalkd, keeps full database record (keeps `bkid`)
202
+ - **`remove!`** - Removes from Beanstalkd, marks `removed_at`, keeps database record (**recommended for canceling jobs**)
203
+ - **`destroy!`** - Removes from both Beanstalkd and database (complete deletion)
204
+ - **`kick!`** - Moves buried job back to ready queue (for retrying failed jobs)
205
+
206
+ ### [Beaneater](https://github.com/beanstalkd/beaneater) and [beanstalkd](https://raw.githubusercontent.com/beanstalkd/beanstalkd/master/doc/protocol.txt) Integration
207
+
208
+ Access Beanstalkd directly for advanced queue operations:
209
+
210
+ ```ruby
211
+ # Get the beanstalkd job id
212
+ job.bkid
213
+ => 1104
214
+
215
+ # Get the beaneater job object - call any beaneater methods
216
+ job.beanstalk_job
217
+
218
+ # Get beanstalkd stats for this job
219
+ job.beanstalk_job.stats
220
+ => {"id"=>1104, "tube"=>"critical", "state"=>"ready", "pri"=>0, ...}
221
+
222
+ # Get a cached Backburner connection
86
223
  c = Postburner.connection
87
224
  c.beanstalk.tubes.to_a
88
225
  c.beanstalk.tubes.to_a.map{|t| c.tubes[t.name].peek(:buried)}
@@ -91,10 +228,12 @@ c.beanstalk.tubes['ss.development.caching'].peek(:buried).kick
91
228
  c.beanstalk.tubes['ss.development.caching'].kick(3)
92
229
  c.close
93
230
 
94
- # automatically close
231
+ # Automatically close connection (recommended)
95
232
  Postburner.connected do |connection|
96
233
  # do stuff with connection
234
+ connection.tubes['my.tube'].stats
97
235
  end
236
+ # Connection automatically closed
98
237
  ```
99
238
 
100
239
  Read about the [beanstalkd protocol](https://raw.githubusercontent.com/beanstalkd/beanstalkd/master/doc/protocol.txt).
@@ -188,6 +327,52 @@ class RunDonation < Postburner::Job
188
327
  end
189
328
  ```
190
329
 
330
+ ### Callbacks
331
+
332
+ Postburner provides ActiveJob-style lifecycle callbacks for job execution:
333
+
334
+ ```ruby
335
+ class ProcessPayment < Postburner::Job
336
+ queue 'critical'
337
+
338
+ before_enqueue :validate_payment
339
+ after_enqueue :send_confirmation
340
+
341
+ before_attempt :log_attempt
342
+ after_attempt :update_metrics
343
+
344
+ before_processing :acquire_lock
345
+ after_processing :release_lock
346
+
347
+ after_processed :send_receipt # Only runs on success
348
+
349
+ def perform(args)
350
+ payment = Payment.find(args['payment_id'])
351
+ payment.process!
352
+ end
353
+
354
+ private
355
+
356
+ def validate_payment
357
+ raise "Invalid payment" unless args['payment_id'].present?
358
+ end
359
+
360
+ def send_confirmation
361
+ PaymentMailer.queued(args['payment_id']).deliver_later
362
+ end
363
+
364
+ def send_receipt
365
+ PaymentMailer.receipt(args['payment_id']).deliver_later
366
+ end
367
+ end
368
+ ```
369
+
370
+ **Available callbacks:**
371
+ - `before_enqueue`, `around_enqueue`, `after_enqueue` - Run when `queue!` is called
372
+ - `before_attempt`, `around_attempt`, `after_attempt` - Run on every execution attempt (including retries)
373
+ - `before_processing`, `around_processing`, `after_processing` - Run during job execution
374
+ - `after_processed` - Only runs after successful completion (not on errors)
375
+
191
376
  ### Optionally, mount the engine
192
377
 
193
378
  ```ruby
@@ -203,6 +388,199 @@ to add your own authentication or changes - or just create your own routes, cont
203
388
  [Override the views](https://guides.rubyonrails.org/engines.html#overriding-views) to make them
204
389
  prettier - or follow the suggestion above and use your own.
205
390
 
391
+ ## Testing
392
+
393
+ Postburner provides multiple testing strategies to make your tests fast and predictable without requiring Beanstalkd. Tests automatically use `TestQueue` when Rails test mode is detected.
394
+
395
+ ### Rails and ActiveJob Test Configuration
396
+
397
+ Rails provides several configuration options that affect how background jobs behave in test environments:
398
+
399
+ #### ActiveJob Queue Adapter
400
+
401
+ The `config.active_job.queue_adapter` setting controls how ActiveJob queues jobs. Rails supports several built-in adapters:
402
+
403
+ ```ruby
404
+ # config/environments/test.rb (or config/application.rb)
405
+
406
+ # :test adapter (Rails default for test environment)
407
+ # - Jobs are NOT executed automatically
408
+ # - Jobs accumulate in ActiveJob::Base.queue_adapter.enqueued_jobs
409
+ # - You must explicitly call perform_enqueued_jobs to execute them
410
+ config.active_job.queue_adapter = :test
411
+
412
+ # :inline adapter
413
+ # - Jobs execute immediately in the same process
414
+ # - No queueing, synchronous execution like Postburner's test strategies
415
+ config.active_job.queue_adapter = :inline
416
+
417
+ # :async adapter
418
+ # - Jobs execute asynchronously in a thread pool
419
+ # - Not recommended for tests due to race conditions
420
+ config.active_job.queue_adapter = :async
421
+
422
+ # :backburner adapter (for production use with Postburner)
423
+ # - Requires Beanstalkd to be running
424
+ # - Jobs queued to Beanstalkd tubes
425
+ config.active_job.queue_adapter = :backburner
426
+ ```
427
+
428
+ #### ActiveJob Test Helpers
429
+
430
+ When using the `:test` adapter, Rails provides test helpers in `ActiveJob::TestHelper`:
431
+
432
+ ```ruby
433
+ # test/test_helper.rb
434
+ require 'test_helper'
435
+
436
+ class MyTest < ActiveSupport::TestCase
437
+ include ActiveJob::TestHelper
438
+
439
+ test "job is enqueued" do
440
+ assert_enqueued_with(job: MyJob, args: [{user_id: 123}]) do
441
+ MyJob.perform_later(user_id: 123)
442
+ end
443
+ end
444
+
445
+ test "job is performed" do
446
+ perform_enqueued_jobs do
447
+ MyJob.perform_later(user_id: 123)
448
+ end
449
+ # Job has now executed
450
+ end
451
+ end
452
+ ```
453
+
454
+ **Available helpers:**
455
+ - `perform_enqueued_jobs` - Executes all enqueued jobs
456
+ - `assert_enqueued_jobs` - Asserts number of jobs enqueued
457
+ - `assert_enqueued_with` - Asserts specific job was enqueued with args
458
+ - `assert_no_enqueued_jobs` - Asserts no jobs were enqueued
459
+ - `assert_performed_jobs` - Asserts number of jobs performed
460
+ - `queue_adapter.enqueued_jobs` - Array of enqueued jobs for inspection
461
+
462
+ #### Other ActiveJob Configuration Options
463
+
464
+ ```ruby
465
+ # config/application.rb or config/environments/test.rb
466
+
467
+ # Set default queue name for all jobs
468
+ config.active_job.queue_name_prefix = "myapp_#{Rails.env}"
469
+
470
+ # Skip after_enqueue callbacks if before_enqueue halts
471
+ config.active_job.skip_after_callbacks_if_terminated = true
472
+
473
+ # Log arguments when jobs are enqueued (useful for debugging)
474
+ config.active_job.log_arguments = true # default: true
475
+
476
+ # Set queue priority (if adapter supports it)
477
+ config.active_job.default_queue_name = 'default'
478
+
479
+ # Configure job serialization
480
+ config.active_job.custom_serializers << MyCustomSerializer
481
+ ```
482
+
483
+ ### Postburner Test Modes vs ActiveJob Adapters
484
+
485
+ **Key Difference:** Postburner jobs (subclasses of `Postburner::Job`) do NOT use ActiveJob's queue adapter. Postburner has its own queue strategies that control execution independently of ActiveJob settings.
486
+
487
+ | Postburner Strategy | Execution | Use Case | ActiveJob Equivalent |
488
+ |---------------------|-----------|----------|----------------------|
489
+ | `TestQueue` | Inline, raises on scheduled | Explicit time control | `:inline` adapter |
490
+ | `ImmediateTestQueue` | Inline with auto time-travel | Convenience testing | `:inline` adapter + `travel_to` |
491
+ | `NullQueue` | Deferred, manual execution | Batch processing | `:test` adapter (manual `perform_enqueued_jobs`) |
492
+ | `Queue` / `NiceQueue` | Async via Beanstalkd | Production | `:backburner` adapter |
493
+
494
+ **When to use what:**
495
+ - **ActiveJob with `:test` adapter:** For testing standard ActiveJob classes (mailers, ActiveStorage, etc.)
496
+ - **Postburner test strategies:** For testing `Postburner::Job` subclasses
497
+ - Both can coexist in the same test suite with different behaviors
498
+
499
+ ### Automatic Test Mode Detection
500
+
501
+ In Rails test environments, Postburner automatically switches to `TestQueue`:
502
+
503
+ ```ruby
504
+ # Happens automatically when:
505
+ # - Rails.env.test? is true
506
+ # - ActiveJob::Base.queue_adapter_name == :test
507
+
508
+ # Verify you're in test mode
509
+ Postburner.testing? # => true in tests
510
+ ```
511
+
512
+ **Note:** Even though `TestQueue` is set automatically in Rails test environments, you can override it at any time to use a different test strategy:
513
+
514
+ ```ruby
515
+ # In test_helper.rb or specific tests
516
+ Postburner.inline_immediate_test_strategy! # Use automatic time travel
517
+ # OR
518
+ Postburner.null_strategy! # Use deferred execution
519
+
520
+ # The auto-detection only sets the initial strategy
521
+ # You can change it anytime during tests
522
+ ```
523
+
524
+ ### TestQueue: Explicit Time Control (Default for Tests)
525
+
526
+ The `TestQueue` strategy forces explicit time management, helping catch scheduling bugs:
527
+
528
+ ```ruby
529
+ test "processes job immediately" do
530
+ job = MyJob.create!(args: { user_id: 123 })
531
+ job.queue!
532
+ assert job.reload.processed_at
533
+ end
534
+
535
+ test "processes scheduled job with time travel" do
536
+ job = MyJob.create!(args: { user_id: 123 })
537
+ job.queue!(delay: 2.hours)
538
+
539
+ # Must use travel_to for scheduled jobs
540
+ travel_to(job.run_at) do
541
+ assert job.reload.processed_at
542
+ end
543
+ end
544
+ ```
545
+
546
+ ### ImmediateTestQueue: Automatic Time Travel
547
+
548
+ For convenience in integration tests, `ImmediateTestQueue` automatically handles scheduled jobs:
549
+
550
+ ```ruby
551
+ test "scheduled jobs execute immediately" do
552
+ Postburner.inline_immediate_test_strategy!
553
+
554
+ job = SendReminderEmail.create!(args: { user_id: 123 })
555
+ job.queue!(delay: 24.hours)
556
+
557
+ # Automatically time-travels to scheduled time
558
+ assert job.reload.processed_at
559
+ assert_emails 1
560
+ end
561
+ ```
562
+
563
+ ### NullQueue: Deferred Execution
564
+
565
+ Create jobs without queueing, then manually execute them:
566
+
567
+ ```ruby
568
+ test "batch processes jobs" do
569
+ Postburner.null_strategy!
570
+
571
+ jobs = 5.times.map do |i|
572
+ job = ProcessRecord.create!(args: { id: i })
573
+ job.queue!
574
+ job
575
+ end
576
+
577
+ # Manually execute when ready
578
+ jobs.each { |job| Postburner::Job.perform(job.id) }
579
+
580
+ assert jobs.all? { |j| j.reload.processed_at }
581
+ end
582
+ ```
583
+
206
584
  ## Installation
207
585
 
208
586
  First [install beanstalkd](https://beanstalkd.github.io/download.html). On Debian-based systems, that goes like this:
@@ -343,5 +721,51 @@ bundle exec backburner
343
721
  Where <nac> is an authorized key with push capabilities.
344
722
 
345
723
 
724
+ ## FAQ
725
+
726
+ ### Can I save a Postburner::Job without enqueuing it?
727
+
728
+ Yes! You can absolutely save a `Postburner::Job` without enqueuing it. Looking at the code, here's how it works:
729
+
730
+ #### Creating vs. Queueing
731
+
732
+ When you use `create!` or `save!`, the job is persisted to the database but **not** sent to Beanstalkd:
733
+
734
+ ```ruby
735
+ # Creates job in database but does NOT enqueue
736
+ job = RunDonation.create!(args: {donation_id: 123})
737
+ job.persisted? # => true
738
+ job.queued_at # => nil
739
+ job.bkid # => nil (no Beanstalkd ID)
740
+ ```
741
+
742
+ To actually enqueue it, you must explicitly call `queue!`:
743
+
744
+ ```ruby
745
+ # Now it's enqueued to Beanstalkd
746
+ job.queue!(delay: 1.hour)
747
+ job.queued_at # => Time.zone.now
748
+ job.bkid # => 12345 (Beanstalkd job ID)
749
+ ```
750
+
751
+ #### How it Works
752
+
753
+ The implementation uses a clever pattern (app/models/postburner/job.rb:86-724):
754
+
755
+ 1. **after_save_commit callback** (`insert_if_queued!` at line 86) runs on every save
756
+ 2. **Conditional insertion** (line 722) - only inserts if `will_insert?` returns true
757
+ 3. **Flag check** (lines 242-244) - `will_insert?` only returns true if `@_insert_options` is set
758
+ 4. **Flag set** (line 199) - `@_insert_options` is only set inside the `queue!` method
759
+
760
+ So the job is safely persisted without queueing until you explicitly call `queue!`.
761
+
762
+ #### Use Cases
763
+
764
+ This is useful for:
765
+ - Creating jobs conditionally queued based on business logic
766
+ - Building jobs within transactions that might roll back
767
+ - Testing job creation without running workers
768
+ - Creating foreign key relationships before queueing
769
+
346
770
  ## License
347
771
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -3,7 +3,7 @@ require "bundler/setup"
3
3
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
- load "rails/tasks/statistics.rake"
6
+ #load "rails/tasks/statistics.rake"
7
7
 
8
8
  require "bundler/gem_tasks"
9
9