Class: Postburner::OrphanedJob

Inherits:
Job show all
Defined in:
app/models/postburner/orphaned_job.rb

Overview

Inert STI placeholder loaded when a Postburner::Job row's type column references a class that no longer exists in the application.

Why readonly?

Rails' ensure_proper_type (called via save/update) would overwrite the original type value with 'Postburner::OrphanedJob', permanently destroying the record of which class was missing. readonly? returning true prevents all save/update paths and therefore blocks ensure_proper_type from running.

Rails' instantiate reads the type column verbatim when loading records from the database and does NOT call ensure_proper_type, so the original class name is preserved on load. This class is only ever instantiated via the find_sti_class fallback in Job, never created directly.

Why remove! is overridden here

In Rails 8.1+, update_column respects readonly? and raises ActiveRecord::ReadOnlyRecord. The base Commands#remove! calls update_column(:removed_at, ...) — so it would be blocked too. This subclass overrides remove! to use self.class.where(id: self.id).update_all(...) which operates at the relation level (bypasses both readonly? and all instance callbacks) while still soft-deleting the row. Admins can therefore safely soft-remove orphaned rows without restoring the missing class.

Usage

This class is loaded automatically by Job.find_sti_class when a row's type is unresolvable. You should never instantiate it directly in application code.

Examples:

Typical lifecycle

# A DB row exists with type: 'MyApp::RemovedJob' (class deleted)
job = Postburner::Job.find(42)
job.class         # => Postburner::OrphanedJob
job.type          # => 'MyApp::RemovedJob'
job.orphaned?     # => true
job.missing_class_name  # => 'MyApp::RemovedJob'
job.perform             # => raises Postburner::Job::OrphanedJobError
job.remove!             # soft-deletes fine despite readonly?

Instance Attribute Summary

Attributes inherited from Job

#args, #attempt_count, #attempting_at, #attempts, #bkid, #duration, #errata, #error_count, #id, #lag, #log_count, #logs, #processed_at, #processing_at, #queued_at, #removed_at, #run_at, #sid, #type

Instance Method Summary collapse

Methods inherited from Job

#bk, #bk!, #bk=, #destroy!, find_sti_class

Methods included from Statistics

#elapsed_ms, #intended_at, #stats

Methods included from Execution

#perform!

Methods included from Insertion

#will_insert?

Methods included from Commands

#delete!, #extend!, #kick!

Methods included from Logging

#log, #log!, #log_exception, #log_exception!

Methods included from Properties

#priority, #queue_name, #retry_delay_for_attempt, #should_retry?, #ttr, #tube_name

Instance Method Details

#destroyself

Hard-deletes this orphaned row, removing BOTH its Beanstalkd job and its AR row, while keeping readonly? true for saves/updates.

The base ActiveRecord#destroy raises ReadOnlyRecord on a readonly record, which would make ScheduleExecution#teardown_job! (which calls job.destroy uniformly across job shapes) fail for an execution whose job class was deleted/renamed. This override mirrors #remove!: it removes the Beanstalkd job via Commands#delete! and deletes the row at the relation level — using the STI base class so the missing type value doesn't scope the row out — bypassing both readonly? and instance callbacks. The row is genuinely removed (not a soft-delete), matching destroy semantics.

Returns:

  • (self)

    frozen, with destroyed? == true



118
119
120
121
122
123
# File 'app/models/postburner/orphaned_job.rb', line 118

def destroy
  self.delete!
  self.class.base_class.where(id: self.id).delete_all
  @destroyed = true
  freeze
end

#missing_class_nameString

Returns the original class name stored in the type column — the deleted or renamed class that this placeholder stands in for.

Returns:

  • (String)


51
52
53
# File 'app/models/postburner/orphaned_job.rb', line 51

def missing_class_name
  self.type.to_s
end

#orphaned?Boolean

Always true; identifies this instance as an orphaned placeholder.

Returns:

  • (Boolean)


59
# File 'app/models/postburner/orphaned_job.rb', line 59

def orphaned? = true

#performObject

Raises Job::OrphanedJobError because the original job class no longer exists and there is no implementation to run.

Raises:



66
67
68
69
# File 'app/models/postburner/orphaned_job.rb', line 66

def perform(*)
  raise Postburner::Job::OrphanedJobError,
    "Cannot perform orphaned job ##{self.id}: class '#{self.missing_class_name}' no longer exists"
end

#queue!Object Also known as: requeue!

Raises Job::OrphanedJobError because re-enqueuing a job whose class is gone would only produce another unperformable entry.

Raises:



76
77
78
79
# File 'app/models/postburner/orphaned_job.rb', line 76

def queue!(*)
  raise Postburner::Job::OrphanedJobError,
    "Cannot enqueue orphaned job ##{self.id}: class '#{self.missing_class_name}' no longer exists"
end

#readonly?Boolean

Prevents saves that would allow Rails' ensure_proper_type to overwrite the original type value with 'Postburner::OrphanedJob'.

Returns:

  • (Boolean)

    always true



130
# File 'app/models/postburner/orphaned_job.rb', line 130

def readonly? = true

#remove!void

This method returns an undefined value.

Soft-deletes this orphaned row by setting removed_at, bypassing the readonly? guard via a relation-level update_all call (which does not go through instance save/update and therefore does not trigger ensure_proper_type either).

Uses the STI base class for the relation so the row's missing type value (which differs from OrphanedJob's sti_name) does not scope it out — a self.class.where would match zero rows and silently fail to persist.

Idempotent: does nothing if already removed.



95
96
97
98
99
100
101
102
# File 'app/models/postburner/orphaned_job.rb', line 95

def remove!
  return if self.removed_at

  self.delete!
  now = Time.current
  self.class.base_class.where(id: self.id).update_all(removed_at: now)
  self.removed_at = now
end