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,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'google/cloud/tasks'
|
|
4
|
+
|
|
5
|
+
module Cloudtasker
|
|
6
|
+
# Build, serialize and schedule tasks on the processing backend.
|
|
7
|
+
class WorkerHandler
|
|
8
|
+
attr_reader :worker
|
|
9
|
+
|
|
10
|
+
# Alrogith used to sign the verification token
|
|
11
|
+
JWT_ALG = 'HS256'
|
|
12
|
+
|
|
13
|
+
# Sub-namespace to use for redis keys when storing
|
|
14
|
+
# payloads in Redis
|
|
15
|
+
REDIS_PAYLOAD_NAMESPACE = 'payload'
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Return a namespaced key
|
|
19
|
+
#
|
|
20
|
+
# @param [String, Symbol] val The key to namespace
|
|
21
|
+
#
|
|
22
|
+
# @return [String] The namespaced key.
|
|
23
|
+
#
|
|
24
|
+
def self.key(val)
|
|
25
|
+
return nil if val.nil?
|
|
26
|
+
|
|
27
|
+
[to_s.underscore, val.to_s].join('/')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
# Return the cloudtasker redis client
|
|
32
|
+
#
|
|
33
|
+
# @return [Cloudtasker::RedisClient] The cloudtasker redis client.
|
|
34
|
+
#
|
|
35
|
+
def self.redis
|
|
36
|
+
@redis ||= begin
|
|
37
|
+
require 'cloudtasker/redis_client'
|
|
38
|
+
RedisClient.new
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#
|
|
43
|
+
# Log error on execution failure.
|
|
44
|
+
#
|
|
45
|
+
# @param [Cloudtasker::Worker, nil] worker The worker.
|
|
46
|
+
# @param [Exception] error The error to log.
|
|
47
|
+
#
|
|
48
|
+
# @void
|
|
49
|
+
#
|
|
50
|
+
def self.log_execution_error(worker, error)
|
|
51
|
+
# ActiveJob has its own error logging. No need to double log the error.
|
|
52
|
+
# Note: we use string matching instead of class matching as
|
|
53
|
+
# ActiveJob::QueueAdapters::CloudtaskerAdapter::JobWrapper might not be loaded
|
|
54
|
+
return if worker.class.to_s =~ /^ActiveJob::/
|
|
55
|
+
|
|
56
|
+
# Do not log error when a retry was specifically requested
|
|
57
|
+
return if error.is_a?(RetryWorkerError)
|
|
58
|
+
|
|
59
|
+
# Choose logger to use based on context
|
|
60
|
+
# Worker will be nil on InvalidWorkerError - in that case we use generic logging
|
|
61
|
+
logger = worker&.logger || Cloudtasker.logger
|
|
62
|
+
|
|
63
|
+
# Log error
|
|
64
|
+
logger.error(error)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#
|
|
68
|
+
# Execute a task worker from a task payload
|
|
69
|
+
#
|
|
70
|
+
# @param [Hash] input_payload The Cloud Task payload.
|
|
71
|
+
#
|
|
72
|
+
# @return [Any] The return value of the worker perform method.
|
|
73
|
+
#
|
|
74
|
+
def self.execute_from_payload!(input_payload)
|
|
75
|
+
with_worker_handling(input_payload, &:execute)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
#
|
|
79
|
+
# Local middleware used to retrieve the job arg payload from cache
|
|
80
|
+
# if a arg payload reference is present.
|
|
81
|
+
#
|
|
82
|
+
# @param [Hash] payload The full job payload
|
|
83
|
+
#
|
|
84
|
+
# @yield [Hash] The actual payload to use to process the job.
|
|
85
|
+
#
|
|
86
|
+
# @return [Any] The block result
|
|
87
|
+
#
|
|
88
|
+
def self.with_worker_handling(input_payload)
|
|
89
|
+
# Extract payload information
|
|
90
|
+
extracted_payload = extract_payload(input_payload)
|
|
91
|
+
payload = extracted_payload[:payload]
|
|
92
|
+
args_payload_key = extracted_payload[:args_payload_key]
|
|
93
|
+
|
|
94
|
+
# Build worker
|
|
95
|
+
worker = Cloudtasker::Worker.from_hash(payload) || raise(InvalidWorkerError)
|
|
96
|
+
|
|
97
|
+
# Yied worker
|
|
98
|
+
resp = yield(worker)
|
|
99
|
+
|
|
100
|
+
# Delete stored args payload if job has completed
|
|
101
|
+
redis.del(args_payload_key) if args_payload_key && !worker.job_reenqueued
|
|
102
|
+
|
|
103
|
+
resp
|
|
104
|
+
rescue DeadWorkerError => e
|
|
105
|
+
# Delete stored args payload if job is dead
|
|
106
|
+
redis.del(args_payload_key) if args_payload_key
|
|
107
|
+
log_execution_error(worker, e)
|
|
108
|
+
Cloudtasker.config.on_dead.call(e, worker)
|
|
109
|
+
raise(e)
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
log_execution_error(worker, e)
|
|
112
|
+
Cloudtasker.config.on_error.call(e, worker)
|
|
113
|
+
raise(e)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
#
|
|
117
|
+
# Return the argument payload key (if present) along with the actual worker payload.
|
|
118
|
+
#
|
|
119
|
+
# If the payload was stored in Redis then retrieve it.
|
|
120
|
+
#
|
|
121
|
+
# @return [Hash] Hash
|
|
122
|
+
#
|
|
123
|
+
def self.extract_payload(input_payload)
|
|
124
|
+
# Get references
|
|
125
|
+
payload = JSON.parse(input_payload.to_json, symbolize_names: true)
|
|
126
|
+
args_payload_id = payload.delete(:job_args_payload_id)
|
|
127
|
+
args_payload_key = args_payload_id ? key([REDIS_PAYLOAD_NAMESPACE, args_payload_id].join('/')) : nil
|
|
128
|
+
|
|
129
|
+
# Retrieve the actual worker args payload
|
|
130
|
+
args_payload = args_payload_key ? redis.fetch(args_payload_key) : payload[:job_args]
|
|
131
|
+
|
|
132
|
+
# Return the payload
|
|
133
|
+
{
|
|
134
|
+
args_payload_key: args_payload_key,
|
|
135
|
+
payload: payload.merge(job_args: args_payload)
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
#
|
|
140
|
+
# Prepare a new cloud task.
|
|
141
|
+
#
|
|
142
|
+
# @param [Cloudtasker::Worker] worker The worker instance.
|
|
143
|
+
#
|
|
144
|
+
def initialize(worker)
|
|
145
|
+
@worker = worker
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#
|
|
149
|
+
# Return the full task configuration sent to Cloud Task
|
|
150
|
+
#
|
|
151
|
+
# @return [Hash] The task body
|
|
152
|
+
#
|
|
153
|
+
def task_payload
|
|
154
|
+
# Generate content
|
|
155
|
+
worker_payload_json = worker_payload.to_json
|
|
156
|
+
|
|
157
|
+
# Build payload
|
|
158
|
+
{
|
|
159
|
+
http_request: {
|
|
160
|
+
http_method: 'POST',
|
|
161
|
+
url: Cloudtasker.config.processor_url,
|
|
162
|
+
headers: {
|
|
163
|
+
Cloudtasker::Config::CONTENT_TYPE_HEADER => 'application/json',
|
|
164
|
+
Cloudtasker::Config::CT_SIGNATURE_HEADER => Authenticator.sign_payload(worker_payload_json)
|
|
165
|
+
}.compact,
|
|
166
|
+
oidc_token: Cloudtasker.config.oidc,
|
|
167
|
+
body: worker_payload_json
|
|
168
|
+
}.compact,
|
|
169
|
+
dispatch_deadline: worker.dispatch_deadline.to_i,
|
|
170
|
+
queue: worker.job_queue
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
#
|
|
175
|
+
# Return true if the worker args must be stored in Redis.
|
|
176
|
+
#
|
|
177
|
+
# @return [Boolean] True if the payload must be stored in redis.
|
|
178
|
+
#
|
|
179
|
+
def store_payload_in_redis?
|
|
180
|
+
Cloudtasker.config.redis_payload_storage_threshold &&
|
|
181
|
+
worker.job_args.to_json.bytesize > (Cloudtasker.config.redis_payload_storage_threshold * 1024)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
#
|
|
185
|
+
# Return the payload to use for job arguments. This payload
|
|
186
|
+
# is merged inside the #worker_payload.
|
|
187
|
+
#
|
|
188
|
+
# If the argument payload must be stored in Redis then returns:
|
|
189
|
+
# `{ job_args_payload_id: <worker_id> }`
|
|
190
|
+
#
|
|
191
|
+
# If the argument payload must be natively handled by the backend
|
|
192
|
+
# then returns:
|
|
193
|
+
# `{ job_args: [...] }`
|
|
194
|
+
#
|
|
195
|
+
# @return [Hash] The worker args payload.
|
|
196
|
+
#
|
|
197
|
+
def worker_args_payload
|
|
198
|
+
@worker_args_payload ||= if store_payload_in_redis?
|
|
199
|
+
# Store payload in Redis
|
|
200
|
+
self.class.redis.write(
|
|
201
|
+
self.class.key([REDIS_PAYLOAD_NAMESPACE, worker.job_id].join('/')),
|
|
202
|
+
worker.job_args
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Return reference to args payload
|
|
206
|
+
{ job_args_payload_id: worker.job_id }
|
|
207
|
+
else
|
|
208
|
+
# Return regular job args payload
|
|
209
|
+
{ job_args: worker.job_args }
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
#
|
|
214
|
+
# Return the task payload that Google Task will eventually
|
|
215
|
+
# send to the job processor.
|
|
216
|
+
#
|
|
217
|
+
# The payload includes the worker name and the arguments to
|
|
218
|
+
# pass to the worker.
|
|
219
|
+
#
|
|
220
|
+
# The worker arguments should use primitive types as much
|
|
221
|
+
# as possible as all arguments will be serialized to JSON.
|
|
222
|
+
#
|
|
223
|
+
# @return [Hash] The job payload
|
|
224
|
+
#
|
|
225
|
+
def worker_payload
|
|
226
|
+
@worker_payload ||= {
|
|
227
|
+
worker: worker.job_class_name,
|
|
228
|
+
job_queue: worker.job_queue,
|
|
229
|
+
job_id: worker.job_id,
|
|
230
|
+
job_meta: worker.job_meta.to_h
|
|
231
|
+
}.merge(worker_args_payload)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
#
|
|
235
|
+
# Schedule the task on GCP Cloud Task.
|
|
236
|
+
#
|
|
237
|
+
# @param [Integer, nil] time_at A unix timestamp specifying when to run the job.
|
|
238
|
+
# Leave to `nil` to run now.
|
|
239
|
+
#
|
|
240
|
+
# @return [Cloudtasker::CloudTask] The Google Task response
|
|
241
|
+
#
|
|
242
|
+
def schedule(time_at: nil)
|
|
243
|
+
# Generate task payload
|
|
244
|
+
task = task_payload.merge(schedule_time: time_at).compact
|
|
245
|
+
|
|
246
|
+
# Create and return remote task
|
|
247
|
+
CloudTask.create(task)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
# Add contextual information to logs generated
|
|
5
|
+
# by workers
|
|
6
|
+
class WorkerLogger
|
|
7
|
+
attr_accessor :worker
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_accessor :log_context_processor
|
|
11
|
+
|
|
12
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
13
|
+
#
|
|
14
|
+
# Truncate an array or hash payload.
|
|
15
|
+
#
|
|
16
|
+
# This can be used to log arguments on jobs while still keeping logs to a
|
|
17
|
+
# reasonable size.
|
|
18
|
+
#
|
|
19
|
+
# @param [Hash,Array] payload The payload to truncate
|
|
20
|
+
# @param [Integer] string_limit The maximum size for strings. Set to -1 to disable.
|
|
21
|
+
# @param [Integer] array_limit The maximum length for arrays. Set to -1 to disable.
|
|
22
|
+
# @param [Hash] max_depth The maximum recursive depth. Set to -1 to disable.
|
|
23
|
+
#
|
|
24
|
+
# @return [Hash,Array] The truncated payload
|
|
25
|
+
#
|
|
26
|
+
def truncate(payload, **kwargs)
|
|
27
|
+
depth = kwargs[:depth].to_i
|
|
28
|
+
max_depth = kwargs[:max_depth] || 3
|
|
29
|
+
string_limit = kwargs[:string_limit] || 64
|
|
30
|
+
array_limit = kwargs[:array_limit] || 10
|
|
31
|
+
|
|
32
|
+
case payload
|
|
33
|
+
when Array
|
|
34
|
+
if max_depth > -1 && depth > max_depth
|
|
35
|
+
["...#{payload.size} items..."]
|
|
36
|
+
elsif array_limit > -1
|
|
37
|
+
payload.take(array_limit).map { |e| truncate(e, **kwargs, depth: depth + 1) } +
|
|
38
|
+
(payload.size > array_limit ? ["...#{payload.size - array_limit} items..."] : [])
|
|
39
|
+
else
|
|
40
|
+
payload.map { |e| truncate(e, **kwargs, depth: depth + 1) }
|
|
41
|
+
end
|
|
42
|
+
when Hash
|
|
43
|
+
if max_depth > -1 && depth > max_depth
|
|
44
|
+
'{hash}'
|
|
45
|
+
else
|
|
46
|
+
payload.transform_values { |e| truncate(e, **kwargs, depth: depth + 1) }
|
|
47
|
+
end
|
|
48
|
+
when String
|
|
49
|
+
if string_limit > -1 && payload.size > string_limit
|
|
50
|
+
payload.truncate(string_limit)
|
|
51
|
+
else
|
|
52
|
+
payload
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
payload
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Only log the job meta information by default (exclude arguments)
|
|
62
|
+
DEFAULT_CONTEXT_PROCESSOR = ->(worker) { worker.to_h.slice(:worker, :job_id, :job_meta, :job_queue, :task_id) }
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# Build a new instance of the class.
|
|
66
|
+
#
|
|
67
|
+
# @param [Cloudtasker::Worker] worker The worker.
|
|
68
|
+
#
|
|
69
|
+
def initialize(worker)
|
|
70
|
+
@worker = worker
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
#
|
|
74
|
+
# Return the Proc responsible for formatting the log payload.
|
|
75
|
+
#
|
|
76
|
+
# @return [Proc] The context processor.
|
|
77
|
+
#
|
|
78
|
+
def context_processor
|
|
79
|
+
@context_processor ||= worker.class.cloudtasker_options_hash[:log_context_processor] ||
|
|
80
|
+
self.class.log_context_processor ||
|
|
81
|
+
DEFAULT_CONTEXT_PROCESSOR
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#
|
|
85
|
+
# The block to pass to log messages.
|
|
86
|
+
#
|
|
87
|
+
# @return [Proc] The log block.
|
|
88
|
+
#
|
|
89
|
+
def log_block
|
|
90
|
+
@log_block ||= proc { context_processor.call(worker) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#
|
|
94
|
+
# Return the Cloudtasker logger.
|
|
95
|
+
#
|
|
96
|
+
# @return [Logger, any] The cloudtasker logger.
|
|
97
|
+
#
|
|
98
|
+
def logger
|
|
99
|
+
Cloudtasker.logger
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
#
|
|
103
|
+
# Format the log message as string.
|
|
104
|
+
#
|
|
105
|
+
# @param [Object] msg The log message or object.
|
|
106
|
+
#
|
|
107
|
+
# @return [String] The formatted message
|
|
108
|
+
#
|
|
109
|
+
def formatted_message_as_string(msg)
|
|
110
|
+
# Format message
|
|
111
|
+
msg_content = if msg.is_a?(Exception)
|
|
112
|
+
[msg.inspect, msg.backtrace].flatten(1).join("\n")
|
|
113
|
+
elsif msg.is_a?(String)
|
|
114
|
+
msg
|
|
115
|
+
else
|
|
116
|
+
msg.inspect
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
"[Cloudtasker][#{worker.class}][#{worker.job_id}] #{msg_content}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
#
|
|
123
|
+
# Format main log message.
|
|
124
|
+
#
|
|
125
|
+
# @param [String] msg The message to log.
|
|
126
|
+
#
|
|
127
|
+
# @return [String] The formatted log message
|
|
128
|
+
#
|
|
129
|
+
def formatted_message(msg)
|
|
130
|
+
if msg.is_a?(String)
|
|
131
|
+
formatted_message_as_string(msg)
|
|
132
|
+
else
|
|
133
|
+
# Delegate object formatting to logger
|
|
134
|
+
msg
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
#
|
|
139
|
+
# Log an info message.
|
|
140
|
+
#
|
|
141
|
+
# @param [String] msg The message to log.
|
|
142
|
+
# @param [Proc] &block Optional context block.
|
|
143
|
+
#
|
|
144
|
+
def info(msg, &block)
|
|
145
|
+
log_message(:info, msg, &block)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#
|
|
149
|
+
# Log an error message.
|
|
150
|
+
#
|
|
151
|
+
# @param [String] msg The message to log.
|
|
152
|
+
# @param [Proc] &block Optional context block.
|
|
153
|
+
#
|
|
154
|
+
def error(msg, &block)
|
|
155
|
+
log_message(:error, msg, &block)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
#
|
|
159
|
+
# Log an fatal message.
|
|
160
|
+
#
|
|
161
|
+
# @param [String] msg The message to log.
|
|
162
|
+
# @param [Proc] &block Optional context block.
|
|
163
|
+
#
|
|
164
|
+
def fatal(msg, &block)
|
|
165
|
+
log_message(:fatal, msg, &block)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
#
|
|
169
|
+
# Log an debut message.
|
|
170
|
+
#
|
|
171
|
+
# @param [String] msg The message to log.
|
|
172
|
+
# @param [Proc] &block Optional context block.
|
|
173
|
+
#
|
|
174
|
+
def debug(msg, &block)
|
|
175
|
+
log_message(:debug, msg, &block)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
#
|
|
179
|
+
# Delegate all methods to the underlying logger.
|
|
180
|
+
#
|
|
181
|
+
# @param [String, Symbol] name The method to delegate.
|
|
182
|
+
# @param [Array<any>] *args The list of method arguments.
|
|
183
|
+
# @param [Proc] &block Block passed to the method.
|
|
184
|
+
#
|
|
185
|
+
# @return [Any] The method return value
|
|
186
|
+
#
|
|
187
|
+
def method_missing(name, ...)
|
|
188
|
+
if logger.respond_to?(name)
|
|
189
|
+
logger.send(name, ...)
|
|
190
|
+
else
|
|
191
|
+
super
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
#
|
|
196
|
+
# Check if the class respond to a certain method.
|
|
197
|
+
#
|
|
198
|
+
# @param [String, Symbol] name The name of the method.
|
|
199
|
+
# @param [Boolean] include_private Whether to check private methods or not. Default to false.
|
|
200
|
+
#
|
|
201
|
+
# @return [Boolean] Return true if the class respond to this method.
|
|
202
|
+
#
|
|
203
|
+
def respond_to_missing?(name, include_private = false)
|
|
204
|
+
logger.respond_to?(name) || super
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
#
|
|
210
|
+
# Log a message for the provided log level.
|
|
211
|
+
#
|
|
212
|
+
# @param [String, Symbol] level The log level
|
|
213
|
+
# @param [String] msg The message to log.
|
|
214
|
+
# @param [Proc] &block Optional context block.
|
|
215
|
+
#
|
|
216
|
+
def log_message(level, msg, &block)
|
|
217
|
+
# Merge log-specific context into worker-specific context
|
|
218
|
+
payload_block = ->(*_args) { log_block.call.merge(block&.call || {}) }
|
|
219
|
+
|
|
220
|
+
# ActiveSupport::Logger does not support passing a payload through a block on top
|
|
221
|
+
# of a message.
|
|
222
|
+
if defined?(ActiveSupport::Logger) && logger.is_a?(ActiveSupport::Logger)
|
|
223
|
+
# The logger is fairly basic in terms of formatting. All inputs get converted
|
|
224
|
+
# as regular strings.
|
|
225
|
+
logger.send(level) { "#{formatted_message_as_string(msg)} -- #{payload_block.call}" }
|
|
226
|
+
else
|
|
227
|
+
logger.send(level, formatted_message(msg), &payload_block)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cloudtasker/worker'
|
|
4
|
+
|
|
5
|
+
module Cloudtasker
|
|
6
|
+
# A worker class used to schedule jobs without actually
|
|
7
|
+
# instantiating the worker class. This is useful for middlewares
|
|
8
|
+
# needing to enqueue jobs in a Rails initializer. Rails 6 complains
|
|
9
|
+
# about instantiating workers in an iniitializer because of autoloading
|
|
10
|
+
# in zeitwerk mode.
|
|
11
|
+
#
|
|
12
|
+
# Downside of this wrapper: any cloudtasker_options specified on on the
|
|
13
|
+
# worker_class will be ignored.
|
|
14
|
+
#
|
|
15
|
+
# See: https://github.com/rails/rails/issues/36363
|
|
16
|
+
#
|
|
17
|
+
class WorkerWrapper
|
|
18
|
+
include Worker
|
|
19
|
+
|
|
20
|
+
attr_accessor :worker_name
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Build a new instance of the class.
|
|
24
|
+
#
|
|
25
|
+
# @param [String] worker_class The name of the worker class.
|
|
26
|
+
# @param [Hash] **opts The worker arguments.
|
|
27
|
+
#
|
|
28
|
+
def initialize(worker_name:, **opts)
|
|
29
|
+
@worker_name = worker_name
|
|
30
|
+
super(**opts)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#
|
|
34
|
+
# Override parent. Return the underlying worker class name.
|
|
35
|
+
#
|
|
36
|
+
# @return [String] The worker class.
|
|
37
|
+
#
|
|
38
|
+
def job_class_name
|
|
39
|
+
worker_name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#
|
|
43
|
+
# Return a new instance of the worker with the same args and metadata
|
|
44
|
+
# but with a different id.
|
|
45
|
+
#
|
|
46
|
+
# @return [Cloudtasker::WorkerWrapper] <description>
|
|
47
|
+
#
|
|
48
|
+
def new_instance
|
|
49
|
+
self.class.new(worker_name: worker_name, job_queue: job_queue, job_args: job_args, job_meta: job_meta)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/cloudtasker.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/object/blank'
|
|
4
|
+
require 'active_support/core_ext/object/try'
|
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
|
6
|
+
require 'active_support/core_ext/string/filters'
|
|
7
|
+
require 'active_support/security_utils'
|
|
8
|
+
|
|
9
|
+
require 'cloudtasker/version'
|
|
10
|
+
require 'cloudtasker/config'
|
|
11
|
+
|
|
12
|
+
require 'cloudtasker/authentication_error'
|
|
13
|
+
require 'cloudtasker/dead_worker_error'
|
|
14
|
+
require 'cloudtasker/retry_worker_error'
|
|
15
|
+
require 'cloudtasker/invalid_worker_error'
|
|
16
|
+
require 'cloudtasker/missing_worker_arguments_error'
|
|
17
|
+
require 'cloudtasker/max_task_size_exceeded_error'
|
|
18
|
+
|
|
19
|
+
require 'cloudtasker/middleware/chain'
|
|
20
|
+
require 'cloudtasker/authenticator'
|
|
21
|
+
require 'cloudtasker/cloud_task'
|
|
22
|
+
require 'cloudtasker/worker_logger'
|
|
23
|
+
require 'cloudtasker/worker_handler'
|
|
24
|
+
require 'cloudtasker/meta_store'
|
|
25
|
+
require 'cloudtasker/worker'
|
|
26
|
+
|
|
27
|
+
# Define and manage Cloud Task based workers
|
|
28
|
+
module Cloudtasker
|
|
29
|
+
attr_writer :config
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# Cloudtasker configurator.
|
|
33
|
+
#
|
|
34
|
+
def self.configure
|
|
35
|
+
yield(config)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Return the Cloudtasker configuration.
|
|
40
|
+
#
|
|
41
|
+
# @return [Cloudtasker::Config] The Cloudtasker configuration.
|
|
42
|
+
#
|
|
43
|
+
def self.config
|
|
44
|
+
@config ||= Config.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#
|
|
48
|
+
# Return the Cloudtasker logger.
|
|
49
|
+
#
|
|
50
|
+
# @return [Logger] The Cloudtasker logger.
|
|
51
|
+
#
|
|
52
|
+
def self.logger
|
|
53
|
+
config.logger
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
require 'cloudtasker/engine' if defined?(Rails::Engine)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cloudtasker/config'
|
|
4
|
+
require 'cloudtasker/cloud_task'
|
|
5
|
+
|
|
6
|
+
ENV['GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS'] ||= 'true'
|
|
7
|
+
|
|
8
|
+
namespace :cloudtasker do
|
|
9
|
+
desc 'Setup a Cloud Task queue. (default options: ' \
|
|
10
|
+
"name=#{Cloudtasker::Config::DEFAULT_JOB_QUEUE}, " \
|
|
11
|
+
"concurrency=#{Cloudtasker::Config::DEFAULT_QUEUE_CONCURRENCY}, " \
|
|
12
|
+
"retries=#{Cloudtasker::Config::DEFAULT_QUEUE_RETRIES})"
|
|
13
|
+
task setup_queue: :environment do
|
|
14
|
+
puts Cloudtasker::CloudTask.setup_production_queue(
|
|
15
|
+
name: ENV.fetch('name', nil),
|
|
16
|
+
concurrency: ENV.fetch('concurrency', nil),
|
|
17
|
+
retries: ENV.fetch('retries', nil)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|