postburner 1.0.0.pre.12 → 1.0.0.pre.14

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: d49bdf9db5c61a4cc37f1ec0b0246b275f7ca825935e1083c98f9caef5ae383d
4
- data.tar.gz: 9990e7870348f823d415755d76d0e33b0b10528dd3412c793c2922829a2ab9fd
3
+ metadata.gz: 71c6a81826b32021b0f0083c1771b27423e1b25198d7c00669cbab1150c431e7
4
+ data.tar.gz: 26403dba984c15f4bad94d81f1f5833d6a6bb16c0bb216a236f6bcaf07c0d019
5
5
  SHA512:
6
- metadata.gz: 7b625d36b312e8eb1b1a4fb6509d87d23518a34943ee0150ccba3d57dd3b6b5970bbf40f5bf283be8c052b3be2b072888fa1fa3d47b9f72264b513ee5db7a05e
7
- data.tar.gz: 9fdd52fb338a3a6575042e2af955711128c02b9534074935a5360113391169f0ab53657c6155acf40ca8ee34424a2b01583500a3efb75eb48e9ea467e428c759
6
+ metadata.gz: bc088b5d6e5b36b514562635ee3ed75955dfe809efe981e3ddbdc47c5f5387715233b7eb0eec8b4e78f97cf715202beabb7f8b90df553c605eec058eee462a9c
7
+ data.tar.gz: 40dbfc6f0979226d5c67780ffe31c73700a6a7b7faf97bf8e7fd127a2c889e708db91835bc9389032191ae2fae6460f3cc1b21f28ee17a6a3f00dbc84c31fbea
data/README.md CHANGED
@@ -110,8 +110,7 @@ bundle exec rake postburner:work WORKER=default
110
110
  - [Why Beanstalkd?](#why-beanstalkd)
111
111
  - [Beanstalkd Integration](#beanstalkd-integration)
112
112
  - [Installation](#installation)
113
- - [Deployment](#deployment)
114
- - [Web UI](#web-ui)
113
+ - [Web UI - v2 Coming Soon](#web-ui)
115
114
 
116
115
  ## Why
117
116
 
@@ -249,6 +248,30 @@ Jobs without `Postburner::Beanstalkd` use defaults from `config/postburner.yml`:
249
248
  - `default_priority: 65536`
250
249
  - `default_ttr: 300`
251
250
 
251
+ #### Configuring Third-Party Jobs
252
+
253
+ For jobs you don't control (e.g., `Turbo::Streams::BroadcastJob`, `ActionMailer::MailDeliveryJob`), use the `enqueue_options` hook:
254
+
255
+ ```ruby
256
+ # config/initializers/postburner.rb
257
+ Postburner.configure do |config|
258
+ config.enqueue_options = ->(job) do
259
+ case job.class.name
260
+ when 'Turbo::Streams::BroadcastJob'
261
+ { priority: 100, ttr: 60 }
262
+ when 'ActionMailer::MailDeliveryJob'
263
+ { priority: 500, ttr: 120 }
264
+ else
265
+ {} # Use defaults
266
+ end
267
+ end
268
+ end
269
+ ```
270
+
271
+ **Priority cascade:** `job.priority` (from `.set(priority: n)`) > `enqueue_options` hook > class-level > `default_priority`
272
+
273
+ **TTR cascade:** class-level `ttr` > `enqueue_options` hook > `default_ttr`
274
+
252
275
  ### Tracked Jobs
253
276
 
254
277
  Tracked jobs store full execution details in PostgreSQL, providing comprehensive audit trails (i.e. logging, timing, errors, retry tracking) for critical operations.
@@ -763,36 +786,42 @@ Postburner uses different strategies to control job execution. These affect `Pos
763
786
 
764
787
  | Strategy | When to Use | Behavior | Requires Beanstalkd |
765
788
  |----------|-------------|----------|---------------------|
766
- | **NiceQueue** (default) | Production | Async via Beanstalkd, gracefully re-queues premature jobs | Yes |
767
- | **Queue** | Production (strict) | Async via Beanstalkd, raises error on premature execution | Yes |
768
- | **TestQueue** | Testing with explicit time control | Inline execution, raises error for scheduled jobs | No |
769
- | **ImmediateTestQueue** | Testing with automatic time travel | Inline execution, auto time-travels for scheduled jobs | No |
770
- | **NullQueue** | Batch processing / deferred execution | Jobs created but not queued, manual execution | No |
789
+ | **DefaultQueue** | Production | Async, gracefully requeues premature jobs | Yes |
790
+ | **StrictQueue** | Production | Async via Beanstalkd, raises error on premature execution | Yes |
791
+ | **NullQueue** | Test (usually) | Jobs created but not inserted into Beanstalkd, manual execution | No |
792
+ | **InlineTestQueue** | Test | Inline execution (error raised for delayed jobs) | No |
793
+ | **TimeTravelTestQueue** | Test | Inline execution, (auto time-travels for delayed jobs) | No |
771
794
 
772
795
  ```ruby
773
796
  # Switch strategies
774
- Postburner.nice_async_strategy! # Default production (NiceQueue)
775
- Postburner.async_strategy! # Strict production (Queue)
776
- Postburner.inline_test_strategy! # Testing (TestQueue)
777
- Postburner.inline_immediate_test_strategy! # Testing with time travel
778
- Postburner.null_strategy! # Deferred execution
797
+ Postburner.default_strategy! # Default production (DefaultQueue)
798
+ Postburner.strict_strategy! # Strict production (StrictQueue)
799
+ Postburner.null_strategy! # Deferred, or manual execution
800
+ Postburner.inline_test_strategy! # Testing (InlineTestQueue)
801
+ Postburner.time_travel_test_strategy! # Testing inline but also with time travel
779
802
  ```
780
803
 
781
- **Note:** These strategies only affect `Postburner::Job` subclasses. ActiveJob classes execute according to the ActiveJob adapter configuration.
804
+ ### Adding a Queue Strategy
782
805
 
783
- ## Testing
806
+ There are 4 hook methods that are used with inheritance:
807
+ - `insert` - Inserts the job into the queue
808
+ - `handle_perform!` - Handles the job being performed
809
+ - `handle_premature_perform` - Handles jobs executed before their scheduled run_at time
810
+ - `testing` - Returns true if the strategy is for testing
784
811
 
785
- Postburner provides test-friendly execution modes that don't require Beanstalkd.
812
+ ## Testing
786
813
 
787
814
  ### Automatic Test Mode
788
815
 
789
- In Rails test environments, Postburner automatically uses inline execution:
816
+ In Rails test environments, Postburner automatically uses inline execution via the `InlineTestQueue` strategy:
790
817
 
791
818
  ```ruby
792
819
  # test/test_helper.rb - automatic!
793
820
  Postburner.testing? # => true in tests
794
821
  ```
795
822
 
823
+ Postburner provides test-friendly execution modes that don't require Beanstalkd.
824
+
796
825
  ### Testing Default Jobs (ActiveJob)
797
826
 
798
827
  Use standard ActiveJob test helpers:
@@ -826,26 +855,47 @@ test "tracked job logs execution" do
826
855
  end
827
856
  ```
828
857
 
829
- ### Testing Legacy Postburner::Job
858
+ ### Switching Queue Strategies
859
+
860
+ For tests requiring specific queue behaviors, use `switch_queue_strategy!` and `restore_queue_strategy!`:
830
861
 
831
862
  ```ruby
832
- test "processes immediately" do
833
- job = ProcessPayment.create!(args: { 'payment_id' => 123 })
834
- job.queue!
863
+ class ScheduleTest < ActiveSupport::TestCase
864
+ def setup
865
+ switch_queue_strategy! Postburner::TimeTravelTestQueue
866
+ end
835
867
 
836
- assert job.reload.processed_at
868
+ def teardown
869
+ restore_queue_strategy!
870
+ end
871
+
872
+ test "scheduled job executes" do
873
+ job = MyJob.create!(args: {})
874
+ job.queue!(delay: 1.hour)
875
+ assert job.reload.processed_at # Auto time-travels
876
+ end
837
877
  end
878
+ ```
838
879
 
839
- test "scheduled job with time travel" do
840
- job = ProcessPayment.create!(args: { 'payment_id' => 123 })
880
+ **Block form for isolated tests:**
841
881
 
842
- travel_to(2.hours.from_now) do
843
- job.queue!(delay: 2.hours)
844
- assert job.reload.processed_at
882
+ ```ruby
883
+ test "specific strategy for one test" do
884
+ use_queue_strategy Postburner::NullQueue do
885
+ job = MyJob.create!(args: {})
886
+ job.queue!
887
+ assert_nil job.bkid # Not queued to Beanstalkd
845
888
  end
846
889
  end
847
890
  ```
848
891
 
892
+ **Available strategies:**
893
+ - `Postburner::InlineTestQueue` - Default test mode, raises on delayed jobs
894
+ - `Postburner::TimeTravelTestQueue` - Auto time-travel for delayed jobs
895
+ - `Postburner::NullQueue` - Create jobs without queueing
896
+ - `Postburner::StrictQueue` - Production mode with error on premature execution
897
+ - `Postburner::DefaultQueue` - Production mode with graceful requeue
898
+
849
899
  ## Workers
850
900
 
851
901
  Postburner uses named worker configurations to support different deployment patterns. Each worker can have different fork/thread settings and process different queues, enabling flexible production deployments.
@@ -965,6 +1015,8 @@ bin/postburner --worker general # Run the 'general' worker
965
1015
  bin/postburner --worker general --queues default,mailers # Only process specific queues
966
1016
  ```
967
1017
 
1018
+ **Note:** Prefer defining separate workers in `config/postburner.yml` rather than using `--queues` overrides. Named workers make your deployment configuration explicit and version-controlled. Use `--queues` only for debugging or temporary overrides.
1019
+
968
1020
  **Rake task:**
969
1021
  ```bash
970
1022
  bundle exec rake postburner:work # Auto-select worker
@@ -1216,6 +1268,15 @@ production: # <- environment config, i.e. defaults
1216
1268
  - mailers
1217
1269
  ```
1218
1270
 
1271
+ **Start workers with:**
1272
+
1273
+ ```bash
1274
+ bin/postburner # Auto-select single worker
1275
+ bin/postburner --worker default # Specify worker
1276
+ rake postburner:work # Rake task (auto-select)
1277
+ rake postburner:work WORKER=default
1278
+ ```
1279
+
1219
1280
  ### Queue Names
1220
1281
 
1221
1282
  Postburner automatically prefixes all queue names with `postburner.{env}.` to create Beanstalkd tube names. This namespacing prevents collisions when multiple applications share the same Beanstalkd server.
@@ -1348,16 +1409,86 @@ end
1348
1409
 
1349
1410
  ## Instrumentation
1350
1411
 
1351
- Postburner emits ActiveSupport::Notifications events following ActiveJob conventions. Use these for monitoring, logging, or alerting.
1412
+ Postburner emits ActiveSupport::Notifications events following Rails conventions. Use these for monitoring, logging, or alerting.
1413
+
1414
+ ### Job Events
1415
+
1416
+ | Event | When | Payload Keys |
1417
+ |-------|------|--------------|
1418
+ | `perform_start.job.postburner` | Before job execution begins | `:job`, `:beanstalk_job_id` |
1419
+ | `perform.job.postburner` | Around job execution (includes duration) | `:job`, `:beanstalk_job_id` |
1420
+ | `retry.job.postburner` | When job is retried after error | `:job`, `:beanstalk_job_id`, `:error`, `:wait`, `:attempt` |
1421
+ | `retry_stopped.job.postburner` | When tracked job exhausts retries | `:job`, `:beanstalk_job_id`, `:error` |
1422
+ | `discard.job.postburner` | When default job exhausts retries | `:job`, `:beanstalk_job_id`, `:error` |
1423
+ | `enqueue.job.postburner` | When job is enqueued for immediate execution | `:job` |
1424
+ | `enqueue_at.job.postburner` | When job is enqueued with delay | `:job`, `:scheduled_at` |
1425
+
1426
+ **Job Payload Structure:**
1427
+
1428
+ ```ruby
1429
+ {
1430
+ class: "ProcessPayment", # Job class name
1431
+ id: 123, # Postburner job ID (tracked jobs only)
1432
+ job_id: "abc-123", # ActiveJob UUID
1433
+ arguments: { payment_id: 456 }, # Job arguments
1434
+ queue_name: "critical", # Queue name
1435
+ beanstalk_job_id: 789, # Beanstalkd job ID
1436
+ tracked: true # Whether job is tracked in PostgreSQL
1437
+ }
1438
+ ```
1439
+
1440
+ ### Schedule Events
1441
+
1442
+ | Event | When | Payload Keys |
1443
+ |-------|------|--------------|
1444
+ | `create.schedule.postburner` | When schedule is created | `:schedule` |
1445
+ | `update.schedule.postburner` | When schedule is updated | `:schedule`, `:changes` |
1446
+ | `audit.schedule.postburner` | When scheduler audits a schedule | `:schedule` |
1447
+
1448
+ **Schedule Payload Structure:**
1352
1449
 
1353
- ### Available Events
1450
+ ```ruby
1451
+ {
1452
+ id: 1,
1453
+ name: "daily_cleanup",
1454
+ job_class: "CleanupJob",
1455
+ enabled: true,
1456
+ cron: nil,
1457
+ anchor: "2025-01-01T09:00:00Z",
1458
+ interval: 1,
1459
+ interval_unit: "days"
1460
+ }
1461
+ ```
1462
+
1463
+ ### Schedule Execution Events
1354
1464
 
1355
1465
  | Event | When | Payload Keys |
1356
1466
  |-------|------|--------------|
1357
- | `perform_start.postburner` | Before job execution | `:payload`, `:beanstalk_job_id` |
1358
- | `perform.postburner` | Around job execution (includes duration) | `:payload`, `:beanstalk_job_id` |
1359
- | `retry.postburner` | When default job is retried | `:payload`, `:beanstalk_job_id`, `:error`, `:wait`, `:attempt` |
1360
- | `discard.postburner` | When default job exhausts retries | `:payload`, `:beanstalk_job_id`, `:error` |
1467
+ | `create.schedule_execution.postburner` | When execution is created | `:schedule`, `:execution` |
1468
+ | `enqueue.schedule_execution.postburner` | When execution is enqueued to Beanstalkd | `:schedule`, `:execution`, `:beanstalk_job_id` |
1469
+ | `skip.schedule_execution.postburner` | When execution is skipped | `:schedule`, `:execution` |
1470
+
1471
+ **Execution Payload Structure:**
1472
+
1473
+ ```ruby
1474
+ {
1475
+ id: 42,
1476
+ schedule_id: 1,
1477
+ status: "scheduled",
1478
+ run_at: "2025-01-15T09:00:00Z",
1479
+ next_run_at: "2025-01-16T09:00:00Z",
1480
+ enqueued_at: "2025-01-14T10:00:00Z",
1481
+ beanstalk_job_id: 789,
1482
+ job_id: 123
1483
+ }
1484
+ ```
1485
+
1486
+ ### Scheduler Events
1487
+
1488
+ | Event | When | Payload Keys |
1489
+ |-------|------|--------------|
1490
+ | `perform_start.scheduler.postburner` | Before scheduler watchdog runs | `:interval` |
1491
+ | `perform.scheduler.postburner` | Around scheduler watchdog (includes summary) | `:interval`, `:lock_acquired`, `:schedules_processed`, `:schedules_failed`, `:executions_created`, `:orphans_enqueued` |
1361
1492
 
1362
1493
  ### Subscribing to Events
1363
1494
 
@@ -1365,32 +1496,39 @@ Postburner emits ActiveSupport::Notifications events following ActiveJob convent
1365
1496
  # config/initializers/postburner_instrumentation.rb
1366
1497
 
1367
1498
  # Log all job executions
1368
- ActiveSupport::Notifications.subscribe('perform.postburner') do |name, start, finish, id, payload|
1499
+ ActiveSupport::Notifications.subscribe('perform.job.postburner') do |name, start, finish, id, payload|
1369
1500
  duration = (finish - start) * 1000
1370
- Rails.logger.info "[Postburner] #{payload[:payload]['job_class']} completed in #{duration.round(2)}ms"
1501
+ Rails.logger.info "[Postburner] #{payload[:job][:class]} completed in #{duration.round(2)}ms"
1371
1502
  end
1372
1503
 
1373
1504
  # Alert on discarded jobs
1374
- ActiveSupport::Notifications.subscribe('discard.postburner') do |*args|
1505
+ ActiveSupport::Notifications.subscribe('discard.job.postburner') do |*args|
1375
1506
  payload = args.last
1376
1507
  Alerting.notify(
1377
1508
  "Job discarded after max retries",
1378
- job_class: payload[:payload]['job_class'],
1509
+ job_class: payload[:job][:class],
1379
1510
  error: payload[:error].message
1380
1511
  )
1381
1512
  end
1382
1513
 
1383
1514
  # Track retry metrics
1384
- ActiveSupport::Notifications.subscribe('retry.postburner') do |*args|
1515
+ ActiveSupport::Notifications.subscribe('retry.job.postburner') do |*args|
1385
1516
  payload = args.last
1386
1517
  StatsD.increment('postburner.retry', tags: [
1387
- "job:#{payload[:payload]['job_class']}",
1518
+ "job:#{payload[:job][:class]}",
1388
1519
  "attempt:#{payload[:attempt]}"
1389
1520
  ])
1390
1521
  end
1522
+
1523
+ # Monitor scheduler performance
1524
+ ActiveSupport::Notifications.subscribe('perform.scheduler.postburner') do |name, start, finish, id, payload|
1525
+ duration = (finish - start) * 1000
1526
+ Rails.logger.info "[Scheduler] Processed #{payload[:schedules_processed]} schedules, " \
1527
+ "created #{payload[:executions_created]} executions in #{duration.round(2)}ms"
1528
+ end
1391
1529
  ```
1392
1530
 
1393
- **Note:** These events are emitted by the worker for jobs processed through Beanstalkd. They complement (don't replace) ActiveJob's built-in instrumentation events.
1531
+ **Note:** These events complement (don't replace) ActiveJob's built-in instrumentation events like `enqueue.active_job` and `perform.active_job`.
1394
1532
 
1395
1533
  ## Why Beanstalkd?
1396
1534
 
@@ -1936,49 +2074,6 @@ cp config/postburner.yml.example config/postburner.yml
1936
2074
 
1937
2075
  Edit `config/postburner.yml` for your environment (see [Configuration](#configuration)).
1938
2076
 
1939
- ## Deployment
1940
-
1941
- Some recipies that might be useful or instructive:
1942
-
1943
- ### Docker
1944
-
1945
- ```dockerfile
1946
- # Dockerfile.worker
1947
- FROM ruby:3.3
1948
-
1949
- WORKDIR /app
1950
- COPY . .
1951
- RUN bundle install
1952
-
1953
- CMD ["bundle", "exec", "postburner", "--config", "config/postburner.yml", "--env", "production"]
1954
- ```
1955
-
1956
- ```yaml
1957
- # docker-compose.yml
1958
- services:
1959
- beanstalkd:
1960
- image: schickling/beanstalkd
1961
- command: ["-l", "0.0.0.0", "-p", "11300", "-b", "/var/lib/beanstalkd"]
1962
- ports:
1963
- - "11300:11300"
1964
- volumes:
1965
- - beanstalkd_data:/var/lib/beanstalkd
1966
-
1967
- worker:
1968
- build:
1969
- context: .
1970
- dockerfile: Dockerfile.worker
1971
- depends_on:
1972
- - beanstalkd
1973
- - postgres
1974
- environment:
1975
- BEANSTALK_URL: beanstalk://beanstalkd:11300
1976
- DATABASE_URL: postgres://postgres@postgres/myapp_production
1977
-
1978
- volumes:
1979
- beanstalkd_data:
1980
- ```
1981
-
1982
2077
  ## Migration from v0.x
1983
2078
 
1984
2079
  Key changes in v1.0:
@@ -53,6 +53,9 @@ module Postburner
53
53
  #
54
54
  # @example Queue at specific time
55
55
  # job.queue!(at: Time.zone.now + 2.days)
56
+ # job.queue!(at: Time.zone.parse('2025-01-15 09:00:00'))
57
+ # job.queue!(at: '2025-01-15 09:00:00'.in_time_zone)
58
+ # job.queue!(at: Time.parse('2025-01-15 09:00:00 EST'))
56
59
  #
57
60
  # @example Queue with priority
58
61
  # job.queue!(pri: 0, delay: 30.minutes)
@@ -148,6 +151,10 @@ module Postburner
148
151
  # queueing (Beanstalkd in production, inline execution in test mode).
149
152
  # Updates bkid if the strategy returns a Beanstalkd job ID.
150
153
  #
154
+ # Instruments with ActiveSupport::Notifications:
155
+ # - enqueue.job.postburner: When job is queued immediately
156
+ # - enqueue_at.job.postburner: When job is queued with delay/at
157
+ #
151
158
  # @param options [Hash] Queue options (delay, pri, ttr, etc.)
152
159
  #
153
160
  # @return [Hash, nil] Queue strategy response
@@ -156,7 +163,6 @@ module Postburner
156
163
  #
157
164
  def insert!(options={})
158
165
  response = Postburner.queue_strategy.insert(self, options)
159
- #debugger
160
166
 
161
167
  # Response must be a hash with an :id key (value can be nil)
162
168
  unless response.is_a?(Hash) && response.key?(:id)
@@ -165,6 +171,19 @@ module Postburner
165
171
 
166
172
  persist_metadata!(bkid: response[:id])
167
173
 
174
+ # Instrument enqueue event
175
+ job_payload = Postburner::Instrumentation.job_payload_from_model(self, beanstalk_job_id: response[:id])
176
+ if self.run_at.present? && self.run_at > Time.current
177
+ ActiveSupport::Notifications.instrument('enqueue_at.job.postburner', {
178
+ job: job_payload,
179
+ scheduled_at: self.run_at
180
+ })
181
+ else
182
+ ActiveSupport::Notifications.instrument('enqueue.job.postburner', {
183
+ job: job_payload
184
+ })
185
+ end
186
+
168
187
  self.log("QUEUED: #{response}") if response
169
188
 
170
189
  response
@@ -109,10 +109,15 @@ module Postburner
109
109
  validates :name, presence: true, uniqueness: true
110
110
  validates :job_class, presence: true
111
111
  validates :timezone, presence: true
112
+ validate :validate_timezone_exists!
112
113
  validate :validate_scheduling_mode!
113
114
  validate :validate_job_class_exists!
114
115
  validate :validate_cron_expression!, if: :cron?
115
116
 
117
+ # Instrumentation callbacks
118
+ after_create_commit :instrument_create
119
+ after_update_commit :instrument_update
120
+
116
121
  # Scopes
117
122
  scope :enabled, -> { where(enabled: true) }
118
123
  scope :disabled, -> { where(enabled: false) }
@@ -347,6 +352,9 @@ module Postburner
347
352
  # delayed queue will hold it until run_at. This ensures all future executions
348
353
  # are already queued and ready to go.
349
354
  #
355
+ # Instruments with ActiveSupport::Notifications:
356
+ # - create.schedule_execution.postburner: When execution is created
357
+ #
350
358
  # @param after [Time, ScheduleExecution, nil] Calculate after this time/execution
351
359
  # @return [ScheduleExecution, nil] The created execution, or nil if no more runs
352
360
  #
@@ -357,6 +365,13 @@ module Postburner
357
365
  return nil if execution.nil?
358
366
 
359
367
  execution.save!
368
+
369
+ # Instrument execution creation
370
+ ActiveSupport::Notifications.instrument('create.schedule_execution.postburner', {
371
+ schedule: Postburner::Instrumentation.schedule_payload(self),
372
+ execution: Postburner::Instrumentation.execution_payload(execution)
373
+ })
374
+
360
375
  execution.enqueue!
361
376
  execution
362
377
  end
@@ -387,6 +402,24 @@ module Postburner
387
402
  )
388
403
  end
389
404
 
405
+ # Validate that timezone is a recognized ActiveSupport timezone.
406
+ #
407
+ # Uses Time.find_zone to check if the timezone string resolves to a valid
408
+ # ActiveSupport::TimeZone. Invalid timezones would cause nil pointer errors
409
+ # during schedule calculations.
410
+ #
411
+ # @return [void]
412
+ #
413
+ # @api private
414
+ #
415
+ def validate_timezone_exists!
416
+ return if timezone.blank?
417
+
418
+ if Time.find_zone(timezone).nil?
419
+ errors.add(:timezone, "is not a recognized timezone")
420
+ end
421
+ end
422
+
390
423
  # Validate that exactly one scheduling mode is configured.
391
424
  #
392
425
  # Ensures either anchor+interval+interval_unit OR cron is set, but not both.
@@ -699,5 +732,41 @@ module Postburner
699
732
  n += 1
700
733
  end
701
734
  end
735
+
736
+ # Instrument schedule creation.
737
+ #
738
+ # Emits create.schedule.postburner event with schedule payload and changes.
739
+ #
740
+ # @return [void]
741
+ #
742
+ # @api private
743
+ #
744
+ def instrument_create
745
+ ActiveSupport::Notifications.instrument('create.schedule.postburner', {
746
+ schedule: Postburner::Instrumentation.schedule_payload(self),
747
+ changes: Postburner::Instrumentation.changes_payload(self, exclude: ['last_audit_at'])
748
+ })
749
+ end
750
+
751
+ # Instrument schedule update.
752
+ #
753
+ # Emits update.schedule.postburner event with schedule payload and changes.
754
+ # Excludes last_audit_at from changes to avoid noise from scheduler audits.
755
+ #
756
+ # @return [void]
757
+ #
758
+ # @api private
759
+ #
760
+ def instrument_update
761
+ changes = Postburner::Instrumentation.changes_payload(self, exclude: ['last_audit_at'])
762
+
763
+ # Skip instrumentation if only excluded attributes changed
764
+ return if changes.empty?
765
+
766
+ ActiveSupport::Notifications.instrument('update.schedule.postburner', {
767
+ schedule: Postburner::Instrumentation.schedule_payload(self),
768
+ changes: changes
769
+ })
770
+ end
702
771
  end
703
772
  end
@@ -99,6 +99,9 @@ module Postburner
99
99
  #
100
100
  # This method is idempotent - calling it multiple times will only enqueue once.
101
101
  #
102
+ # Instruments with ActiveSupport::Notifications:
103
+ # - enqueue.schedule_execution.postburner: When execution is enqueued
104
+ #
102
105
  # @return [void]
103
106
  #
104
107
  # @note Creating the next execution is the scheduler's responsibility via
@@ -128,6 +131,13 @@ module Postburner
128
131
  enqueue_default_job!
129
132
  end
130
133
  end
134
+
135
+ # Instrument enqueue event (after transaction commits)
136
+ ActiveSupport::Notifications.instrument('enqueue.schedule_execution.postburner', {
137
+ schedule: Postburner::Instrumentation.schedule_payload(schedule),
138
+ execution: Postburner::Instrumentation.execution_payload(self.reload),
139
+ beanstalk_job_id: beanstalk_job_id
140
+ })
131
141
  end
132
142
 
133
143
  # Skip this execution.
@@ -138,6 +148,9 @@ module Postburner
138
148
  # This is a destructive operation - skipped executions cannot be unskipped.
139
149
  # A new execution must be created if needed.
140
150
  #
151
+ # Instruments with ActiveSupport::Notifications:
152
+ # - skip.schedule_execution.postburner: When execution is skipped
153
+ #
141
154
  # @return [Boolean] true if skipped successfully, false if already skipped
142
155
  #
143
156
  # @example Skip a future execution
@@ -156,6 +169,12 @@ module Postburner
156
169
  update!(status: :skipped)
157
170
  end
158
171
 
172
+ # Instrument skip event (after transaction commits)
173
+ ActiveSupport::Notifications.instrument('skip.schedule_execution.postburner', {
174
+ schedule: Postburner::Instrumentation.schedule_payload(schedule),
175
+ execution: Postburner::Instrumentation.execution_payload(self)
176
+ })
177
+
159
178
  true
160
179
  end
161
180
 
data/bin/postburner CHANGED
@@ -3,14 +3,22 @@
3
3
 
4
4
  # Postburner worker executable
5
5
  #
6
- # Loads configuration from YAML and starts the appropriate worker type.
6
+ # Loads configuration from YAML and starts the appropriate worker.
7
7
  #
8
8
  # Usage:
9
- # bin/postburner [--config PATH] [--env ENVIRONMENT]
9
+ # bin/postburner [options]
10
+ #
11
+ # Options:
12
+ # -c, --config PATH Path to YAML config (default: config/postburner.yml)
13
+ # -e, --env ENVIRONMENT Environment (default: RAILS_ENV, RACK_ENV, or development)
14
+ # -w, --worker WORKER Worker name (required if multiple workers defined)
15
+ # -q, --queues QUEUES Override configured queues (prefer defining workers)
10
16
  #
11
17
  # Examples:
12
18
  # bin/postburner
13
- # bin/postburner --config config/postburner.yml --env production
19
+ # bin/postburner --worker default
20
+ # bin/postburner --env production --worker imports
21
+ # bin/postburner --worker default --queues default,mailers
14
22
  #
15
23
 
16
24
  require 'optparse'
@@ -30,7 +38,7 @@ OptionParser.new do |opts|
30
38
  options[:config] = path
31
39
  end
32
40
 
33
- opts.on('-e', '--env ENVIRONMENT', 'Environment (default: RAILS_ENV or development)') do |env|
41
+ opts.on('-e', '--env ENVIRONMENT', 'Environment (default: RAILS_ENV, RACK_ENV, or development)') do |env|
34
42
  options[:env] = env
35
43
  end
36
44
 
@@ -38,7 +46,7 @@ OptionParser.new do |opts|
38
46
  options[:worker] = worker
39
47
  end
40
48
 
41
- opts.on('-q', '--queues QUEUES', 'Comma-separated list of queues to process (default: all configured queues)') do |queues|
49
+ opts.on('-q', '--queues QUEUES', 'Override configured queues, e.g. --queues default,mailers (prefer defining in yaml file, and selecting sets with --worker)') do |queues|
42
50
  options[:queues] = queues.split(',').map(&:strip)
43
51
  end
44
52