cloudtasker 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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