cloudtasker-tonix 0.1.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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/lint_rubocop.yml +15 -0
  3. data/.github/workflows/test_ruby_3.x.yml +40 -0
  4. data/.gitignore +23 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +96 -0
  7. data/Appraisals +76 -0
  8. data/CHANGELOG.md +248 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +18 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +1311 -0
  13. data/Rakefile +8 -0
  14. data/_config.yml +1 -0
  15. data/app/controllers/cloudtasker/worker_controller.rb +107 -0
  16. data/bin/console +15 -0
  17. data/bin/setup +8 -0
  18. data/cloudtasker.gemspec +42 -0
  19. data/config/routes.rb +5 -0
  20. data/docs/BATCH_JOBS.md +144 -0
  21. data/docs/CRON_JOBS.md +129 -0
  22. data/docs/STORABLE_JOBS.md +68 -0
  23. data/docs/UNIQUE_JOBS.md +190 -0
  24. data/exe/cloudtasker +30 -0
  25. data/gemfiles/.bundle/config +2 -0
  26. data/gemfiles/google_cloud_tasks_1.0.gemfile +17 -0
  27. data/gemfiles/google_cloud_tasks_1.1.gemfile +17 -0
  28. data/gemfiles/google_cloud_tasks_1.2.gemfile +17 -0
  29. data/gemfiles/google_cloud_tasks_1.3.gemfile +17 -0
  30. data/gemfiles/google_cloud_tasks_1.4.gemfile +17 -0
  31. data/gemfiles/google_cloud_tasks_1.5.gemfile +17 -0
  32. data/gemfiles/google_cloud_tasks_2.0.gemfile +17 -0
  33. data/gemfiles/google_cloud_tasks_2.1.gemfile +17 -0
  34. data/gemfiles/rails_6.1.gemfile +20 -0
  35. data/gemfiles/rails_7.0.gemfile +18 -0
  36. data/gemfiles/rails_7.1.gemfile +18 -0
  37. data/gemfiles/rails_8.0.gemfile +18 -0
  38. data/gemfiles/rails_8.1.gemfile +18 -0
  39. data/gemfiles/semantic_logger_3.4.gemfile +16 -0
  40. data/gemfiles/semantic_logger_4.6.gemfile +16 -0
  41. data/gemfiles/semantic_logger_4.7.0.gemfile +16 -0
  42. data/gemfiles/semantic_logger_4.7.2.gemfile +16 -0
  43. data/lib/active_job/queue_adapters/cloudtasker_adapter.rb +89 -0
  44. data/lib/cloudtasker/authentication_error.rb +6 -0
  45. data/lib/cloudtasker/authenticator.rb +90 -0
  46. data/lib/cloudtasker/backend/google_cloud_task_v1.rb +228 -0
  47. data/lib/cloudtasker/backend/google_cloud_task_v2.rb +231 -0
  48. data/lib/cloudtasker/backend/memory_task.rb +202 -0
  49. data/lib/cloudtasker/backend/redis_task.rb +291 -0
  50. data/lib/cloudtasker/batch/batch_progress.rb +142 -0
  51. data/lib/cloudtasker/batch/extension/worker.rb +13 -0
  52. data/lib/cloudtasker/batch/job.rb +558 -0
  53. data/lib/cloudtasker/batch/middleware/server.rb +14 -0
  54. data/lib/cloudtasker/batch/middleware.rb +25 -0
  55. data/lib/cloudtasker/batch.rb +5 -0
  56. data/lib/cloudtasker/cli.rb +194 -0
  57. data/lib/cloudtasker/cloud_task.rb +130 -0
  58. data/lib/cloudtasker/config.rb +319 -0
  59. data/lib/cloudtasker/cron/job.rb +205 -0
  60. data/lib/cloudtasker/cron/middleware/server.rb +14 -0
  61. data/lib/cloudtasker/cron/middleware.rb +20 -0
  62. data/lib/cloudtasker/cron/schedule.rb +308 -0
  63. data/lib/cloudtasker/cron.rb +5 -0
  64. data/lib/cloudtasker/dead_worker_error.rb +6 -0
  65. data/lib/cloudtasker/engine.rb +24 -0
  66. data/lib/cloudtasker/invalid_worker_error.rb +6 -0
  67. data/lib/cloudtasker/local_server.rb +99 -0
  68. data/lib/cloudtasker/max_task_size_exceeded_error.rb +14 -0
  69. data/lib/cloudtasker/meta_store.rb +86 -0
  70. data/lib/cloudtasker/middleware/chain.rb +250 -0
  71. data/lib/cloudtasker/missing_worker_arguments_error.rb +6 -0
  72. data/lib/cloudtasker/redis_client.rb +166 -0
  73. data/lib/cloudtasker/retry_worker_error.rb +6 -0
  74. data/lib/cloudtasker/storable/worker.rb +78 -0
  75. data/lib/cloudtasker/storable.rb +3 -0
  76. data/lib/cloudtasker/testing.rb +184 -0
  77. data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +39 -0
  78. data/lib/cloudtasker/unique_job/conflict_strategy/raise.rb +28 -0
  79. data/lib/cloudtasker/unique_job/conflict_strategy/reject.rb +11 -0
  80. data/lib/cloudtasker/unique_job/conflict_strategy/reschedule.rb +30 -0
  81. data/lib/cloudtasker/unique_job/job.rb +168 -0
  82. data/lib/cloudtasker/unique_job/lock/base_lock.rb +70 -0
  83. data/lib/cloudtasker/unique_job/lock/no_op.rb +11 -0
  84. data/lib/cloudtasker/unique_job/lock/until_completed.rb +40 -0
  85. data/lib/cloudtasker/unique_job/lock/until_executed.rb +36 -0
  86. data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
  87. data/lib/cloudtasker/unique_job/lock/while_executing.rb +25 -0
  88. data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
  89. data/lib/cloudtasker/unique_job/middleware/client.rb +15 -0
  90. data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
  91. data/lib/cloudtasker/unique_job/middleware.rb +36 -0
  92. data/lib/cloudtasker/unique_job.rb +32 -0
  93. data/lib/cloudtasker/version.rb +5 -0
  94. data/lib/cloudtasker/worker.rb +487 -0
  95. data/lib/cloudtasker/worker_handler.rb +250 -0
  96. data/lib/cloudtasker/worker_logger.rb +231 -0
  97. data/lib/cloudtasker/worker_wrapper.rb +52 -0
  98. data/lib/cloudtasker.rb +57 -0
  99. data/lib/tasks/setup_queue.rake +20 -0
  100. metadata +241 -0
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ module UniqueJob
5
+ module Middleware
6
+ # Server middleware, invoked when jobs are executed
7
+ class Server
8
+ def call(worker, **_kwargs, &block)
9
+ Job.new(worker).lock_instance.execute(&block)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/redis_client'
4
+
5
+ require_relative 'lock_error'
6
+
7
+ require_relative 'conflict_strategy/base_strategy'
8
+ require_relative 'conflict_strategy/raise'
9
+ require_relative 'conflict_strategy/reject'
10
+ require_relative 'conflict_strategy/reschedule'
11
+
12
+ require_relative 'lock/base_lock'
13
+ require_relative 'lock/no_op'
14
+ require_relative 'lock/until_executed'
15
+ require_relative 'lock/until_executing'
16
+ require_relative 'lock/while_executing'
17
+ require_relative 'lock/until_completed'
18
+
19
+ require_relative 'job'
20
+
21
+ require_relative 'middleware/client'
22
+ require_relative 'middleware/server'
23
+
24
+ module Cloudtasker
25
+ module UniqueJob
26
+ # Registration module
27
+ module Middleware
28
+ def self.configure
29
+ Cloudtasker.configure do |config|
30
+ config.client_middleware { |c| c.add(Middleware::Client) }
31
+ config.server_middleware { |c| c.add(Middleware::Server) }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'unique_job/middleware'
4
+
5
+ Cloudtasker::UniqueJob::Middleware.configure
6
+
7
+ module Cloudtasker
8
+ # UniqueJob configurator
9
+ module UniqueJob
10
+ # The maximum duration a lock can remain in place
11
+ # after schedule time.
12
+ DEFAULT_LOCK_TTL = 10 * 60 # 10 minutes
13
+
14
+ class << self
15
+ attr_writer :lock_ttl
16
+
17
+ # Configure the middleware
18
+ def configure
19
+ yield(self)
20
+ end
21
+
22
+ #
23
+ # Return the max TTL for locks
24
+ #
25
+ # @return [Integer] The lock TTL.
26
+ #
27
+ def lock_ttl
28
+ @lock_ttl || DEFAULT_LOCK_TTL
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,487 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ # Cloud Task based workers
5
+ module Worker
6
+ # Add class method to including class
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ base.attr_writer :job_queue
10
+ base.attr_accessor :job_args, :job_id, :job_meta, :job_reenqueued, :job_retries,
11
+ :perform_started_at, :perform_ended_at, :task_id
12
+ end
13
+
14
+ #
15
+ # Return a worker instance from a serialized worker.
16
+ # A worker can be serialized by calling `MyWorker#to_json`
17
+ #
18
+ # @param [String] json Worker serialized as json.
19
+ #
20
+ # @return [Cloudtasker::Worker, nil] The instantiated worker.
21
+ #
22
+ def self.from_json(json)
23
+ from_hash(JSON.parse(json))
24
+ rescue JSON::ParserError
25
+ nil
26
+ end
27
+
28
+ #
29
+ # Return a worker instance from a worker hash description.
30
+ # A worker hash description is typically generated by calling `MyWorker#to_h`
31
+ #
32
+ # @param [Hash] hash A worker hash description.
33
+ #
34
+ # @return [Cloudtasker::Worker, nil] The instantiated worker.
35
+ #
36
+ def self.from_hash(hash)
37
+ # Symbolize metadata keys and stringify job arguments
38
+ payload = JSON.parse(hash.to_json, symbolize_names: true)
39
+ payload[:job_args] = JSON.parse(payload[:job_args].to_json)
40
+
41
+ # Extract worker parameters
42
+ klass_name = payload&.dig(:worker)
43
+ return nil unless klass_name
44
+
45
+ # Check that worker class is a valid worker
46
+ worker_klass = Object.const_get(klass_name)
47
+ return nil unless worker_klass.include?(self)
48
+
49
+ # Return instantiated worker
50
+ worker_klass.new(**payload.slice(:job_queue, :job_args, :job_id, :job_meta, :job_retries, :task_id))
51
+ rescue NameError
52
+ nil
53
+ end
54
+
55
+ # Module class methods
56
+ module ClassMethods
57
+ #
58
+ # Return the Cloudtasker redis client
59
+ #
60
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client.
61
+ #
62
+ def redis
63
+ @redis ||= RedisClient.new
64
+ end
65
+
66
+ #
67
+ # Set the worker runtime options.
68
+ #
69
+ # @param [Hash] opts The worker options.
70
+ #
71
+ # @return [Hash] The options set.
72
+ #
73
+ def cloudtasker_options(opts = {})
74
+ opt_list = opts&.map { |k, v| [k.to_sym, v] } || [] # symbolize
75
+ @cloudtasker_options_hash = opt_list.to_h
76
+ end
77
+
78
+ #
79
+ # Return the worker runtime options.
80
+ #
81
+ # @return [Hash] The worker runtime options.
82
+ #
83
+ def cloudtasker_options_hash
84
+ @cloudtasker_options_hash || {}
85
+ end
86
+
87
+ #
88
+ # Return a namespaced cache key.
89
+ #
90
+ # @param [Any, Array<Any>, nil] val The key to namespace
91
+ #
92
+ # @return [String] The namespaced key(s).
93
+ #
94
+ def cache_key(val = nil)
95
+ [to_s.underscore, val].flatten.compact.map(&:to_s).join('/')
96
+ end
97
+
98
+ #
99
+ # Enqueue worker in the background.
100
+ #
101
+ # @param [Array<any>] *args List of worker arguments
102
+ #
103
+ # @return [Cloudtasker::CloudTask] The Google Task response
104
+ #
105
+ def perform_async(*args)
106
+ schedule(args: args)
107
+ end
108
+
109
+ #
110
+ # Enqueue worker and delay processing.
111
+ #
112
+ # @param [Integer, nil] interval The delay in seconds.
113
+ # @param [Array<any>] *args List of worker arguments.
114
+ #
115
+ # @return [Cloudtasker::CloudTask] The Google Task response
116
+ #
117
+ def perform_in(interval, *args)
118
+ schedule(args: args, time_in: interval)
119
+ end
120
+
121
+ #
122
+ # Enqueue worker and delay processing.
123
+ #
124
+ # @param [Time, Integer] time_at The time at which the job should run.
125
+ # @param [Array<any>] *args List of worker arguments
126
+ #
127
+ # @return [Cloudtasker::CloudTask] The Google Task response
128
+ #
129
+ def perform_at(time_at, *args)
130
+ schedule(args: args, time_at: time_at)
131
+ end
132
+
133
+ #
134
+ # Perform a worker inline, without sending it to the queue for processing.
135
+ #
136
+ # Middlewares (unique job, batch etc.) will still be invoked as if the job
137
+ # had been scheduled.
138
+ #
139
+ # @param [Array<any>] *args List of worker arguments
140
+ #
141
+ # @return [Any] The result of the worker perform method.
142
+ #
143
+ def perform_now(*args)
144
+ # Serialize/deserialize arguments to mimic job enqueueing and produce a similar context
145
+ job_args = JSON.parse(args.to_json)
146
+ new(job_args: job_args).execute
147
+ end
148
+
149
+ #
150
+ # Enqueue a worker with explicity options.
151
+ #
152
+ # @param [Array<any>] args The job arguments.
153
+ # @param [Time, Integer] time_in The delay in seconds.
154
+ # @param [Time, Integer] time_at The time at which the job should run.
155
+ # @param [String, Symbol] queue The queue on which the worker should run.
156
+ #
157
+ # @return [Cloudtasker::CloudTask] The Google Task response
158
+ #
159
+ def schedule(args: nil, time_in: nil, time_at: nil, queue: nil)
160
+ new(job_args: args, job_queue: queue).schedule(**{ interval: time_in, time_at: time_at }.compact)
161
+ end
162
+
163
+ #
164
+ # Return the numbeer of times this worker will be retried.
165
+ #
166
+ # @return [Integer] The number of retries.
167
+ #
168
+ def max_retries
169
+ cloudtasker_options_hash[:max_retries] || Cloudtasker.config.max_retries
170
+ end
171
+ end
172
+
173
+ #
174
+ # Build a new worker instance.
175
+ #
176
+ # @param [Array<any>] job_args The list of perform args.
177
+ # @param [String] job_id A unique ID identifying this job.
178
+ #
179
+ def initialize(job_queue: nil, job_args: nil, job_id: nil, job_meta: {}, job_retries: 0, task_id: nil)
180
+ @job_args = job_args || []
181
+ @job_id = job_id || SecureRandom.uuid
182
+ @job_meta = MetaStore.new(job_meta)
183
+ @job_retries = job_retries || 0
184
+ @job_queue = job_queue
185
+ @task_id = task_id
186
+ end
187
+
188
+ #
189
+ # Return the class name of the worker.
190
+ #
191
+ # @return [String] The class name.
192
+ #
193
+ def job_class_name
194
+ self.class.to_s
195
+ end
196
+
197
+ #
198
+ # Return the queue to use for this worker.
199
+ #
200
+ # @return [String] The name of queue.
201
+ #
202
+ def job_queue
203
+ (
204
+ @job_queue ||=
205
+ Thread.current[:cloudtasker_propagated_queue] ||
206
+ self.class.cloudtasker_options_hash[:queue] ||
207
+ Config::DEFAULT_JOB_QUEUE
208
+ ).to_s
209
+ end
210
+
211
+ #
212
+ # Return the Dispatch deadline duration. Cloud Tasks will timeout the job after
213
+ # this duration is elapsed.
214
+ #
215
+ # @return [Integer] The value in seconds.
216
+ #
217
+ def dispatch_deadline
218
+ @dispatch_deadline ||= begin
219
+ configured_deadline = (
220
+ self.class.cloudtasker_options_hash[:dispatch_deadline] ||
221
+ Cloudtasker.config.dispatch_deadline
222
+ ).to_i
223
+ configured_deadline.clamp(Config::MIN_DISPATCH_DEADLINE, Config::MAX_DISPATCH_DEADLINE)
224
+ end
225
+ end
226
+
227
+ #
228
+ # Return the Cloudtasker logger instance.
229
+ #
230
+ # @return [Logger, any] The cloudtasker logger.
231
+ #
232
+ def logger
233
+ @logger ||= WorkerLogger.new(self)
234
+ end
235
+
236
+ #
237
+ # Execute the worker by calling the `perform` with the args.
238
+ #
239
+ # @return [Any] The result of the worker perform method.
240
+ #
241
+ def execute
242
+ logger.info('Starting job...')
243
+
244
+ # Perform job logic
245
+ resp = execute_middleware_chain
246
+
247
+ # Log job completion and return result
248
+ logger.info("Job done after #{job_duration}s") { { duration: job_duration * 1000 } }
249
+ resp
250
+ rescue DeadWorkerError => e
251
+ logger.info("Job dead after #{job_duration}s and #{job_retries} retries") { { duration: job_duration * 1000 } }
252
+ raise(e)
253
+ rescue RetryWorkerError => e
254
+ logger.info("Job done after #{job_duration}s (retry requested)") do
255
+ { duration: job_duration * 1000, reason: e.message }
256
+ end
257
+ raise(e)
258
+ rescue StandardError => e
259
+ logger.info("Job failed after #{job_duration}s") { { duration: job_duration * 1000 } }
260
+ raise(e)
261
+ end
262
+
263
+ #
264
+ # Return a unix timestamp specifying when to run the task.
265
+ #
266
+ # @param [Integer, nil] interval The time to wait.
267
+ # @param [Integer, nil] time_at The time at which the job should run.
268
+ #
269
+ # @return [Integer, nil] The Unix timestamp.
270
+ #
271
+ def schedule_time(interval: nil, time_at: nil)
272
+ return nil unless interval || time_at
273
+
274
+ # Generate the complete Unix timestamp
275
+ (time_at || Time.now).to_i + interval.to_i
276
+ end
277
+
278
+ #
279
+ # Enqueue a worker, with or without delay.
280
+ #
281
+ # @param [Integer] interval The delay in seconds.
282
+ # @param [Time, Integer] interval The time at which the job should run
283
+ #
284
+ # @return [Cloudtasker::CloudTask, nil] The Google Task response or nil if the job was not scheduled
285
+ #
286
+ def schedule(**args)
287
+ # Evaluate when to schedule the job
288
+ time_at = schedule_time(**args)
289
+
290
+ # Schedule job through client middlewares
291
+ Cloudtasker.config.client_middleware.invoke(self, time_at: time_at) do
292
+ WorkerHandler.new(self).schedule(time_at: time_at)
293
+ end
294
+ end
295
+
296
+ #
297
+ # Helper method used to re-enqueue the job. Re-enqueued
298
+ # jobs keep the same job_id.
299
+ #
300
+ # This helper may be useful when jobs must pause activity due to external
301
+ # factors such as when a third-party API is throttling the rate of API calls.
302
+ #
303
+ # @param [Integer] interval Delay to wait before processing the job again (in seconds).
304
+ #
305
+ # @return [Cloudtasker::CloudTask] The Google Task response
306
+ #
307
+ def reenqueue(interval)
308
+ @job_reenqueued = true
309
+ schedule(interval: interval)
310
+ end
311
+
312
+ #
313
+ # Return a new instance of the worker with the same args and metadata
314
+ # but with a different id.
315
+ #
316
+ # @return [Cloudtasker::Worker] <description>
317
+ #
318
+ def new_instance
319
+ self.class.new(job_queue: job_queue, job_args: job_args, job_meta: job_meta)
320
+ end
321
+
322
+ #
323
+ # Return a hash description of the worker.
324
+ #
325
+ # @return [Hash] The worker hash description.
326
+ #
327
+ def to_h
328
+ {
329
+ worker: self.class.to_s,
330
+ job_id: job_id,
331
+ job_args: job_args,
332
+ job_meta: job_meta.to_h,
333
+ job_retries: job_retries,
334
+ job_queue: job_queue,
335
+ task_id: task_id
336
+ }
337
+ end
338
+
339
+ #
340
+ # Return a json representation of the worker.
341
+ #
342
+ # @param [Array<any>] *args Arguments passed to to_json.
343
+ #
344
+ # @return [String] The worker json representation.
345
+ #
346
+ def to_json(*args)
347
+ to_h.to_json(*args)
348
+ end
349
+
350
+ #
351
+ # Equality operator.
352
+ #
353
+ # @param [Any] other The object to compare.
354
+ #
355
+ # @return [Boolean] True if the object is equal.
356
+ #
357
+ def ==(other)
358
+ other.is_a?(self.class) && other.job_id == job_id
359
+ end
360
+
361
+ #
362
+ # Return the max number of retries allowed for this job.
363
+ #
364
+ # The order of precedence for retry lookup is:
365
+ # - Worker `max_retries` method
366
+ # - Class `max_retries` option
367
+ # - Cloudtasker `max_retries` config option
368
+ #
369
+ # @return [Integer] The number of retries
370
+ #
371
+ def job_max_retries
372
+ @job_max_retries ||= try(:max_retries, *job_args) || self.class.max_retries
373
+ end
374
+
375
+ #
376
+ # Return true if the job must declared dead upon raising
377
+ # an error.
378
+ #
379
+ # @return [Boolean] True if the job must die on error.
380
+ #
381
+ def job_must_die?
382
+ job_retries >= job_max_retries
383
+ end
384
+
385
+ #
386
+ # Return true if the job has strictly excceeded its maximum number
387
+ # of retries.
388
+ #
389
+ # Used a preemptive filter when running the job.
390
+ #
391
+ # @return [Boolean] True if the job is dead
392
+ #
393
+ def job_dead?
394
+ job_retries > job_max_retries
395
+ end
396
+
397
+ #
398
+ # Return true if the job arguments are missing.
399
+ #
400
+ # This may happen if a job
401
+ # was successfully run but retried due to Cloud Task dispatch deadline
402
+ # exceeded. If the arguments were stored in Redis then they may have
403
+ # been flushed already after the successful completion.
404
+ #
405
+ # If job arguments are missing then the job will simply be declared dead.
406
+ #
407
+ # @return [Boolean] True if the arguments are missing.
408
+ #
409
+ def arguments_missing?
410
+ job_args.empty? && ![0, -1].include?(method(:perform).arity)
411
+ end
412
+
413
+ #
414
+ # Return the time taken (in seconds) to perform the job. This duration
415
+ # includes the middlewares and the actual perform method.
416
+ #
417
+ # @return [Float] The time taken in seconds as a floating point number.
418
+ #
419
+ def job_duration
420
+ return 0.0 unless perform_ended_at && perform_started_at
421
+
422
+ @job_duration ||= (perform_ended_at - perform_started_at).ceil(3)
423
+ end
424
+
425
+ #
426
+ # Run worker callback.
427
+ #
428
+ # @param [String, Symbol] callback The callback to run.
429
+ # @param [Array<any>] *args The callback arguments.
430
+ #
431
+ # @return [any] The callback return value
432
+ #
433
+ def run_callback(callback, *args)
434
+ try(callback, *args)
435
+ end
436
+
437
+ #=============================
438
+ # Private
439
+ #=============================
440
+ private
441
+
442
+ #
443
+ # Flag the worker as dead by invoking the on_dead hook
444
+ # and raising a DeadWorkerError
445
+ #
446
+ # @param [Exception, nil] error An optional exception to be passed to the DeadWorkerError.
447
+ #
448
+ def flag_as_dead(error = nil)
449
+ run_callback(:on_dead, error || DeadWorkerError.new)
450
+ ensure
451
+ raise(DeadWorkerError, error)
452
+ end
453
+
454
+ #
455
+ # Execute the worker perform method through the middleware chain.
456
+ #
457
+ # @return [Any] The result of the perform method.
458
+ #
459
+ def execute_middleware_chain
460
+ self.perform_started_at = Time.now
461
+
462
+ # Store the parent worker queue so as to propagate it to the child workers
463
+ # See: #job_queue
464
+ Thread.current[:cloudtasker_propagated_queue] = job_queue if self.class.cloudtasker_options_hash[:propagate_queue]
465
+
466
+ Cloudtasker.config.server_middleware.invoke(self) do
467
+ # Immediately abort the job if it is already dead
468
+ flag_as_dead if job_dead?
469
+ flag_as_dead(MissingWorkerArgumentsError.new('worker arguments are missing')) if arguments_missing?
470
+
471
+ begin
472
+ # Perform the job
473
+ perform(*job_args)
474
+ rescue StandardError => e
475
+ run_callback(:on_error, e) unless e.is_a?(RetryWorkerError)
476
+ return raise(e) unless job_must_die?
477
+
478
+ # Flag job as dead
479
+ flag_as_dead(e)
480
+ end
481
+ end
482
+ ensure
483
+ self.perform_ended_at = Time.now
484
+ Thread.current[:cloudtasker_propagated_queue] = nil
485
+ end
486
+ end
487
+ end