class Procrastinator::TaskMetaData

TaskMetaData objects are State Patterns that record information about the work done on a particular task.

It contains the specific information needed to run a task instance. Users define a task handler class, which describes the “how” of a task and TaskMetaData represents the “what” and “when”.

It contains task-specific data, timing information, and error records.

All of its state is read-only.

@author Robin Miller

@!attribute [r] :id

@return [Integer] the unique identifier for this task

@!attribute [r] :run_at

@return [Integer] Linux epoch timestamp of when to attempt this task next

@!attribute [r] :initial_run_at

@return [Integer] Linux epoch timestamp of the original value for run_at

@!attribute [r] :expire_at

@return [Integer] Linux epoch timestamp of when to consider this task obsolete

@!attribute [r] :attempts

@return [Integer] The number of times this task has been attempted

@!attribute [r] :last_error

@return [String] The message and stack trace of the error encountered on the most recent failed attempt

@!attribute [r] :last_fail_at

@return [Integer] Linux epoch timestamp of when the last_error was recorded

@!attribute [r] :data

@return [String] App-provided JSON data

Constants

EXPECTED_DATA

These are the attributes expected to be in the persistence mechanism

Public Class Methods

new(id: nil, queue: nil, data: nil, run_at: nil, initial_run_at: nil, expire_at: nil, attempts: 0, last_error: nil, last_fail_at: nil) click to toggle source
# File lib/procrastinator/task_meta_data.rb, line 39
def initialize(id: nil, queue: nil, data: nil,
               run_at: nil, initial_run_at: nil, expire_at: nil,
               attempts: 0, last_error: nil, last_fail_at: nil)
   @id             = id
   @queue          = queue || raise(ArgumentError, 'queue cannot be nil')
   @run_at         = get_time(run_at)
   @initial_run_at = get_time(initial_run_at) || @run_at
   @expire_at      = get_time(expire_at)
   @attempts       = (attempts || 0).to_i
   @last_error     = last_error
   @last_fail_at   = get_time(last_fail_at)
   @data           = data ? JSON.parse(data, symbolize_names: true) : nil
end

Public Instance Methods

add_attempt() click to toggle source

Increases the number of attempts on this task by one, unless the limit has been reached.

@raise [Task::AttemptsExhaustedError] when the number of attempts has exceeded the Queue’s defined maximum.

# File lib/procrastinator/task_meta_data.rb, line 56
def add_attempt
   raise Task::AttemptsExhaustedError unless attempts_left?

   @attempts += 1
end
attempts_left?() click to toggle source

@return [Boolean] whether there are attempts left until the Queue’s defined maximum is reached (if any)

# File lib/procrastinator/task_meta_data.rb, line 89
def attempts_left?
   @queue.max_attempts.nil? || @attempts < @queue.max_attempts
end
clear_fails() click to toggle source

Resets the last failure time and error.

# File lib/procrastinator/task_meta_data.rb, line 148
def clear_fails
   @last_error   = nil
   @last_fail_at = nil
end
expired?() click to toggle source

@return [Boolean] whether the task is expired

# File lib/procrastinator/task_meta_data.rb, line 84
def expired?
   !@expire_at.nil? && @expire_at < Time.now
end
failure(error) click to toggle source

Records a failure on this task

@param error [StandardError] The error to record

# File lib/procrastinator/task_meta_data.rb, line 65
def failure(error)
   @last_fail_at = Time.now
   @last_error   = %[Task failed: #{ error.message }\n#{ error.backtrace&.join("\n") }]

   if retryable?
      reschedule
      :fail
   else
      @run_at = nil
      :final_fail
   end
end
reschedule(run_at: nil, expire_at: nil) click to toggle source

Updates the run and/or expiry time. If neither is provided, will reschedule based on the rescheduling calculation algorithm.

@param run_at - the new time to run this task @param expire_at - the new time to expire this task

# File lib/procrastinator/task_meta_data.rb, line 111
def reschedule(run_at: nil, expire_at: nil)
   validate_run_at(run_at, expire_at)

   @expire_at = expire_at if expire_at

   if run_at
      @run_at = @initial_run_at = get_time(run_at)
      clear_fails
      @attempts = 0
   end

   return if run_at || expire_at

   # (30 + n_attempts^4) seconds is chosen to rapidly expand
   # but with the baseline of 30s to avoid hitting the disk too frequently.
   @run_at += 30 + (@attempts ** 4) unless @run_at.nil?
end
retryable?() click to toggle source

@return [Boolean] whether the task has attempts left and is not expired

# File lib/procrastinator/task_meta_data.rb, line 79
def retryable?
   attempts_left? && !expired?
end
runnable?() click to toggle source

@return [Boolean] whether the task’s run_at is exceeded

# File lib/procrastinator/task_meta_data.rb, line 94
def runnable?
   !@run_at.nil? && @run_at <= Time.now
end
serialized_data() click to toggle source

@return [String] :data serialized as a JSON string

# File lib/procrastinator/task_meta_data.rb, line 143
def serialized_data
   JSON.dump(@data)
end
successful?() click to toggle source

@return [Boolean] whether the task’s last execution completed successfully. @raise [RuntimeError] when the task has not been attempted yet or when it is expired

# File lib/procrastinator/task_meta_data.rb, line 100
def successful?
   raise 'you cannot check for success before running #work' if !expired? && @attempts <= 0

   !expired? && @last_error.nil? && @last_fail_at.nil?
end
to_h() click to toggle source

@return [Hash] representation of the task metadata as a hash

# File lib/procrastinator/task_meta_data.rb, line 130
def to_h
   {id:             @id,
    queue:          @queue.name.to_s,
    run_at:         @run_at,
    initial_run_at: @initial_run_at,
    expire_at:      @expire_at,
    attempts:       @attempts,
    last_fail_at:   @last_fail_at,
    last_error:     @last_error,
    data:           serialized_data}
end

Private Instance Methods

get_time(data) click to toggle source
# File lib/procrastinator/task_meta_data.rb, line 155
def get_time(data)
   case data
   when NilClass
      nil
   when Numeric
      Time.at data
   when String
      Time.parse data
   when Time
      data
   else
      return data.to_time if data.respond_to? :to_time

      raise ArgumentError, "Unknown data type: #{ data.class } (#{ data })"
   end
end
validate_run_at(run_at, expire_at) click to toggle source
# File lib/procrastinator/task_meta_data.rb, line 172
def validate_run_at(run_at, expire_at)
   return unless run_at

   if expire_at && run_at > expire_at
      raise ArgumentError, "new run_at (#{ run_at }) is later than new expire_at (#{ expire_at })"
   end

   return unless @expire_at && run_at > @expire_at

   raise ArgumentError, "new run_at (#{ run_at }) is later than existing expire_at (#{ @expire_at })"
end