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.
- checksums.yaml +7 -0
- data/.github/workflows/lint_rubocop.yml +15 -0
- data/.github/workflows/test_ruby_3.x.yml +40 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/.rubocop.yml +96 -0
- data/Appraisals +76 -0
- data/CHANGELOG.md +248 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +1311 -0
- data/Rakefile +8 -0
- data/_config.yml +1 -0
- data/app/controllers/cloudtasker/worker_controller.rb +107 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cloudtasker.gemspec +42 -0
- data/config/routes.rb +5 -0
- data/docs/BATCH_JOBS.md +144 -0
- data/docs/CRON_JOBS.md +129 -0
- data/docs/STORABLE_JOBS.md +68 -0
- data/docs/UNIQUE_JOBS.md +190 -0
- data/exe/cloudtasker +30 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/google_cloud_tasks_1.0.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.1.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.2.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.3.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.4.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.5.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_2.0.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_2.1.gemfile +17 -0
- data/gemfiles/rails_6.1.gemfile +20 -0
- data/gemfiles/rails_7.0.gemfile +18 -0
- data/gemfiles/rails_7.1.gemfile +18 -0
- data/gemfiles/rails_8.0.gemfile +18 -0
- data/gemfiles/rails_8.1.gemfile +18 -0
- data/gemfiles/semantic_logger_3.4.gemfile +16 -0
- data/gemfiles/semantic_logger_4.6.gemfile +16 -0
- data/gemfiles/semantic_logger_4.7.0.gemfile +16 -0
- data/gemfiles/semantic_logger_4.7.2.gemfile +16 -0
- data/lib/active_job/queue_adapters/cloudtasker_adapter.rb +89 -0
- data/lib/cloudtasker/authentication_error.rb +6 -0
- data/lib/cloudtasker/authenticator.rb +90 -0
- data/lib/cloudtasker/backend/google_cloud_task_v1.rb +228 -0
- data/lib/cloudtasker/backend/google_cloud_task_v2.rb +231 -0
- data/lib/cloudtasker/backend/memory_task.rb +202 -0
- data/lib/cloudtasker/backend/redis_task.rb +291 -0
- data/lib/cloudtasker/batch/batch_progress.rb +142 -0
- data/lib/cloudtasker/batch/extension/worker.rb +13 -0
- data/lib/cloudtasker/batch/job.rb +558 -0
- data/lib/cloudtasker/batch/middleware/server.rb +14 -0
- data/lib/cloudtasker/batch/middleware.rb +25 -0
- data/lib/cloudtasker/batch.rb +5 -0
- data/lib/cloudtasker/cli.rb +194 -0
- data/lib/cloudtasker/cloud_task.rb +130 -0
- data/lib/cloudtasker/config.rb +319 -0
- data/lib/cloudtasker/cron/job.rb +205 -0
- data/lib/cloudtasker/cron/middleware/server.rb +14 -0
- data/lib/cloudtasker/cron/middleware.rb +20 -0
- data/lib/cloudtasker/cron/schedule.rb +308 -0
- data/lib/cloudtasker/cron.rb +5 -0
- data/lib/cloudtasker/dead_worker_error.rb +6 -0
- data/lib/cloudtasker/engine.rb +24 -0
- data/lib/cloudtasker/invalid_worker_error.rb +6 -0
- data/lib/cloudtasker/local_server.rb +99 -0
- data/lib/cloudtasker/max_task_size_exceeded_error.rb +14 -0
- data/lib/cloudtasker/meta_store.rb +86 -0
- data/lib/cloudtasker/middleware/chain.rb +250 -0
- data/lib/cloudtasker/missing_worker_arguments_error.rb +6 -0
- data/lib/cloudtasker/redis_client.rb +166 -0
- data/lib/cloudtasker/retry_worker_error.rb +6 -0
- data/lib/cloudtasker/storable/worker.rb +78 -0
- data/lib/cloudtasker/storable.rb +3 -0
- data/lib/cloudtasker/testing.rb +184 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +39 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/raise.rb +28 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/reject.rb +11 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/reschedule.rb +30 -0
- data/lib/cloudtasker/unique_job/job.rb +168 -0
- data/lib/cloudtasker/unique_job/lock/base_lock.rb +70 -0
- data/lib/cloudtasker/unique_job/lock/no_op.rb +11 -0
- data/lib/cloudtasker/unique_job/lock/until_completed.rb +40 -0
- data/lib/cloudtasker/unique_job/lock/until_executed.rb +36 -0
- data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
- data/lib/cloudtasker/unique_job/lock/while_executing.rb +25 -0
- data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
- data/lib/cloudtasker/unique_job/middleware/client.rb +15 -0
- data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
- data/lib/cloudtasker/unique_job/middleware.rb +36 -0
- data/lib/cloudtasker/unique_job.rb +32 -0
- data/lib/cloudtasker/version.rb +5 -0
- data/lib/cloudtasker/worker.rb +487 -0
- data/lib/cloudtasker/worker_handler.rb +250 -0
- data/lib/cloudtasker/worker_logger.rb +231 -0
- data/lib/cloudtasker/worker_wrapper.rb +52 -0
- data/lib/cloudtasker.rb +57 -0
- data/lib/tasks/setup_queue.rake +20 -0
- 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,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
|