cloudtasker 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.rubocop.yml +5 -0
  4. data/.travis.yml +10 -1
  5. data/Appraisals +25 -0
  6. data/Gemfile.lock +10 -4
  7. data/README.md +550 -4
  8. data/app/controllers/cloudtasker/application_controller.rb +2 -0
  9. data/app/controllers/cloudtasker/worker_controller.rb +22 -2
  10. data/cloudtasker.gemspec +4 -3
  11. data/docs/BATCH_JOBS.md +66 -0
  12. data/docs/CRON_JOBS.md +63 -0
  13. data/docs/UNIQUE_JOBS.md +127 -0
  14. data/exe/cloudtasker +15 -0
  15. data/gemfiles/.bundle/config +2 -0
  16. data/gemfiles/google_cloud_tasks_1.0.gemfile +9 -0
  17. data/gemfiles/google_cloud_tasks_1.0.gemfile.lock +263 -0
  18. data/gemfiles/google_cloud_tasks_1.1.gemfile +9 -0
  19. data/gemfiles/google_cloud_tasks_1.1.gemfile.lock +263 -0
  20. data/gemfiles/google_cloud_tasks_1.2.gemfile +9 -0
  21. data/gemfiles/google_cloud_tasks_1.2.gemfile.lock +263 -0
  22. data/gemfiles/google_cloud_tasks_1.3.gemfile +9 -0
  23. data/gemfiles/google_cloud_tasks_1.3.gemfile.lock +264 -0
  24. data/gemfiles/rails_4.0.gemfile +10 -0
  25. data/gemfiles/rails_4.1.gemfile +9 -0
  26. data/gemfiles/rails_4.2.gemfile +9 -0
  27. data/gemfiles/rails_5.0.gemfile +9 -0
  28. data/gemfiles/rails_5.1.gemfile +9 -0
  29. data/gemfiles/rails_5.2.gemfile +9 -0
  30. data/gemfiles/rails_5.2.gemfile.lock +247 -0
  31. data/gemfiles/rails_6.0.gemfile +9 -0
  32. data/gemfiles/rails_6.0.gemfile.lock +263 -0
  33. data/lib/cloudtasker.rb +19 -1
  34. data/lib/cloudtasker/backend/google_cloud_task.rb +139 -0
  35. data/lib/cloudtasker/backend/memory_task.rb +190 -0
  36. data/lib/cloudtasker/backend/redis_task.rb +248 -0
  37. data/lib/cloudtasker/batch/batch_progress.rb +19 -1
  38. data/lib/cloudtasker/batch/job.rb +81 -20
  39. data/lib/cloudtasker/cli.rb +194 -0
  40. data/lib/cloudtasker/cloud_task.rb +91 -0
  41. data/lib/cloudtasker/config.rb +64 -2
  42. data/lib/cloudtasker/cron/job.rb +2 -2
  43. data/lib/cloudtasker/cron/schedule.rb +15 -5
  44. data/lib/cloudtasker/dead_worker_error.rb +6 -0
  45. data/lib/cloudtasker/local_server.rb +74 -0
  46. data/lib/cloudtasker/railtie.rb +10 -0
  47. data/lib/cloudtasker/testing.rb +133 -0
  48. data/lib/cloudtasker/unique_job/job.rb +1 -1
  49. data/lib/cloudtasker/unique_job/lock/base_lock.rb +1 -1
  50. data/lib/cloudtasker/unique_job/lock/until_executed.rb +3 -1
  51. data/lib/cloudtasker/unique_job/lock/while_executing.rb +3 -1
  52. data/lib/cloudtasker/version.rb +1 -1
  53. data/lib/cloudtasker/worker.rb +59 -16
  54. data/lib/cloudtasker/{task.rb → worker_handler.rb} +10 -77
  55. data/lib/cloudtasker/worker_logger.rb +155 -0
  56. data/lib/tasks/setup_queue.rake +10 -0
  57. metadata +55 -6
@@ -1,15 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
4
+
3
5
  module Cloudtasker
4
6
  # Holds cloudtasker configuration. See Cloudtasker#configure
5
7
  class Config
6
8
  attr_accessor :redis
7
9
  attr_writer :secret, :gcp_location_id, :gcp_project_id,
