procrastinator 0.6.1 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -0
- data/.ruby-version +1 -1
- data/Gemfile +2 -0
- data/README.md +482 -128
- data/Rakefile +5 -3
- data/lib/procrastinator.rb +39 -18
- data/lib/procrastinator/config.rb +185 -0
- data/lib/procrastinator/loaders/csv_loader.rb +107 -0
- data/lib/procrastinator/queue.rb +50 -0
- data/lib/procrastinator/queue_manager.rb +201 -0
- data/lib/procrastinator/queue_worker.rb +91 -71
- data/lib/procrastinator/scheduler.rb +171 -0
- data/lib/procrastinator/task.rb +46 -0
- data/lib/procrastinator/task_meta_data.rb +128 -0
- data/lib/procrastinator/task_worker.rb +66 -86
- data/lib/procrastinator/version.rb +3 -1
- data/lib/rake/procrastinator_task.rb +34 -0
- data/procrastinator.gemspec +11 -9
- metadata +40 -19
- data/lib/procrastinator/environment.rb +0 -148
@@ -1,116 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Procrastinator
|
4
|
+
# A QueueWorker checks for tasks to run from the loader defined in the provided config and executes them,
|
5
|
+
# updating information in the task loader as necessary.
|
6
|
+
#
|
7
|
+
# @author Robin Miller
|
2
8
|
class QueueWorker
|
3
|
-
|
4
|
-
DEFAULT_MAX_ATTEMPTS = 20
|
5
|
-
DEFAULT_UPDATE_PERIOD = 10 # seconds
|
6
|
-
DEFAULT_MAX_TASKS = 10
|
7
|
-
|
8
|
-
attr_reader :name, :timeout, :max_attempts, :update_period, :max_tasks
|
9
|
-
|
10
|
-
# Timeout is in seconds
|
11
|
-
def initialize(name:,
|
12
|
-
persister:,
|
13
|
-
log_dir: nil,
|
14
|
-
log_level: Logger::INFO,
|
15
|
-
max_attempts: DEFAULT_MAX_ATTEMPTS,
|
16
|
-
timeout: DEFAULT_TIMEOUT,
|
17
|
-
update_period: DEFAULT_UPDATE_PERIOD,
|
18
|
-
max_tasks: DEFAULT_MAX_TASKS)
|
19
|
-
raise ArgumentError.new('Queue name may not be nil') unless name
|
20
|
-
raise ArgumentError.new('Persister may not be nil') unless persister
|
21
|
-
|
22
|
-
raise(MalformedTaskPersisterError.new('The supplied IO object must respond to #read_tasks')) unless persister.respond_to? :read_tasks
|
23
|
-
raise(MalformedTaskPersisterError.new('The supplied IO object must respond to #update_task')) unless persister.respond_to? :update_task
|
24
|
-
raise(MalformedTaskPersisterError.new('The supplied IO object must respond to #delete_task')) unless persister.respond_to? :delete_task
|
25
|
-
|
26
|
-
@name = name.to_s.gsub(/\s/, '_').to_sym
|
27
|
-
@timeout = timeout
|
28
|
-
@max_attempts = max_attempts
|
29
|
-
@update_period = update_period
|
30
|
-
@max_tasks = max_tasks
|
31
|
-
@persister = persister
|
32
|
-
@log_dir = log_dir
|
33
|
-
@log_level = log_level
|
9
|
+
extend Forwardable
|
34
10
|
|
35
|
-
|
11
|
+
def_delegators :@queue, :name
|
12
|
+
|
13
|
+
# expected methods for all persistence strategies
|
14
|
+
PERSISTER_METHODS = [:read, :update, :delete].freeze
|
15
|
+
|
16
|
+
def initialize(queue:, config:, scheduler: nil)
|
17
|
+
@queue = queue
|
18
|
+
@config = config
|
19
|
+
@scheduler = scheduler
|
20
|
+
|
21
|
+
@logger = nil
|
36
22
|
end
|
37
23
|
|
38
24
|
def work
|
25
|
+
start_log
|
26
|
+
|
39
27
|
begin
|
40
28
|
loop do
|
41
|
-
sleep(@update_period)
|
29
|
+
sleep(@queue.update_period)
|
42
30
|
|
43
31
|
act
|
44
32
|
end
|
45
33
|
rescue StandardError => e
|
34
|
+
raise if @config.test_mode? || !@logger
|
35
|
+
|
46
36
|
@logger.fatal(e)
|
47
|
-
# raise e
|
48
37
|
end
|
49
38
|
end
|
50
39
|
|
51
40
|
def act
|
52
|
-
|
53
|
-
# when receiving already sorted data. Ideally, we'd use a better algo, but this will do for now
|
54
|
-
tasks = @persister.read_tasks(@name).reject { |t| t[:run_at].nil? }.shuffle.sort_by { |t| t[:run_at] }
|
41
|
+
persister = @config.loader
|
55
42
|
|
56
|
-
tasks
|
57
|
-
if Time.now.to_i >= task_data[:run_at].to_i
|
58
|
-
task_data.merge!(logger: @logger) if @logger
|
43
|
+
tasks = fetch_tasks(persister)
|
59
44
|
|
60
|
-
|
45
|
+
tasks.each do |metadata|
|
46
|
+
tw = build_worker(metadata)
|
61
47
|
|
62
|
-
|
48
|
+
tw.work
|
63
49
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
end
|
50
|
+
if tw.successful?
|
51
|
+
persister.delete(metadata.id)
|
52
|
+
else
|
53
|
+
persister.update(metadata.id, tw.to_h.merge(queue: @queue.name.to_s))
|
69
54
|
end
|
70
55
|
end
|
71
56
|
end
|
72
57
|
|
73
58
|
def long_name
|
74
|
-
"#{@name}-queue-worker"
|
59
|
+
name = "#{ @queue.name }-queue-worker"
|
60
|
+
|
61
|
+
name = "#{ @config.prefix }-#{ name }" if @config.prefix
|
62
|
+
|
63
|
+
name
|
75
64
|
end
|
76
65
|
|
77
66
|
# Starts a log file and stores the logger within this queue worker.
|
78
67
|
#
|
79
68
|
# Separate from init because logging is context-dependent
|
80
69
|
def start_log
|
81
|
-
if @log_dir
|
82
|
-
log_path = Pathname.new("#{@log_dir}/#{long_name}.log")
|
70
|
+
return if @logger || !@config.log_dir
|
83
71
|
|
84
|
-
|
85
|
-
File.open(log_path.to_path, 'a+') do |f|
|
86
|
-
f.write ''
|
87
|
-
end
|
72
|
+
@logger = Logger.new(log_target, level: @config.log_level)
|
88
73
|
|
89
|
-
|
74
|
+
msg = <<~MSG
|
75
|
+
======================================================================
|
76
|
+
Started worker process, #{ long_name }, to work off queue #{ @queue.name }.
|
77
|
+
Worker pid=#{ Process.pid }; parent pid=#{ Process.ppid }.
|
78
|
+
======================================================================
|
79
|
+
MSG
|
90
80
|
|
91
|
-
|
81
|
+
@logger.info("\n#{ msg }")
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def build_worker(metadata)
|
87
|
+
start_log
|
92
88
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
89
|
+
TaskWorker.new(metadata: metadata,
|
90
|
+
queue: @queue,
|
91
|
+
scheduler: @scheduler,
|
92
|
+
context: @config.context,
|
93
|
+
logger: @logger)
|
94
|
+
end
|
95
|
+
|
96
|
+
def log_target
|
97
|
+
return $stdout if @config.test_mode?
|
98
|
+
|
99
|
+
log_path = @config.log_dir + "#{ long_name }.log"
|
100
|
+
|
101
|
+
write_log_file(log_path)
|
102
|
+
|
103
|
+
log_path.to_path
|
104
|
+
end
|
105
|
+
|
106
|
+
def write_log_file(log_path)
|
107
|
+
@config.log_dir.mkpath
|
108
|
+
File.open(log_path.to_path, 'a+') do |f|
|
109
|
+
f.write ''
|
98
110
|
end
|
99
111
|
end
|
100
112
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
113
|
+
def fetch_tasks(persister)
|
114
|
+
tasks = persister.read(queue: @queue.name).map(&:to_h).reject { |t| t[:run_at].nil? }
|
115
|
+
|
116
|
+
tasks = sort_tasks(tasks)
|
117
|
+
|
118
|
+
metas = tasks.collect do |t|
|
119
|
+
TaskMetaData.new(t.delete_if { |key| !TaskMetaData::EXPECTED_DATA.include?(key) })
|
120
|
+
end
|
121
|
+
|
122
|
+
metas.select(&:runnable?)
|
123
|
+
end
|
109
124
|
|
110
|
-
|
125
|
+
def sort_tasks(tasks)
|
126
|
+
# shuffling and re-sorting to avoid worst case O(n^2) when receiving already sorted data
|
127
|
+
# on quicksort (which is default ruby sort). It is not unreasonable that the persister could return sorted
|
128
|
+
# results
|
129
|
+
# Ideally, we'd use a better algo than qsort for this, but this will do for now
|
130
|
+
tasks.shuffle.sort_by { |t| t[:run_at] }.first(@queue.max_tasks)
|
111
131
|
end
|
112
132
|
end
|
113
133
|
|
114
134
|
class MalformedTaskPersisterError < StandardError
|
115
135
|
end
|
116
|
-
end
|
136
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Procrastinator
|
4
|
+
# A Scheduler object provides the API for client applications to manage delayed tasks.
|
5
|
+
#
|
6
|
+
# Use #delay to schedule new tasks, #reschedule to alter existing tasks, and #cancel to remove unwanted tasks.
|
7
|
+
#
|
8
|
+
# @author Robin Miller
|
9
|
+
class Scheduler
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def_delegators :@queue_manager, :act
|
13
|
+
|
14
|
+
def initialize(config, queue_manager)
|
15
|
+
@config = config
|
16
|
+
@queue_manager = queue_manager
|
17
|
+
end
|
18
|
+
|
19
|
+
# Records a new task to be executed at the given time.
|
20
|
+
#
|
21
|
+
# @param queue [Symbol] the symbol identifier for the queue to add a new task on
|
22
|
+
# @param run_at [Time, Integer] Optional time when this task should be executed. Defaults to the current time.
|
23
|
+
# @param data [Hash, Array] Optional simple data object to be provided to the task upon execution.
|
24
|
+
# @param expire_at [Time, Integer] Optional time when the task should be abandoned
|
25
|
+
def delay(queue = nil, data: nil, run_at: Time.now.to_i, expire_at: nil)
|
26
|
+
verify_queue_arg!(queue)
|
27
|
+
|
28
|
+
queue = @config.queue.name if @config.single_queue?
|
29
|
+
|
30
|
+
verify_queue_data!(queue, data)
|
31
|
+
|
32
|
+
loader.create(queue: queue.to_s,
|
33
|
+
run_at: run_at.to_i,
|
34
|
+
initial_run_at: run_at.to_i,
|
35
|
+
expire_at: expire_at.nil? ? nil : expire_at.to_i,
|
36
|
+
data: YAML.dump(data))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Alters an existing task to run at a new time, expire at a new time, or both.
|
40
|
+
#
|
41
|
+
# Call #to on the result and pass in the new :run_at and/or :expire_at.
|
42
|
+
#
|
43
|
+
# Example:
|
44
|
+
#
|
45
|
+
# scheduler.reschedule(:alerts, data: {user_id: 5}).to(run_at: Time.now, expire_at: Time.now + 10)
|
46
|
+
#
|
47
|
+
# The identifier can include any data field stored in the task loader. Often this is the information in :data.
|
48
|
+
#
|
49
|
+
# @param queue [Symbol] the symbol identifier for the queue to add a new task on
|
50
|
+
# @param identifier [Hash] Some identifying information to find the appropriate task.
|
51
|
+
#
|
52
|
+
# @see TaskMetaData
|
53
|
+
def reschedule(queue, identifier)
|
54
|
+
UpdateProxy.new(@config, identifier: identifier.merge(queue: queue.to_s))
|
55
|
+
end
|
56
|
+
|
57
|
+
# Removes an existing task, as located by the givne identifying information.
|
58
|
+
#
|
59
|
+
# The identifier can include any data field stored in the task loader. Often this is the information in :data.
|
60
|
+
#
|
61
|
+
# @param queue [Symbol] the symbol identifier for the queue to add a new task on
|
62
|
+
# @param identifier [Hash] Some identifying information to find the appropriate task.
|
63
|
+
#
|
64
|
+
# @see TaskMetaData
|
65
|
+
def cancel(queue, identifier)
|
66
|
+
tasks = loader.read(identifier.merge(queue: queue.to_s))
|
67
|
+
|
68
|
+
raise "no task matches search: #{ identifier }" if tasks.empty?
|
69
|
+
raise "multiple tasks match search: #{ identifier }" if tasks.size > 1
|
70
|
+
|
71
|
+
loader.delete(tasks.first[:id])
|
72
|
+
end
|
73
|
+
|
74
|
+
# Provides a more natural syntax for rescheduling tasks
|
75
|
+
#
|
76
|
+
# @see Scheduler#reschedule
|
77
|
+
class UpdateProxy
|
78
|
+
def initialize(config, identifier:)
|
79
|
+
identifier[:data] = YAML.dump(identifier[:data]) if identifier[:data]
|
80
|
+
|
81
|
+
@config = config
|
82
|
+
@identifier = identifier
|
83
|
+
end
|
84
|
+
|
85
|
+
def to(run_at: nil, expire_at: nil)
|
86
|
+
task = fetch_task(@identifier)
|
87
|
+
|
88
|
+
verify_time_provided(run_at, expire_at)
|
89
|
+
validate_run_at(run_at, task[:expire_at], expire_at)
|
90
|
+
|
91
|
+
new_data = {
|
92
|
+
attempts: 0,
|
93
|
+
last_error: nil,
|
94
|
+
last_error_at: nil
|
95
|
+
}
|
96
|
+
|
97
|
+
new_data = new_data.merge(run_at: run_at.to_i, initial_run_at: run_at.to_i) if run_at
|
98
|
+
new_data = new_data.merge(expire_at: expire_at.to_i) if expire_at
|
99
|
+
|
100
|
+
@config.loader.update(task[:id], new_data)
|
101
|
+
end
|
102
|
+
|
103
|
+
alias at to
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def verify_time_provided(run_at, expire_at)
|
108
|
+
raise ArgumentError, 'you must provide at least :run_at or :expire_at' if run_at.nil? && expire_at.nil?
|
109
|
+
end
|
110
|
+
|
111
|
+
def validate_run_at(run_at, saved_expire_at, expire_at)
|
112
|
+
return unless run_at
|
113
|
+
|
114
|
+
after_new_expire = expire_at && run_at.to_i > expire_at.to_i
|
115
|
+
|
116
|
+
raise "given run_at (#{ run_at }) is later than given expire_at (#{ expire_at })" if after_new_expire
|
117
|
+
|
118
|
+
after_old_expire = saved_expire_at && run_at.to_i > saved_expire_at
|
119
|
+
|
120
|
+
raise "given run_at (#{ run_at }) is later than saved expire_at (#{ saved_expire_at })" if after_old_expire
|
121
|
+
end
|
122
|
+
|
123
|
+
def fetch_task(identifier)
|
124
|
+
tasks = @config.loader.read(identifier)
|
125
|
+
|
126
|
+
raise "no task found matching #{ identifier }" if tasks.nil? || tasks.empty?
|
127
|
+
raise "too many (#{ tasks.size }) tasks match #{ identifier }. Found: #{ tasks }" if tasks.size > 1
|
128
|
+
|
129
|
+
tasks.first
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
# Scheduler must always get the loader indirectly. If it saves the loader to an instance variable,
|
136
|
+
# then that could hold a reference to a bad (ie. gone) connection on the previous process
|
137
|
+
def loader
|
138
|
+
@config.loader
|
139
|
+
end
|
140
|
+
|
141
|
+
def verify_queue_arg!(queue_name)
|
142
|
+
raise ArgumentError, <<~ERR if !queue_name.nil? && !queue_name.is_a?(Symbol)
|
143
|
+
must provide a queue name as the first argument. Received: #{ queue_name }
|
144
|
+
ERR
|
145
|
+
|
146
|
+
raise ArgumentError, <<~ERR if queue_name.nil? && !@config.single_queue?
|
147
|
+
queue must be specified when more than one is registered. Defined queues are: #{ @config.queues_string }
|
148
|
+
ERR
|
149
|
+
end
|
150
|
+
|
151
|
+
def verify_queue_data!(queue_name, data)
|
152
|
+
queue = @config.queue(name: queue_name)
|
153
|
+
|
154
|
+
unless queue
|
155
|
+
queue_list = @config.queues_string
|
156
|
+
raise ArgumentError, "there is no :#{ queue_name } queue registered. Defined queues are: #{ queue_list }"
|
157
|
+
end
|
158
|
+
|
159
|
+
if data.nil?
|
160
|
+
if queue.task_class.method_defined?(:data=)
|
161
|
+
raise ArgumentError, "task #{ queue.task_class } expects to receive :data. Provide :data to #delay."
|
162
|
+
end
|
163
|
+
elsif !queue.task_class.method_defined?(:data=)
|
164
|
+
raise ArgumentError, <<~ERROR
|
165
|
+
task #{ queue.task_class } does not import :data. Add this in your class definition:
|
166
|
+
task_attr :data
|
167
|
+
ERROR
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Procrastinator
|
4
|
+
# Module to be included by user-defined task classes. It provides some extra error checking and a convenient way
|
5
|
+
# for the task class to access additional information (data, logger, etc) from Procrastinator.
|
6
|
+
#
|
7
|
+
# If you are averse to including this in your task class, you can just declare an attr_accessor for the
|
8
|
+
# information you want Procrastinator to feed your task.
|
9
|
+
#
|
10
|
+
# @author Robin Miller
|
11
|
+
module Task
|
12
|
+
KNOWN_ATTRIBUTES = [:logger, :context, :data, :scheduler].freeze
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
base.extend(TaskClassMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
def respond_to_missing?(name, include_private)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(method_name, *args, &block)
|
23
|
+
if KNOWN_ATTRIBUTES.include?(method_name)
|
24
|
+
raise NameError, "To access Procrastinator::Task attribute :#{ method_name }, " \
|
25
|
+
"call task_attr(:#{ method_name }) in your class definition."
|
26
|
+
end
|
27
|
+
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
# Module that provides the task_attr class method for task definitions to declare their expected information.
|
32
|
+
module TaskClassMethods
|
33
|
+
def task_attr(*fields)
|
34
|
+
attr_list = KNOWN_ATTRIBUTES.collect { |a| ':' + a.to_s }.join(', ')
|
35
|
+
|
36
|
+
fields.each do |field|
|
37
|
+
err = "Unknown Procrastinator::Task attribute :#{ field }. " \
|
38
|
+
"Importable attributes are: #{ attr_list }"
|
39
|
+
raise ArgumentError, err unless KNOWN_ATTRIBUTES.include?(field)
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_accessor(*fields)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Procrastinator
|
4
|
+
# TaskMetaData objects are State Patterns that record information about the work done on a particular task.
|
5
|
+
#
|
6
|
+
# It contains the specific information needed to run a task instance. Users define a task class, which describes
|
7
|
+
# the "how" of a task and TaskMetaData represents the "what" and "when".
|
8
|
+
#
|
9
|
+
# It contains task-specific data, timing information, and error records.
|
10
|
+
#
|
11
|
+
# All of its state is read-only.
|
12
|
+
#
|
13
|
+
# @author Robin Miller
|
14
|
+
#
|
15
|
+
# @!attribute [r] :id
|
16
|
+
# @return [Integer] the unique identifier for this task
|
17
|
+
# @!attribute [r] :run_at
|
18
|
+
# @return [Integer] Linux epoch timestamp of when to attempt this task next
|
19
|
+
# @!attribute [r] :initial_run_at
|
20
|
+
# @return [Integer] Linux epoch timestamp of the original value for run_at
|
21
|
+
# @!attribute [r] :expire_at
|
22
|
+
# @return [Integer] Linux epoch timestamp of when to consider this task obsolete
|
23
|
+
# @!attribute [r] :attempts
|
24
|
+
# @return [Integer] The number of times this task has been attempted
|
25
|
+
# @!attribute [r] :last_error
|
26
|
+
# @return [String] The message and stack trace of the error encountered on the most recent failed attempt
|
27
|
+
# @!attribute [r] :last_fail_at
|
28
|
+
# @return [Integer] Linux epoch timestamp of when the last_error was recorded
|
29
|
+
# @!attribute [r] :data
|
30
|
+
# @return [String] User-provided data serialized to string using YAML
|
31
|
+
class TaskMetaData
|
32
|
+
# These are the attributes expected to be in the persistence mechanism
|
33
|
+
EXPECTED_DATA = [:id, :run_at, :initial_run_at, :expire_at, :attempts, :last_error, :last_fail_at, :data].freeze
|
34
|
+
|
35
|
+
attr_reader(*EXPECTED_DATA)
|
36
|
+
|
37
|
+
def initialize(id: nil,
|
38
|
+
run_at: nil,
|
39
|
+
initial_run_at: nil,
|
40
|
+
expire_at: nil,
|
41
|
+
attempts: 0,
|
42
|
+
last_error: nil,
|
43
|
+
last_fail_at: nil,
|
44
|
+
data: nil)
|
45
|
+
@id = id
|
46
|
+
@run_at = run_at.nil? ? nil : run_at.to_i
|
47
|
+
@initial_run_at = initial_run_at.to_i
|
48
|
+
@expire_at = expire_at.nil? ? nil : expire_at.to_i
|
49
|
+
@attempts = attempts || 0
|
50
|
+
@last_error = last_error
|
51
|
+
@last_fail_at = last_fail_at
|
52
|
+
@data = data ? YAML.safe_load(data, [Symbol, Date]) : nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def init_task(queue)
|
56
|
+
@data ? queue.task_class.new(@data) : queue.task_class.new
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_attempt
|
60
|
+
@attempts += 1
|
61
|
+
end
|
62
|
+
|
63
|
+
def clear_fails
|
64
|
+
@last_error = nil
|
65
|
+
@last_fail_at = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def fail(msg, final: false)
|
69
|
+
@last_fail_at = Time.now.to_i
|
70
|
+
@last_error = msg
|
71
|
+
@run_at = nil if final
|
72
|
+
end
|
73
|
+
|
74
|
+
def final_fail?(queue)
|
75
|
+
too_many_fails?(queue) || expired?
|
76
|
+
end
|
77
|
+
|
78
|
+
def expired?
|
79
|
+
!@expire_at.nil? && Time.now.to_i > @expire_at
|
80
|
+
end
|
81
|
+
|
82
|
+
def too_many_fails?(queue)
|
83
|
+
!queue.max_attempts.nil? && @attempts >= queue.max_attempts
|
84
|
+
end
|
85
|
+
|
86
|
+
def runnable?
|
87
|
+
!(@run_at.nil? || Time.now.to_i < @run_at)
|
88
|
+
end
|
89
|
+
|
90
|
+
def successful?
|
91
|
+
raise 'you cannot check for success before running #work' if !expired? && @attempts <= 0
|
92
|
+
|
93
|
+
!expired? && @last_error.nil? && @last_fail_at.nil?
|
94
|
+
end
|
95
|
+
|
96
|
+
# TODO: This cop for ** is currently incorrect. This disable can be removed once they fix it.
|
97
|
+
# rubocop:disable Layout/SpaceAroundOperators
|
98
|
+
def reschedule
|
99
|
+
# (30 + n_attempts^4) seconds is chosen to rapidly expand
|
100
|
+
# but with the baseline of 30s to avoid hitting the disk too frequently.
|
101
|
+
@run_at += 30 + (@attempts ** 4) unless @run_at.nil?
|
102
|
+
end
|
103
|
+
|
104
|
+
# rubocop:enable Layout/SpaceAroundOperators
|
105
|
+
|
106
|
+
def serialized_data
|
107
|
+
YAML.dump(@data)
|
108
|
+
end
|
109
|
+
|
110
|
+
def verify_expiry!
|
111
|
+
raise TaskExpiredError, "task is over its expiry time of #{ @expire_at }" if expired?
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_h
|
115
|
+
{id: @id,
|
116
|
+
run_at: @run_at,
|
117
|
+
initial_run_at: @initial_run_at,
|
118
|
+
expire_at: @expire_at,
|
119
|
+
attempts: @attempts,
|
120
|
+
last_fail_at: @last_fail_at,
|
121
|
+
last_error: @last_error,
|
122
|
+
data: serialized_data}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class TaskExpiredError < StandardError
|
127
|
+
end
|
128
|
+
end
|