procrastinator 1.0.0.pre.rc4 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +285 -294
- data/RELEASE_NOTES.md +24 -5
- data/Rakefile +7 -0
- data/lib/procrastinator/config.rb +34 -3
- data/lib/procrastinator/logged_task.rb +2 -4
- data/lib/procrastinator/queue.rb +29 -5
- data/lib/procrastinator/queue_worker.rb +4 -0
- data/lib/procrastinator/rake/daemon_tasks.rb +16 -9
- data/lib/procrastinator/rspec/matchers.rb +30 -0
- data/lib/procrastinator/scheduler.rb +21 -13
- data/lib/procrastinator/task.rb +12 -0
- data/lib/procrastinator/task_meta_data.rb +12 -0
- data/lib/procrastinator/task_store/file_transaction.rb +53 -55
- data/lib/procrastinator/task_store/simple_comma_store.rb +33 -9
- data/lib/procrastinator/test/mocks.rb +9 -0
- data/lib/procrastinator/version.rb +2 -1
- data/procrastinator.gemspec +1 -0
- metadata +19 -5
- data/.travis.yml +0 -4
data/RELEASE_NOTES.md
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
# Release Notes
|
2
2
|
|
3
|
-
## 1.0.
|
3
|
+
## 1.0.1 (2022-09-20)
|
4
|
+
|
5
|
+
### Major Changes
|
6
|
+
|
7
|
+
* none
|
8
|
+
|
9
|
+
### Minor Changes
|
10
|
+
|
11
|
+
* none
|
12
|
+
|
13
|
+
### Bugfixes
|
14
|
+
|
15
|
+
* Fixed integration error in rescheduling tasks
|
16
|
+
|
17
|
+
## 1.0.0 (2022-09-18)
|
4
18
|
|
5
19
|
### Major Changes
|
6
20
|
|
@@ -21,16 +35,19 @@
|
|
21
35
|
* `Procrastinator::Config#run_process_block`
|
22
36
|
* Removed use of envvar `PROCRASTINATOR_STOP`
|
23
37
|
* `Procrastinator::QueueManager` is merged into `Procrastinator::Scheduler`
|
24
|
-
* Removed rake task to halt queue processes
|
38
|
+
* Removed rake task to halt individual queue processes
|
25
39
|
* Renamed `Procrastinator::Config#provide_context` to `provide_container`
|
26
40
|
* You must now call `Scheduler#work` on the result of `Procrastinator.config`
|
27
|
-
* Use a dedicated process monitor (like `monit`) instead in production environments
|
28
|
-
|
29
|
-
|
41
|
+
* Use a dedicated process monitor (like `monit`) instead in production environments to maintain uptime
|
42
|
+
* `max_tasks` is removed as it only added concurrency complexity. Each queue worker only selects one task from only its
|
43
|
+
queue.
|
30
44
|
* Data is now stored as JSON instead of YAML
|
31
45
|
* Added with_store that applies its settings to its block
|
32
46
|
* `load_with` has been removed
|
33
47
|
* Removed `task_attr` and `Procrastinator::Task` module. Tasks is now duck-type checked for accessors instead.
|
48
|
+
* Added Rake tasks to manage process daemon
|
49
|
+
* Times are passed to Task Store as a Ruby Time object instead of an epoch time integer
|
50
|
+
* `#delay` is now `#defer`
|
34
51
|
|
35
52
|
### Minor Changes
|
36
53
|
|
@@ -38,6 +55,8 @@
|
|
38
55
|
* Updated development gems
|
39
56
|
* Logs now include the queue name in log lines
|
40
57
|
* Logs can now set the shift size or age (like Ruby's Logger)
|
58
|
+
* Log format is now tab-separated to align better and work with POSIX cut
|
59
|
+
* General renaming of terms
|
41
60
|
|
42
61
|
### Bugfixes
|
43
62
|
|
data/Rakefile
CHANGED
@@ -2,7 +2,14 @@
|
|
2
2
|
|
3
3
|
require 'bundler/gem_tasks'
|
4
4
|
require 'rspec/core/rake_task'
|
5
|
+
require 'yard'
|
5
6
|
|
6
7
|
RSpec::Core::RakeTask.new(:spec)
|
7
8
|
|
8
9
|
task default: :spec
|
10
|
+
|
11
|
+
YARD::Rake::YardocTask.new do |t|
|
12
|
+
t.files = %w[lib/**/*.rb]
|
13
|
+
# t.options = %w[--some-option]
|
14
|
+
t.stats_options = ['--list-undoc']
|
15
|
+
end
|
@@ -25,10 +25,20 @@ module Procrastinator
|
|
25
25
|
class Config
|
26
26
|
attr_reader :queues, :log_dir, :log_level, :log_shift_age, :log_shift_size, :container
|
27
27
|
|
28
|
-
|
29
|
-
|
28
|
+
# Default directory to keep logs in.
|
29
|
+
DEFAULT_LOG_DIRECTORY = Pathname.new('log').freeze
|
30
|
+
|
31
|
+
# Default age to keep log files for.
|
32
|
+
# @see Logger
|
33
|
+
DEFAULT_LOG_SHIFT_AGE = 0
|
34
|
+
|
35
|
+
# Default max size to keep log files under.
|
36
|
+
# @see Logger
|
30
37
|
DEFAULT_LOG_SHIFT_SIZE = 2 ** 20 # 1 MB
|
31
|
-
|
38
|
+
|
39
|
+
# Default log formatter
|
40
|
+
# @see Logger
|
41
|
+
DEFAULT_LOG_FORMATTER = proc do |severity, datetime, progname, msg|
|
32
42
|
[datetime.iso8601(8),
|
33
43
|
severity,
|
34
44
|
"#{ progname } (#{ Process.pid }):",
|
@@ -70,10 +80,29 @@ module Procrastinator
|
|
70
80
|
@default_store = old_store
|
71
81
|
end
|
72
82
|
|
83
|
+
# Defines the container to assign to each Task Handler's :container attribute.
|
84
|
+
#
|
85
|
+
# @param container [Object] the container
|
73
86
|
def provide_container(container)
|
74
87
|
@container = container
|
75
88
|
end
|
76
89
|
|
90
|
+
# Defines a queue in the Procrastinator Scheduler.
|
91
|
+
#
|
92
|
+
# The Task Handler will be initialized for each task and assigned each of these attributes:
|
93
|
+
# :container, :logger, :scheduler
|
94
|
+
#
|
95
|
+
# @param name [Symbol, String] the identifier to label the queue. Referenced in #defer calls
|
96
|
+
# @param task_class [Class] the Task Handler class.
|
97
|
+
# @param properties [Hash] Settings options object for this queue
|
98
|
+
# @option properties [Object] :store (Procrastinator::TaskStore::SimpleCommaStore)
|
99
|
+
# Storage strategy for tasks in the queue.
|
100
|
+
# @option properties [Integer] :max_attempts (Procrastinator::Queue::DEFAULT_MAX_ATTEMPTS)
|
101
|
+
# Maximum number of times a task may be attempted before giving up.
|
102
|
+
# @option properties [Integer] :timeout (Procrastinator::Queue::DEFAULT_TIMEOUT)
|
103
|
+
# Maximum number of seconds to wait for a single task to complete.
|
104
|
+
# @option properties [Integer] :update_period (Procrastinator::Queue::DEFAULT_UPDATE_PERIOD)
|
105
|
+
# Time to wait before checking for new tasks.
|
77
106
|
def define_queue(name, task_class, properties = {})
|
78
107
|
raise ArgumentError, 'queue name cannot be nil' if name.nil?
|
79
108
|
raise ArgumentError, 'queue task class cannot be nil' if task_class.nil?
|
@@ -142,7 +171,9 @@ module Procrastinator
|
|
142
171
|
end
|
143
172
|
end
|
144
173
|
|
174
|
+
# Raised when there is an error in the setup configuration.
|
145
175
|
class SetupError < RuntimeError
|
176
|
+
# Standard error message for when there is no queue defined
|
146
177
|
ERR_NO_QUEUE = 'setup block must call #define_queue on the environment'
|
147
178
|
end
|
148
179
|
end
|
@@ -14,10 +14,6 @@ module Procrastinator
|
|
14
14
|
#
|
15
15
|
# @see Task
|
16
16
|
class LoggedTask < DelegateClass(Task)
|
17
|
-
# extend Forwardable
|
18
|
-
#
|
19
|
-
# def_delegators :@task, :id, :to_h
|
20
|
-
|
21
17
|
attr_reader :logger
|
22
18
|
|
23
19
|
alias task __getobj__
|
@@ -27,6 +23,7 @@ module Procrastinator
|
|
27
23
|
@logger = logger || raise(ArgumentError, 'Logger cannot be nil')
|
28
24
|
end
|
29
25
|
|
26
|
+
# (see Task#run)
|
30
27
|
def run
|
31
28
|
task.run
|
32
29
|
|
@@ -37,6 +34,7 @@ module Procrastinator
|
|
37
34
|
end
|
38
35
|
end
|
39
36
|
|
37
|
+
# @param (see Task#fail)
|
40
38
|
def fail(error)
|
41
39
|
hook = task.fail(error)
|
42
40
|
begin
|
data/lib/procrastinator/queue.rb
CHANGED
@@ -10,16 +10,21 @@ module Procrastinator
|
|
10
10
|
# @!attribute [r] :task_class
|
11
11
|
# @return [Class] Class that defines the work to be done for jobs in this queue.
|
12
12
|
# @!attribute [r] :timeout
|
13
|
-
# @return [
|
13
|
+
# @return [Numeric] Duration (seconds) after which tasks in this queue should fail for taking too long.
|
14
14
|
# @!attribute [r] :max_attempts
|
15
|
-
# @return [
|
15
|
+
# @return [Integer] Maximum number of attempts for tasks in this queue.
|
16
16
|
# @!attribute [r] :update_period
|
17
|
-
# @return [
|
17
|
+
# @return [Numeric] Delay (seconds) between reloads of tasks from the task store.
|
18
18
|
class Queue
|
19
19
|
extend Forwardable
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
# Default number of seconds to wait for a task to complete
|
22
|
+
DEFAULT_TIMEOUT = 3600 # in seconds; one hour total
|
23
|
+
|
24
|
+
# Default number of times to retry a task
|
25
|
+
DEFAULT_MAX_ATTEMPTS = 20
|
26
|
+
|
27
|
+
# Default amount of time between checks for new Tasks
|
23
28
|
DEFAULT_UPDATE_PERIOD = 10 # seconds
|
24
29
|
|
25
30
|
attr_reader :name, :max_attempts, :timeout, :update_period, :task_store, :task_class
|
@@ -54,6 +59,12 @@ module Procrastinator
|
|
54
59
|
freeze
|
55
60
|
end
|
56
61
|
|
62
|
+
# Constructs the next available task on the queue.
|
63
|
+
#
|
64
|
+
# @param logger [Logger] logger to provide to the constructed task handler
|
65
|
+
# @param container [Object, nil] container to provide to the constructed task handler
|
66
|
+
# @param scheduler [Procrastinator::Scheduler, nil] the scheduler to provide to the constructed task handler
|
67
|
+
# @return [LoggedTask, nil] A Task or nil if no task is found
|
57
68
|
def next_task(logger: Logger.new(StringIO.new), container: nil, scheduler: nil)
|
58
69
|
metadata = next_metas.find(&:runnable?)
|
59
70
|
|
@@ -67,6 +78,9 @@ module Procrastinator
|
|
67
78
|
LoggedTask.new(task, logger: logger)
|
68
79
|
end
|
69
80
|
|
81
|
+
# Fetch a task matching the given identifier
|
82
|
+
#
|
83
|
+
# @param identifier [Hash] attributes to match
|
70
84
|
def fetch_task(identifier)
|
71
85
|
identifier[:data] = JSON.dump(identifier[:data]) if identifier[:data]
|
72
86
|
|
@@ -78,6 +92,11 @@ module Procrastinator
|
|
78
92
|
TaskMetaData.new(tasks.first.merge(queue: self))
|
79
93
|
end
|
80
94
|
|
95
|
+
# Creates a task on the queue, saved using the Task Store strategy.
|
96
|
+
#
|
97
|
+
# @param run_at [Time] Earliest time to attempt running the task
|
98
|
+
# @param expire_at [Time, nil] Time after which the task will not be attempted
|
99
|
+
# @param data [Hash, String, Numeric, nil] The data to save
|
81
100
|
def create(run_at:, expire_at:, data:)
|
82
101
|
if data.nil? && expects_data?
|
83
102
|
raise ArgumentError, "task #{ @task_class } expects to receive :data. Provide :data to #delay."
|
@@ -102,6 +121,7 @@ module Procrastinator
|
|
102
121
|
@task_store.create(**create_data)
|
103
122
|
end
|
104
123
|
|
124
|
+
# @return [Boolean] whether the task handler will accept data to be assigned via its :data attribute
|
105
125
|
def expects_data?
|
106
126
|
@task_class.method_defined?(:data=)
|
107
127
|
end
|
@@ -136,6 +156,8 @@ module Procrastinator
|
|
136
156
|
|
137
157
|
# Internal queue validator
|
138
158
|
module QueueValidation
|
159
|
+
private
|
160
|
+
|
139
161
|
def validate!
|
140
162
|
verify_task_class!
|
141
163
|
verify_task_store!
|
@@ -198,9 +220,11 @@ module Procrastinator
|
|
198
220
|
include QueueValidation
|
199
221
|
end
|
200
222
|
|
223
|
+
# Raised when a Task Handler does not conform to the expected API
|
201
224
|
class MalformedTaskError < StandardError
|
202
225
|
end
|
203
226
|
|
227
|
+
# Raised when a Task Store strategy does not conform to the expected API
|
204
228
|
class MalformedTaskStoreError < RuntimeError
|
205
229
|
end
|
206
230
|
end
|
@@ -64,6 +64,7 @@ module Procrastinator
|
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
+
# Logs halting the queue
|
67
68
|
def halt
|
68
69
|
@logger&.info("Halted worker on queue: #{ name }")
|
69
70
|
@logger&.close
|
@@ -86,6 +87,9 @@ module Procrastinator
|
|
86
87
|
end
|
87
88
|
end
|
88
89
|
|
90
|
+
# Raised when a Task Storage strategy is missing a required part of the API.
|
91
|
+
#
|
92
|
+
# @see TaskStore
|
89
93
|
class MalformedTaskPersisterError < StandardError
|
90
94
|
end
|
91
95
|
end
|
@@ -3,14 +3,20 @@
|
|
3
3
|
require 'rake'
|
4
4
|
|
5
5
|
module Procrastinator
|
6
|
+
# Rake tasks specific to Procrastinator.
|
7
|
+
#
|
8
|
+
# Provide this in your Rakefile:
|
9
|
+
#
|
10
|
+
# require 'procrastinator/rake/task'
|
11
|
+
# Procrastinator::RakeTask.new do
|
12
|
+
# # return your Procrastinator::Scheduler here or construct it using Procrastinator.config
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# And then you will be able to run rake tasks like:
|
16
|
+
#
|
17
|
+
# bundle exec rake procrastinator:start
|
6
18
|
module Rake
|
7
|
-
# RakeTask builder.
|
8
|
-
#
|
9
|
-
# require 'procrastinator/rake/task'
|
10
|
-
# Procrastinator::RakeTask.new('/var/run') do
|
11
|
-
# # return your Procrastinator::Scheduler here or construct it using Procrastinator.config
|
12
|
-
# end
|
13
|
-
#
|
19
|
+
# RakeTask builder class. Use DaemonTasks.define to generate the needed tasks.
|
14
20
|
class DaemonTasks
|
15
21
|
include ::Rake::Cloneable
|
16
22
|
include ::Rake::DSL
|
@@ -26,7 +32,7 @@ module Procrastinator
|
|
26
32
|
# Defines procrastinator:start and procrastinator:stop Rake tasks that operate on the given scheduler.
|
27
33
|
#
|
28
34
|
# @param pid_path [Pathname, File, String, nil] The pid file path
|
29
|
-
# @yieldreturn
|
35
|
+
# @yieldreturn [Procrastinator::Scheduler] Constructed Scheduler to use as basis for starting tasks
|
30
36
|
#
|
31
37
|
# @see Scheduler::DaemonWorking#daemonized!
|
32
38
|
def define(pid_path: nil)
|
@@ -78,8 +84,9 @@ module Procrastinator
|
|
78
84
|
def stop
|
79
85
|
return unless Scheduler::DaemonWorking.running?(@pid_path)
|
80
86
|
|
87
|
+
pid = File.read(@pid_path)
|
81
88
|
Scheduler::DaemonWorking.halt!(@pid_path)
|
82
|
-
warn "Procrastinator pid #{
|
89
|
+
warn "Procrastinator pid #{ pid } halted."
|
83
90
|
end
|
84
91
|
end
|
85
92
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/expectations'
|
4
|
+
|
5
|
+
# Determines if the given task store has a task that matches the expectation hash
|
6
|
+
RSpec::Matchers.define :have_task do |expected_task|
|
7
|
+
match do |task_store|
|
8
|
+
task_store.read.any? do |task|
|
9
|
+
task_hash = task.to_h
|
10
|
+
task_hash[:data] = JSON.parse(task_hash[:data], symbolize_names: true) unless task_hash[:data].empty?
|
11
|
+
|
12
|
+
expected_task.all? do |field, expected_value|
|
13
|
+
expected_value = case field
|
14
|
+
when :queue
|
15
|
+
expected_value.to_sym
|
16
|
+
when :run_at, :initial_run_at, :expire_at, :last_fail_at
|
17
|
+
Time.at(expected_value.to_i)
|
18
|
+
else
|
19
|
+
expected_value
|
20
|
+
end
|
21
|
+
|
22
|
+
values_match? expected_value, task_hash[field]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
description do
|
28
|
+
"have a task with properties #{ expected_task.collect { |k, v| "#{ k }=#{ v }" }.join(', ') }"
|
29
|
+
end
|
30
|
+
end
|
@@ -3,9 +3,10 @@
|
|
3
3
|
require 'stringio'
|
4
4
|
|
5
5
|
module Procrastinator
|
6
|
-
# A Scheduler object provides the API for client applications to manage
|
6
|
+
# A Scheduler object provides the API for client applications to manage scheduled tasks.
|
7
7
|
#
|
8
|
-
# Use #
|
8
|
+
# Use Scheduler#defer to schedule new tasks, Scheduler#reschedule to alter existing tasks, and Scheduler#cancel to
|
9
|
+
# remove unwanted tasks.
|
9
10
|
#
|
10
11
|
# @author Robin Miller
|
11
12
|
class Scheduler
|
@@ -19,7 +20,7 @@ module Procrastinator
|
|
19
20
|
# @param run_at [Time, Integer] Optional time when this task should be executed. Defaults to the current time.
|
20
21
|
# @param data [Hash, Array, String, Integer] Optional simple data object to be provided to the task on execution.
|
21
22
|
# @param expire_at [Time, Integer] Optional time when the task should be abandoned
|
22
|
-
def
|
23
|
+
def defer(queue_name = nil, data: nil, run_at: Time.now, expire_at: nil)
|
23
24
|
raise ArgumentError, <<~ERR unless queue_name.nil? || queue_name.is_a?(Symbol)
|
24
25
|
must provide a queue name as the first argument. Received: #{ queue_name }
|
25
26
|
ERR
|
@@ -29,13 +30,15 @@ module Procrastinator
|
|
29
30
|
queue.create(run_at: run_at, expire_at: expire_at, data: data)
|
30
31
|
end
|
31
32
|
|
33
|
+
alias delay defer
|
34
|
+
|
32
35
|
# Alters an existing task to run at a new time, expire at a new time, or both.
|
33
36
|
#
|
34
37
|
# Call #to on the result and pass in the new :run_at and/or :expire_at.
|
35
38
|
#
|
36
39
|
# Example:
|
37
40
|
#
|
38
|
-
#
|
41
|
+
# scheduler.reschedule(:alerts, data: {user_id: 5}).to(run_at: Time.now, expire_at: Time.now + 10)
|
39
42
|
#
|
40
43
|
# The identifier can include any data field stored in the task loader. Often this is the information in :data.
|
41
44
|
#
|
@@ -44,7 +47,7 @@ module Procrastinator
|
|
44
47
|
#
|
45
48
|
# @see TaskMetaData
|
46
49
|
def reschedule(queue, identifier)
|
47
|
-
UpdateProxy.new(@config, identifier: identifier
|
50
|
+
UpdateProxy.new(@config.queue(name: queue), identifier: identifier)
|
48
51
|
end
|
49
52
|
|
50
53
|
# Removes an existing task, as located by the given identifying information.
|
@@ -86,7 +89,7 @@ module Procrastinator
|
|
86
89
|
class UpdateProxy
|
87
90
|
def initialize(queue, identifier:)
|
88
91
|
@queue = queue
|
89
|
-
@identifier = identifier
|
92
|
+
@identifier = identifier.merge(queue: queue.name.to_sym)
|
90
93
|
end
|
91
94
|
|
92
95
|
def to(run_at: nil, expire_at: nil)
|
@@ -123,6 +126,7 @@ module Procrastinator
|
|
123
126
|
#
|
124
127
|
# @see WorkProxy
|
125
128
|
module ThreadedWorking
|
129
|
+
# Program name. Used as default for pid file names and in logging.
|
126
130
|
PROG_NAME = 'Procrastinator'
|
127
131
|
|
128
132
|
# Work off jobs per queue, each in its own thread.
|
@@ -135,7 +139,7 @@ module Procrastinator
|
|
135
139
|
begin
|
136
140
|
@threads = spawn_threads
|
137
141
|
|
138
|
-
@logger.info "
|
142
|
+
@logger.info "#{ PROG_NAME } running. Process ID: #{ Process.pid }"
|
139
143
|
@threads.each do |thread|
|
140
144
|
thread.join(timeout)
|
141
145
|
end
|
@@ -243,10 +247,13 @@ module Procrastinator
|
|
243
247
|
#
|
244
248
|
# @see WorkProxy
|
245
249
|
module DaemonWorking
|
246
|
-
|
247
|
-
|
250
|
+
# File extension for process ID files
|
251
|
+
PID_EXT = '.pid'
|
252
|
+
|
253
|
+
# Default directory to store PID files in.
|
254
|
+
DEFAULT_PID_DIR = Pathname.new('/tmp').freeze
|
248
255
|
|
249
|
-
# 15 chars is linux limit
|
256
|
+
# Maximum process name size. 15 chars is linux limit
|
250
257
|
MAX_PROC_LEN = 15
|
251
258
|
|
252
259
|
# Consumes the current process and turns it into a background daemon and proceed as #threaded.
|
@@ -290,6 +297,10 @@ module Procrastinator
|
|
290
297
|
false
|
291
298
|
end
|
292
299
|
|
300
|
+
# Raised when a process is already found to exist using the same pid_path
|
301
|
+
class ProcessExistsError < RuntimeError
|
302
|
+
end
|
303
|
+
|
293
304
|
private
|
294
305
|
|
295
306
|
def spawn_daemon(pid_path)
|
@@ -384,7 +395,4 @@ module Procrastinator
|
|
384
395
|
end
|
385
396
|
end
|
386
397
|
end
|
387
|
-
|
388
|
-
class ProcessExistsError < RuntimeError
|
389
|
-
end
|
390
398
|
end
|
data/lib/procrastinator/task.rb
CHANGED
@@ -21,6 +21,12 @@ module Procrastinator
|
|
21
21
|
@handler = handler
|
22
22
|
end
|
23
23
|
|
24
|
+
# Executes the Task Handler's #run hook and records the attempt.
|
25
|
+
#
|
26
|
+
# If the #run hook completes successfully, the #success hook will also be executed, if defined.
|
27
|
+
#
|
28
|
+
# @raise [ExpiredError] when the task run_at is after the expired_at.
|
29
|
+
# @raise [AttemptsExhaustedError] when the task has been attempted more times than allowed by the queue settings.
|
24
30
|
def run
|
25
31
|
raise ExpiredError, "task is over its expiry time of #{ @metadata.expire_at.iso8601 }" if @metadata.expired?
|
26
32
|
|
@@ -45,19 +51,25 @@ module Procrastinator
|
|
45
51
|
hook
|
46
52
|
end
|
47
53
|
|
54
|
+
# Attempts to run the given optional event hook on the handler, catching any resultant errors to prevent the whole
|
55
|
+
# task from failing despite the actual work in #run completing.
|
48
56
|
def try_hook(method, *params)
|
49
57
|
@handler.send(method, *params) if @handler.respond_to? method
|
50
58
|
rescue StandardError => e
|
51
59
|
warn "#{ method.to_s.capitalize } hook error: #{ e.message }"
|
52
60
|
end
|
53
61
|
|
62
|
+
# Convert the task into a human-legible string.
|
63
|
+
# @return [String] Including the queue name, id, and serialized data.
|
54
64
|
def to_s
|
55
65
|
"#{ @metadata.queue.name }##{ id } [#{ serialized_data }]"
|
56
66
|
end
|
57
67
|
|
68
|
+
# Raised when a Task's run_at is beyond its expire_at
|
58
69
|
class ExpiredError < RuntimeError
|
59
70
|
end
|
60
71
|
|
72
|
+
# Raised when a Task's attempts has exceeded the max_attempts defined for its queue (if any).
|
61
73
|
class AttemptsExhaustedError < RuntimeError
|
62
74
|
end
|
63
75
|
end
|
@@ -50,6 +50,9 @@ module Procrastinator
|
|
50
50
|
@data = data ? JSON.parse(data, symbolize_names: true) : nil
|
51
51
|
end
|
52
52
|
|
53
|
+
# Increases the number of attempts on this task by one, unless the limit has been reached.
|
54
|
+
#
|
55
|
+
# @raise [Task::AttemptsExhaustedError] when the number of attempts has exceeded the Queue's defined maximum.
|
53
56
|
def add_attempt
|
54
57
|
raise Task::AttemptsExhaustedError unless attempts_left?
|
55
58
|
|
@@ -72,22 +75,28 @@ module Procrastinator
|
|
72
75
|
end
|
73
76
|
end
|
74
77
|
|
78
|
+
# @return [Boolean] whether the task has attempts left and is not expired
|
75
79
|
def retryable?
|
76
80
|
attempts_left? && !expired?
|
77
81
|
end
|
78
82
|
|
83
|
+
# @return [Boolean] whether the task is expired
|
79
84
|
def expired?
|
80
85
|
!@expire_at.nil? && @expire_at < Time.now
|
81
86
|
end
|
82
87
|
|
88
|
+
# @return [Boolean] whether there are attempts left until the Queue's defined maximum is reached (if any)
|
83
89
|
def attempts_left?
|
84
90
|
@queue.max_attempts.nil? || @attempts < @queue.max_attempts
|
85
91
|
end
|
86
92
|
|
93
|
+
# @return [Boolean] whether the task's run_at is exceeded
|
87
94
|
def runnable?
|
88
95
|
!@run_at.nil? && @run_at <= Time.now
|
89
96
|
end
|
90
97
|
|
98
|
+
# @return [Boolean] whether the task's last execution completed successfully.
|
99
|
+
# @raise [RuntimeError] when the task has not been attempted yet or when it is expired
|
91
100
|
def successful?
|
92
101
|
raise 'you cannot check for success before running #work' if !expired? && @attempts <= 0
|
93
102
|
|
@@ -117,6 +126,7 @@ module Procrastinator
|
|
117
126
|
@run_at += 30 + (@attempts ** 4) unless @run_at.nil?
|
118
127
|
end
|
119
128
|
|
129
|
+
# @return [Hash] representation of the task metadata as a hash
|
120
130
|
def to_h
|
121
131
|
{id: @id,
|
122
132
|
queue: @queue.name.to_s,
|
@@ -129,10 +139,12 @@ module Procrastinator
|
|
129
139
|
data: serialized_data}
|
130
140
|
end
|
131
141
|
|
142
|
+
# @return [String] :data serialized as a JSON string
|
132
143
|
def serialized_data
|
133
144
|
JSON.dump(@data)
|
134
145
|
end
|
135
146
|
|
147
|
+
# Resets the last failure time and error.
|
136
148
|
def clear_fails
|
137
149
|
@last_error = nil
|
138
150
|
@last_fail_at = nil
|