8
- :gcp_queue_id, :processor_host, :processor_path
10
+ :gcp_queue_id, :processor_path, :logger, :mode, :max_retries
11
+
12
+ # Retry header in Cloud Task responses
13
+ RETRY_HEADER = 'X-CloudTasks-TaskExecutionCount'
9
14
 
15
+ # Default values
10
16
  DEFAULT_LOCATION_ID = 'us-east1'
11
17
  DEFAULT_PROCESSOR_PATH = '/cloudtasker/run'
12
18
 
19
+ # The number of times jobs will be attempted before declaring them dead
20
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
21
+
13
22
  PROCESSOR_HOST_MISSING = <<~DOC
14
23
  Missing host for processing.
15
24
  Please specify a processor hostname in form of `https://some-public-dns.example.com`'
@@ -27,6 +36,46 @@ module Cloudtasker
27
36
  Please specify a secret in the cloudtasker initializer or add Rails secret_key_base in your credentials
28
37
  DOC
29
38
 
39
+ #
40
+ # The number of times jobs will be retried. This number of
41
+ # retries does not include failures due to the application being unreachable.
42
+ #
43
+ #
44
+ # @return [Integer] The number of retries
45
+ #
46
+ def max_retries
47
+ @max_retries ||= DEFAULT_MAX_RETRY_ATTEMPTS
48
+ end
49
+
50
+ #
51
+ # The operating mode.
52
+ # - :production => process tasks via GCP Cloud Task.
53
+ # - :development => process tasks locally via Redis.
54
+ #
55
+ # @return [<Type>] <description>
56
+ #
57
+ def mode
58
+ @mode ||= environment == 'development' ? :development : :production
59
+ end
60
+
61
+ #
62
+ # Return the current environment.
63
+ #
64
+ # @return [String] The environment name.
65
+ #
66
+ def environment
67
+ ENV['CLOUDTASKER_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
68
+ end
69
+
70
+ #
71
+ # Return the Cloudtasker logger.
72
+ #
73
+ # @return [Logger, any] The cloudtasker logger.
74
+ #
75
+ def logger
76
+ @logger ||= defined?(Rails) ? Rails.logger : ::Logger.new(STDOUT)
77
+ end
78
+
30
79
  #
31
80
  # Return the full URL of the processor. Worker payloads will be sent
32
81
  # to this URL.
@@ -37,6 +86,19 @@ module Cloudtasker
37
86
  File.join(processor_host, processor_path)
38
87
  end
39
88
 
89
+ #
90
+ # Set the processor host. In the context of Rails the host will
91
+ # also be added to the list of authorized Rails hosts.
92
+ #
93
+ # @param [String] val The processor host to set.
94
+ #
95
+ def processor_host=(val)
96
+ @processor_host = val
97
+
98
+ # Add processor host to the list of authorized hosts
99
+ Rails.application.config.hosts << val.gsub(%r{https?://}, '') if val && defined?(Rails)
100
+ end
101
+
40
102
  #
41
103
  # The hostname of the application processing the workers. The hostname must
42
104
  # be reachable from Cloud Task.
@@ -93,7 +155,7 @@ module Cloudtasker
93
155
  #
94
156
  def secret
95
157
  @secret || (
96
- defined?(Rails) && Rails.application.credentials&.secret_key_base
158
+ defined?(Rails) && Rails.application.credentials&.dig(:secret_key_base)
97
159
  ) || raise(StandardError, SECRET_MISSING_ERROR)
98
160
  end
99
161
 
@@ -179,8 +179,8 @@ module Cloudtasker
179
179
  next_worker = worker.new_instance.tap { |e| e.job_meta.set(key(:time_at), next_time.iso8601) }
180
180
 
181
181
  # Schedule next worker
182
- resp = next_worker.schedule(time_at: next_time)
183
- cron_schedule.update(task_id: resp.name, job_id: next_worker.job_id)
182
+ task = next_worker.schedule(time_at: next_time)
183
+ cron_schedule.update(task_id: task.id, job_id: next_worker.job_id)
184
184
  end
185
185
 
186
186
  #
@@ -56,7 +56,7 @@ module Cloudtasker
56
56
  #
57
57
  def self.load_from_hash!(hash)
58
58
  schedules = hash.map do |id, config|
59
- schedule_config = JSON.parse(config.to_json, symbolize_names: true).merge(id: id)
59
+ schedule_config = JSON.parse(config.to_json, symbolize_names: true).merge(id: id.to_s)
60
60
  create(schedule_config)
61
61
  end
62
62
 
@@ -99,7 +99,7 @@ module Cloudtasker
99
99
  return false unless schedule
100
100
 
101
101
  # Delete task and stored schedule
102
- Task.delete(schedule.task_id) if schedule.task_id
102
+ CloudTask.delete(schedule.task_id) if schedule.task_id
103
103
  redis.del(schedule.gid)
104
104
  end
105
105
 
@@ -252,17 +252,27 @@ module Cloudtasker
252
252
  # then any existing cloud task is removed and a task is recreated.
253
253
  #
254
254
  def save(update_task: true)
255
- return false unless valid? && changed?
255
+ return false unless valid?
256
256
 
257
257
  # Save schedule
258
258
  config_was_changed = config_changed?
259
259
  redis.write(gid, to_h)
260
260
 
261
261
  # Stop there if backend does not need update
262
- return true unless update_task && config_was_changed
262
+ return true unless update_task && (config_was_changed || !task_id || !CloudTask.find(task_id))
263
263
 
264
+ # Update backend
265
+ persist_cloud_task
266
+ end
267
+
268
+ private
269
+
270
+ #
271
+ # Update the task in backend.
272
+ #
273
+ def persist_cloud_task
264
274
  # Delete previous instance
265
- Task.delete(task_id) if task_id
275
+ CloudTask.delete(task_id) if task_id
266
276
 
267
277
  # Schedule worker
268
278
  worker_instance = Object.const_get(worker).new
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ class DeadWorkerError < StandardError
5
+ end
6
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/backend/redis_task'
4
+
5
+ module Cloudtasker
6
+ # Process jobs stored in Redis.
7
+ # Only to be used in development.
8
+ class LocalServer
9
+ # Max number of task requests sent to the processing server
10
+ CONCURRENCY = (ENV['CLOUDTASKER_CONCURRENCY'] || 5).to_i
11
+
12
+ #
13
+ # Stop the local server.
14
+ #
15
+ def stop
16
+ @done = true
17
+
18
+ # Terminate threads and repush tasks
19
+ @threads&.each do |t|
20
+ t.terminate
21
+ t['task']&.retry_later(0, is_error: false)
22
+ end
23
+
24
+ # Wait for main server to be done
25
+ sleep 1 while @start&.alive?
26
+ end
27
+
28
+ #
29
+ # Start the local server
30
+ #
31
+ #
32
+ def start
33
+ @start ||= Thread.new do
34
+ until @done
35
+ process_jobs
36
+ sleep 1
37
+ end
38
+ Cloudtasker.logger.info('[Cloudtasker/Server] Local server exiting...')
39
+ end
40
+ end
41
+
42
+ #
43
+ # Process enqueued workers.
44
+ #
45
+ #
46
+ def process_jobs
47
+ @threads ||= []
48
+
49
+ # Remove any done thread
50
+ @threads.select!(&:alive?)
51
+
52
+ # Process tasks
53
+ while @threads.count < CONCURRENCY && (task = Cloudtasker::Backend::RedisTask.pop)
54
+ @threads << Thread.new do
55
+ Thread.current['task'] = task
56
+ Thread.current['attempts'] = 0
57
+
58
+ # Deliver task
59
+ begin
60
+ Thread.current['task'].deliver
61
+ rescue Errno::ECONNREFUSED => e
62
+ raise(e) unless Thread.current['attempts'] < 3
63
+
64
+ # Retry on connection error, in case the web server is not
65
+ # started yet.
66
+ Thread.current['attempts'] += 1
67
+ sleep(3)
68
+ retry
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ # Rails extensions
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load 'tasks/setup_queue.rake'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/backend/memory_task'
4
+
5
+ module Cloudtasker
6
+ # Enable/Disable test mode for Cloudtasker
7
+ module Testing
8
+ module_function
9
+
10
+ #
11
+ # Set the test mode, either permanently or
12
+ # temporarily (via block).
13
+ #
14
+ # @param [Symbol] mode The test mode.
15
+ #
16
+ # @return [Symbol] The test mode.
17
+ #
18
+ def switch_test_mode(mode)
19
+ if block_given?
20
+ current_mode = @test_mode
21
+ begin
22
+ @test_mode = mode
23
+ yield
24
+ ensure
25
+ @test_mode = current_mode
26
+ end
27
+ else
28
+ @test_mode = mode
29
+ end
30
+ end
31
+
32
+ #
33
+ # Set cloudtasker to real mode temporarily
34
+ #
35
+ # @param [Proc] &block The block to run in real mode
36
+ #
37
+ def enable!(&block)
38
+ switch_test_mode(:enabled, &block)
39
+ end
40
+
41
+ #
42
+ # Set cloudtasker to fake mode temporarily
43
+ #
44
+ # @param [Proc] &block The block to run in fake mode
45
+ #
46
+ def fake!(&block)
47
+ switch_test_mode(:fake, &block)
48
+ end
49
+
50
+ #
51
+ # Set cloudtasker to inline mode temporarily
52
+ #
53
+ # @param [Proc] &block The block to run in inline mode
54
+ #
55
+ def inline!(&block)
56
+ switch_test_mode(:inline, &block)
57
+ end
58
+
59
+ #
60
+ # Return true if Cloudtasker is enabled.
61
+ #
62
+ def enabled?
63
+ !@test_mode || @test_mode == :enabled
64
+ end
65
+
66
+ #
67
+ # Return true if Cloudtasker is in fake mode.
68
+ #
69
+ # @return [Boolean] True if jobs must be processed through drain calls.
70
+ #
71
+ def fake?
72
+ @test_mode == :fake
73
+ end
74
+
75
+ #
76
+ # Return true if Cloudtasker is in inline mode.
77
+ #
78
+ # @return [Boolean] True if jobs are run inline.
79
+ #
80
+ def inline?
81
+ @test_mode == :inline
82
+ end
83
+
84
+ #
85
+ # Return true if tasks should be managed in memory.
86
+ #
87
+ # @return [Boolean] True if jobs are managed in memory.
88
+ #
89
+ def in_memory?
90
+ !enabled?
91
+ end
92
+ end
93
+
94
+ # Add extra methods for testing purpose
95
+ module Worker
96
+ #
97
+ # Clear all jobs.
98
+ #
99
+ def self.clear_all
100
+ Backend::MemoryTask.clear
101
+ end
102
+
103
+ #
104
+ # Run all the jobs.
105
+ #
106
+ # @return [Array<any>] The return values of the workers perform method.
107
+ #
108
+ def self.drain_all
109
+ Backend::MemoryTask.drain
110
+ end
111
+
112
+ # Module class methods
113
+ module ClassMethods
114
+ #
115
+ # Return all jobs related to this worker class.
116
+ #
117
+ # @return [Array<Cloudtasker::Worker] The list of workers
118
+ #
119
+ def jobs
120
+ Backend::MemoryTask.jobs(to_s)
121
+ end
122
+
123
+ #
124
+ # Run all jobs related to this worker class.
125
+ #
126
+ # @return [Array<any>] The return values of the workers perform method.
127
+ #
128
+ def drain
129
+ Backend::MemoryTask.drain(to_s)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -40,7 +40,7 @@ module Cloudtasker
40
40
  @lock_instance ||=
41
41
  begin
42
42
  # Infer lock class and get instance
43
- lock_name = options[:lock] || options['lock']
43
+ lock_name = options[:lock]
44
44
  lock_klass = Lock.const_get(lock_name.to_s.split('_').collect(&:capitalize).join)
45
45
  lock_klass.new(self)
46
46
  rescue NameError
@@ -43,7 +43,7 @@ module Cloudtasker
43
43
  @conflict_instance ||=
44
44
  begin
45
45
  # Infer lock class and get instance
46
- strategy_name = options[:on_conflict] || options['on_conflict']
46
+ strategy_name = options[:on_conflict]
47
47
  strategy_klass = ConflictStrategy.const_get(strategy_name.to_s.split('_').collect(&:capitalize).join)
48
48
  strategy_klass.new(job)
49
49
  rescue NameError
@@ -24,9 +24,11 @@ module Cloudtasker
24
24
  def execute
25
25
  job.lock!
26
26
  yield
27
- job.unlock!
28
27
  rescue LockError
29
28
  conflict_instance.on_execute { yield }
29
+ ensure
30
+ # Unlock the job on any error to avoid deadlocks.
31
+ job.unlock!
30
32
  end
31
33
  end
32
34
  end
@@ -13,9 +13,11 @@ module Cloudtasker
13
13
  def execute
14
14
  job.lock!
15
15
  yield
16
- job.unlock!
17
16
  rescue LockError
18
17
  conflict_instance.on_execute { yield }
18
+ ensure
19
+ # Unlock the job on any error to avoid deadlocks.
20
+ job.unlock!
19
21
  end
20
22
  end
21
23
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cloudtasker
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -6,7 +6,7 @@ module Cloudtasker
6
6
  # Add class method to including class
7
7
  def self.included(base)
8
8
  base.extend(ClassMethods)
9
- base.attr_accessor :job_args, :job_id, :job_meta, :job_reenqueued
9
+ base.attr_accessor :job_args, :job_id, :job_meta, :job_reenqueued, :job_retries
10
10
  end
11
11
 
12
12
  #
@@ -44,7 +44,7 @@ module Cloudtasker
44
44
  return nil unless worker_klass.include?(self)
45
45
 
46
46
  # Return instantiated worker
47
- worker_klass.new(payload.slice(:job_args, :job_id, :job_meta))
47
+ worker_klass.new(payload.slice(:job_args, :job_id, :job_meta, :job_retries))
48
48
  rescue NameError
49
49
  nil
50
50
  end
@@ -54,12 +54,12 @@ module Cloudtasker
54
54
  #
55
55
  # Set the worker runtime options.
56
56
  #
57
- # @param [Hash] opts The worker options
57
+ # @param [Hash] opts The worker options.
58
58
  #
59
- # @return [<Type>] <description>
59
+ # @return [Hash] The options set.
60
60
  #
61
61
  def cloudtasker_options(opts = {})
62
- opt_list = opts&.map { |k, v| [k.to_s, v] } || [] # stringify
62
+ opt_list = opts&.map { |k, v| [k.to_sym, v] } || [] # symbolize
63
63
  @cloudtasker_options_hash = Hash[opt_list]
64
64
  end
65
65
 
@@ -69,7 +69,7 @@ module Cloudtasker
69
69
  # @return [Hash] The worker runtime options.
70
70
  #
71
71
  def cloudtasker_options_hash
72
- @cloudtasker_options_hash
72
+ @cloudtasker_options_hash || {}
73
73
  end
74
74
 
75
75
  #
@@ -77,7 +77,7 @@ module Cloudtasker
77
77
  #
78
78
  # @param [Array<any>] *args List of worker arguments
79
79
  #
80
- # @return [Google::Cloud::Tasks::V2beta3::Task] The Google Task response
80
+ # @return [Cloudtasker::CloudTask] The Google Task response
81
81
  #
82
82
  def perform_async(*args)
83
83
  perform_in(nil, *args)
@@ -89,7 +89,7 @@ module Cloudtasker
89
89
  # @param [Integer, nil] interval The delay in seconds.
90
90
  # @param [Array<any>] *args List of worker arguments.
91
91
  #
92
- # @return [Google::Cloud::Tasks::V2beta3::Task] The Google Task response
92
+ # @return [Cloudtasker::CloudTask] The Google Task response
93
93
  #
94
94
  def perform_in(interval, *args)
95
95
  new(job_args: args).schedule(interval: interval)
@@ -101,11 +101,20 @@ module Cloudtasker
101
101
  # @param [Time, Integer] time_at The time at which the job should run.
102
102
  # @param [Array<any>] *args List of worker arguments
103
103
  #
104
- # @return [Google::Cloud::Tasks::V2beta3::Task] The Google Task response
104
+ # @return [Cloudtasker::CloudTask] The Google Task response
105
105
  #
106
106
  def perform_at(time_at, *args)
107
107
  new(job_args: args).schedule(time_at: time_at)
108
108
  end
109
+
110
+ #
111
+ # Return the numbeer of times this worker will be retried.
112
+ #
113
+ # @return [Integer] The number of retries.
114
+ #
115
+ def max_retries
116
+ cloudtasker_options_hash[:max_retries] || Cloudtasker.config.max_retries
117
+ end
109
118
  end
110
119
 
111
120
  #
@@ -114,10 +123,20 @@ module Cloudtasker
114
123
  # @param [Array<any>] job_args The list of perform args.
115
124
  # @param [String] job_id A unique ID identifying this job.
116
125
  #
117
- def initialize(job_args: [], job_id: nil, job_meta: {})
126
+ def initialize(job_args: [], job_id: nil, job_meta: {}, job_retries: 0)
118
127
  @job_args = job_args
119
128
  @job_id = job_id || SecureRandom.uuid
120
129
  @job_meta = MetaStore.new(job_meta)
130
+ @job_retries = job_retries || 0
131
+ end
132
+
133
+ #
134
+ # Return the Cloudtasker logger instance.
135
+ #
136
+ # @return [Logger, any] The cloudtasker logger.
137
+ #
138
+ def logger
139
+ @logger ||= WorkerLogger.new(self)
121
140
  end
122
141
 
123
142
  #
@@ -126,9 +145,22 @@ module Cloudtasker
126
145
  # @return [Any] The result of the perform.
127
146
  #
128
147
  def execute
129
- Cloudtasker.config.server_middleware.invoke(self) do
130
- perform(*job_args)
148
+ logger.info('Starting job...')
149
+ resp = Cloudtasker.config.server_middleware.invoke(self) do
150
+ begin
151
+ perform(*job_args)
152
+ rescue StandardError => e
153
+ try(:on_error, e)
154
+ return raise(e) unless job_dead?
155
+
156
+ # Flag job as dead
157
+ logger.info('Job dead')
158
+ try(:on_dead, e)
159
+ raise(DeadWorkerError, e)
160
+ end
131
161
  end
162
+ logger.info('Job done')
163
+ resp
132
164
  end
133
165
 
134
166
  #
@@ -138,11 +170,11 @@ module Cloudtasker
138
170
  #
139
171
  # @param [Time, Integer] interval The time at which the job should run
140
172
  #
141
- # @return [Google::Cloud::Tasks::V2beta3::Task] The Google Task response
173
+ # @return [Cloudtasker::CloudTask] The Google Task response
142
174
  #
143
175
  def schedule(interval: nil, time_at: nil)
144
176
  Cloudtasker.config.client_middleware.invoke(self) do
145
- Task.new(self).schedule(interval: interval, time_at: time_at)
177
+ WorkerHandler.new(self).schedule(interval: interval, time_at: time_at)
146
178
  end
147
179
  end
148
180
 
@@ -155,7 +187,7 @@ module Cloudtasker
155
187
  #
156
188
  # @param [Integer] interval Delay to wait before processing the job again (in seconds).
157
189
  #
158
- # @return [Google::Cloud::Tasks::V2beta3::Task] The Google Task response
190
+ # @return [Cloudtasker::CloudTask] The Google Task response
159
191
  #
160
192
  def reenqueue(interval)
161
193
  @job_reenqueued = true
@@ -182,7 +214,8 @@ module Cloudtasker
182
214
  worker: self.class.to_s,
183
215
  job_id: job_id,
184
216
  job_args: job_args,
185
- job_meta: job_meta.to_h
217
+ job_meta: job_meta.to_h,
218
+ job_retries: job_retries
186
219
  }
187
220
  end
188
221
 
@@ -207,5 +240,15 @@ module Cloudtasker
207
240
  def ==(other)
208
241
  other.is_a?(self.class) && other.job_id == job_id
209
242
  end
243
+
244
+ #
245
+ # Return true if the job has excceeded its maximum number
246
+ # of retries
247
+ #
248
+ # @return [Boolean] True if the job is dead
249
+ #
250
+ def job_dead?
251
+ job_retries >= Cloudtasker.config.max_retries
252
+ end
210
253
  end
211
254
  end