Class: Postburner::ScheduleExecution
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Postburner::ScheduleExecution
- Defined in:
- app/models/postburner/schedule_execution.rb
Overview
ScheduleExecution represents a single scheduled run of a recurring job.
Executions are created in advance and immediately enqueued to Beanstalkd’s delayed queue. This ensures all future runs are already queued and ready to execute at the scheduled time, regardless of scheduler availability.
## Lifecycle
-
Execution created with ‘pending` status and immediately enqueued to Beanstalkd
-
Status changes to ‘scheduled` once enqueued
-
At ‘run_at` time, Beanstalkd releases job to worker
-
For Postburner::Job schedules: ‘before_attempt` callback creates next execution
-
Job completes (tracked in job record, not execution)
-
Watchdog periodically verifies future executions exist (safety net)
## Status Values
-
‘pending` - Created but not yet enqueued to Beanstalkd
-
‘scheduled` - Enqueued to Beanstalkd, waiting for execution
-
‘skipped` - Cancelled by admin before execution
Note: We don’t track running/completed/failed states here because the actual job (Postburner::Job or ActiveJob) handles its own lifecycle. The execution’s job is done once it’s scheduled.
## Database Fields
-
‘schedule_id` - Foreign key to parent schedule
-
‘run_at` - Scheduled execution time
-
‘next_run_at` - Pre-calculated next run time for validation
-
‘enqueued_at` - When job was queued to Beanstalkd
-
‘status` - Current status (pending, scheduled, skipped)
-
‘beanstalk_job_id` - Beanstalkd job ID (for tracking/cancellation)
-
‘job_id` - Postburner::Job ID (if using Postburner::Job subclass)
-
‘cached_schedule` - JSONB snapshot of schedule at creation time
## Immediate Enqueue Architecture
Unlike traditional schedulers that poll for due jobs, Postburner immediately enqueues executions to Beanstalkd’s delayed queue when created. This means:
-
Jobs are already queued and will run at run_at regardless of scheduler status
-
No polling overhead - Beanstalkd handles delayed delivery
-
Watchdog only acts as safety net to ensure future executions exist
Defined Under Namespace
Classes: NextExecutionRunAtConflict
Instance Method Summary collapse
-
#enqueue! ⇒ void
Enqueue this execution to Beanstalkd.
-
#enqueued? ⇒ Boolean
Check if execution has been enqueued to Beanstalkd.
-
#skip! ⇒ Boolean
Skip this execution.
-
#validate_next_execution!(next_execution) ⇒ Boolean
Validate that the next execution matches the cached next_run_at.
Instance Method Details
#enqueue! ⇒ void
Creating the next execution is the scheduler’s responsibility via Schedule#create_next_execution!, not this method’s responsibility.
This method returns an undefined value.
Enqueue this execution to Beanstalkd.
Creates the appropriate job (ActiveJob or Postburner::Job) and queues it to Beanstalkd at the scheduled run_at time with appropriate delay.
The job type is determined by the job_class in cached_schedule:
-
Postburner::Job subclasses: Creates tracked job with database record
-
ActiveJob classes: Enqueues directly without database tracking
This method is idempotent - calling it multiple times will only enqueue once.
Instruments with ActiveSupport::Notifications:
-
enqueue.schedule_execution.postburner: When execution is enqueued
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'app/models/postburner/schedule_execution.rb', line 115 def enqueue! return if enqueued? # Mark as scheduled BEFORE Beanstalkd operations to prevent duplicates. # If Beanstalkd put fails, we have a scheduled execution with no beanstalk_job_id, # which the watchdog can detect and re-queue. This is safer than having a # pending execution with a job already in Beanstalkd (which causes duplicates). transaction do update_columns( enqueued_at: Time.current, status: self.class.statuses[:scheduled] ) if tracked_job? enqueue_tracked_job! else enqueue_default_job! end end # Instrument enqueue event (after transaction commits) ActiveSupport::Notifications.instrument('enqueue.schedule_execution.postburner', { schedule: Postburner::Instrumentation.schedule_payload(schedule), execution: Postburner::Instrumentation.execution_payload(self.reload), beanstalk_job_id: beanstalk_job_id }) end |
#enqueued? ⇒ Boolean
Check if execution has been enqueued to Beanstalkd.
87 88 89 |
# File 'app/models/postburner/schedule_execution.rb', line 87 def enqueued? beanstalk_job_id.present? && enqueued_at.present? end |
#skip! ⇒ Boolean
Skip this execution.
Cancels the Beanstalkd job if already enqueued and marks the execution as skipped. Use this when an admin wants to cancel a scheduled execution.
This is a destructive operation - skipped executions cannot be unskipped. A new execution must be created if needed.
Instruments with ActiveSupport::Notifications:
-
skip.schedule_execution.postburner: When execution is skipped
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'app/models/postburner/schedule_execution.rb', line 160 def skip! return false if skipped? transaction do # Cancel the Beanstalkd job if it exists if beanstalk_job_id.present? cancel_beanstalk_job! end update!(status: :skipped) end # Instrument skip event (after transaction commits) ActiveSupport::Notifications.instrument('skip.schedule_execution.postburner', { schedule: Postburner::Instrumentation.schedule_payload(schedule), execution: Postburner::Instrumentation.execution_payload(self) }) true end |
#validate_next_execution!(next_execution) ⇒ Boolean
Validate that the next execution matches the cached next_run_at.
Similar to Flex::SubscriptionIteration validation. Ensures that the pre-calculated next_run_at in this execution matches the actual run_at of the next execution created.
This validation helps detect calculation bugs or manual tampering with execution records.
199 200 201 202 203 204 205 206 207 208 209 |
# File 'app/models/postburner/schedule_execution.rb', line 199 def validate_next_execution!(next_execution) return true if next_run_at.nil? # Check if the run_at matches down to the second if next_execution.run_at.to_i == Time.parse(next_run_at.to_s).to_i return true end raise NextExecutionRunAtConflict, "ScheduleExecution #{id} has next_run_at #{next_run_at} but next execution has run_at #{next_execution.run_at}" end |