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,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module Cloudtasker
|
|
6
|
+
# Manage token generation and verification
|
|
7
|
+
module Authenticator
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Algorithm used to sign the verification token
|
|
11
|
+
JWT_ALG = 'HS256'
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# Return the cloudtasker configuration. See Cloudtasker#configure.
|
|
15
|
+
#
|
|
16
|
+
# @return [Cloudtasker::Config] The library configuration.
|
|
17
|
+
#
|
|
18
|
+
def config
|
|
19
|
+
Cloudtasker.config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# A Json Web Token (JWT) which will be used by the processor
|
|
24
|
+
# to authenticate the job.
|
|
25
|
+
#
|
|
26
|
+
# @return [String] The jwt token
|
|
27
|
+
#
|
|
28
|
+
def verification_token
|
|
29
|
+
JWT.encode({ iat: Time.now.to_i }, config.secret, JWT_ALG)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# The Authorization header content
|
|
34
|
+
#
|
|
35
|
+
# @return [String] The Bearer authorization header
|
|
36
|
+
#
|
|
37
|
+
def bearer_token
|
|
38
|
+
"Bearer #{verification_token}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# Verify a bearer token (jwt token)
|
|
43
|
+
#
|
|
44
|
+
# @param [String] bearer_token The token to verify.
|
|
45
|
+
#
|
|
46
|
+
# @return [Boolean] Return true if the token is valid
|
|
47
|
+
#
|
|
48
|
+
def verify(bearer_token)
|
|
49
|
+
JWT.decode(bearer_token, config.secret)
|
|
50
|
+
rescue JWT::VerificationError, JWT::DecodeError
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Verify a bearer token and raise a `Cloudtasker::AuthenticationError`
|
|
56
|
+
# if the token is invalid.
|
|
57
|
+
#
|
|
58
|
+
# @param [String] bearer_token The token to verify.
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean] Return true if the token is valid
|
|
61
|
+
#
|
|
62
|
+
def verify!(bearer_token)
|
|
63
|
+
verify(bearer_token) || raise(AuthenticationError)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#
|
|
67
|
+
# Generate a signature for a payload
|
|
68
|
+
#
|
|
69
|
+
# @param [String] payload The JSON payload
|
|
70
|
+
#
|
|
71
|
+
# @return [String] The HMAC signature
|
|
72
|
+
#
|
|
73
|
+
def sign_payload(payload)
|
|
74
|
+
OpenSSL::HMAC.hexdigest('sha256', config.secret, payload)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#
|
|
78
|
+
# Verify that a signature matches the payload and raise a `Cloudtasker::AuthenticationError`
|
|
79
|
+
# if the signature is invalid.
|
|
80
|
+
#
|
|
81
|
+
# @param [String] signature The tested signature
|
|
82
|
+
# @param [String] payload The JSON payload
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean] Return true if the signature is valid
|
|
85
|
+
#
|
|
86
|
+
def verify_signature!(signature, payload)
|
|
87
|
+
ActiveSupport::SecurityUtils.secure_compare(signature, sign_payload(payload)) || raise(AuthenticationError)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'google/cloud/tasks'
|
|
4
|
+
require 'retriable'
|
|
5
|
+
|
|
6
|
+
module Cloudtasker
|
|
7
|
+
module Backend
|
|
8
|
+
# Manage tasks pushed to GCP Cloud Task
|
|
9
|
+
class GoogleCloudTaskV1
|
|
10
|
+
attr_accessor :gcp_task
|
|
11
|
+
|
|
12
|
+
#
|
|
13
|
+
# Create the queue configured in Cloudtasker if it does not already exist.
|
|
14
|
+
#
|
|
15
|
+
# @param [String] :name The queue name
|
|
16
|
+
# @param [Integer] :concurrency The queue concurrency
|
|
17
|
+
# @param [Integer] :retries The number of retries for the queue
|
|
18
|
+
#
|
|
19
|
+
# @return [Google::Cloud::Tasks::V2beta3::Queue] The queue
|
|
20
|
+
#
|
|
21
|
+
def self.setup_queue(name: nil, concurrency: nil, retries: nil)
|
|
22
|
+
# Build full queue path
|
|
23
|
+
queue_name = name || Cloudtasker::Config::DEFAULT_JOB_QUEUE
|
|
24
|
+
full_queue_name = queue_path(queue_name)
|
|
25
|
+
|
|
26
|
+
# Try to get existing queue
|
|
27
|
+
client.get_queue(full_queue_name)
|
|
28
|
+
rescue Google::Gax::RetryError
|
|
29
|
+
# Extract options
|
|
30
|
+
queue_concurrency = (concurrency || Cloudtasker::Config::DEFAULT_QUEUE_CONCURRENCY).to_i
|
|
31
|
+
queue_retries = (retries || Cloudtasker::Config::DEFAULT_QUEUE_RETRIES).to_i
|
|
32
|
+
|
|
33
|
+
# Create queue on 'not found' error
|
|
34
|
+
client.create_queue(
|
|
35
|
+
client.location_path(config.gcp_project_id, config.gcp_location_id),
|
|
36
|
+
{
|
|
37
|
+
name: full_queue_name,
|
|
38
|
+
retry_config: { max_attempts: queue_retries },
|
|
39
|
+
rate_limits: { max_concurrent_dispatches: queue_concurrency }
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#
|
|
45
|
+
# Return the Google Cloud Task client.
|
|
46
|
+
#
|
|
47
|
+
# @return [Google::Cloud::Tasks] The Google Cloud Task client.
|
|
48
|
+
#
|
|
49
|
+
def self.client
|
|
50
|
+
@client ||= ::Google::Cloud::Tasks.new(version: :v2beta3)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
# Return the cloudtasker configuration. See Cloudtasker#configure.
|
|
55
|
+
#
|
|
56
|
+
# @return [Cloudtasker::Config] The library configuration.
|
|
57
|
+
#
|
|
58
|
+
def self.config
|
|
59
|
+
Cloudtasker.config
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#
|
|
63
|
+
# Return the fully qualified path for the Cloud Task queue.
|
|
64
|
+
#
|
|
65
|
+
# @param [String] queue_name The relative name of the queue.
|
|
66
|
+
#
|
|
67
|
+
# @return [String] The queue path.
|
|
68
|
+
#
|
|
69
|
+
def self.queue_path(queue_name)
|
|
70
|
+
client.queue_path(
|
|
71
|
+
config.gcp_project_id,
|
|
72
|
+
config.gcp_location_id,
|
|
73
|
+
[config.gcp_queue_prefix, queue_name].map(&:presence).compact.join('-')
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#
|
|
78
|
+
# Return a protobuf timestamp specifying how to wait
|
|
79
|
+
# before running a task.
|
|
80
|
+
#
|
|
81
|
+
# @param [Integer, nil] schedule_time A unix timestamp.
|
|
82
|
+
#
|
|
83
|
+
# @return [Google::Protobuf::Timestamp, nil] The protobuff timestamp
|
|
84
|
+
#
|
|
85
|
+
def self.format_protobuf_time(schedule_time)
|
|
86
|
+
return nil unless schedule_time
|
|
87
|
+
|
|
88
|
+
# Generate protobuf timestamp
|
|
89
|
+
Google::Protobuf::Timestamp.new.tap { |e| e.seconds = schedule_time.to_i }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
#
|
|
93
|
+
# Return a protobuf duration.
|
|
94
|
+
#
|
|
95
|
+
# @param [Integer, nil] duration A duration in seconds.
|
|
96
|
+
#
|
|
97
|
+
# @return [Google::Protobuf::Timestamp, nil] The protobuff timestamp
|
|
98
|
+
#
|
|
99
|
+
def self.format_protobuf_duration(duration)
|
|
100
|
+
return nil unless duration
|
|
101
|
+
|
|
102
|
+
# Generate protobuf timestamp
|
|
103
|
+
Google::Protobuf::Duration.new.tap { |e| e.seconds = duration.to_i }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
#
|
|
107
|
+
# Format the job payload sent to Cloud Tasks.
|
|
108
|
+
#
|
|
109
|
+
# @param [Hash] hash The worker payload.
|
|
110
|
+
#
|
|
111
|
+
# @return [Hash] The Cloud Task payloadd.
|
|
112
|
+
#
|
|
113
|
+
def self.format_task_payload(payload)
|
|
114
|
+
payload = JSON.parse(payload.to_json, symbolize_names: true) # deep dup
|
|
115
|
+
|
|
116
|
+
# Format schedule time to Google::Protobuf::Timestamp
|
|
117
|
+
payload[:schedule_time] = format_protobuf_time(payload[:schedule_time])
|
|
118
|
+
|
|
119
|
+
# Format dispatch_deadline to Google::Protobuf::Duration
|
|
120
|
+
payload[:dispatch_deadline] = format_protobuf_duration(payload[:dispatch_deadline])
|
|
121
|
+
|
|
122
|
+
# Setup headers
|
|
123
|
+
payload[:http_request][:headers] ||= {}
|
|
124
|
+
payload[:http_request][:headers][Cloudtasker::Config::CONTENT_TYPE_HEADER] = 'text/json'
|
|
125
|
+
|
|
126
|
+
# Conditionally encode job content to support UTF-8.
|
|
127
|
+
# Google Cloud Task expect content to be ASCII-8BIT compatible (binary)
|
|
128
|
+
if config.base64_encode_body
|
|
129
|
+
payload[:http_request][:headers][Cloudtasker::Config::ENCODING_HEADER] = 'Base64'
|
|
130
|
+
payload[:http_request][:body] = Base64.encode64(payload[:http_request][:body])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
payload.compact
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
#
|
|
137
|
+
# Find a task by id.
|
|
138
|
+
#
|
|
139
|
+
# @param [String] id The task id.
|
|
140
|
+
#
|
|
141
|
+
# @return [Cloudtasker::Backend::GoogleCloudTaskV1, nil] The retrieved task.
|
|
142
|
+
#
|
|
143
|
+
def self.find(id)
|
|
144
|
+
resp = with_gax_retries { client.get_task(id) }
|
|
145
|
+
resp ? new(resp) : nil
|
|
146
|
+
rescue Google::Gax::RetryError, Google::Gax::NotFoundError, GRPC::NotFound
|
|
147
|
+
# The ID does not exist
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
#
|
|
152
|
+
# Create a new task.
|
|
153
|
+
#
|
|
154
|
+
# @param [Hash] payload The task payload.
|
|
155
|
+
#
|
|
156
|
+
# @return [Cloudtasker::Backend::GoogleCloudTaskV1, nil] The created task.
|
|
157
|
+
#
|
|
158
|
+
def self.create(payload)
|
|
159
|
+
payload = format_task_payload(payload)
|
|
160
|
+
|
|
161
|
+
# Extract relative queue name
|
|
162
|
+
relative_queue = payload.delete(:queue)
|
|
163
|
+
|
|
164
|
+
# Create task
|
|
165
|
+
resp = with_gax_retries { client.create_task(queue_path(relative_queue), payload) }
|
|
166
|
+
resp ? new(resp) : nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
#
|
|
170
|
+
# Delete a task by id.
|
|
171
|
+
#
|
|
172
|
+
# @param [String] id The id of the task.
|
|
173
|
+
#
|
|
174
|
+
def self.delete(id)
|
|
175
|
+
with_gax_retries { client.delete_task(id) }
|
|
176
|
+
rescue Google::Gax::RetryError, Google::Gax::NotFoundError, GRPC::NotFound, Google::Gax::PermissionDeniedError
|
|
177
|
+
# The ID does not exist
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
#
|
|
182
|
+
# Helper method encapsulating the retry strategy for GAX calls
|
|
183
|
+
#
|
|
184
|
+
def self.with_gax_retries(&block)
|
|
185
|
+
Retriable.retriable(on: [Google::Gax::UnavailableError], tries: 3, &block)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
#
|
|
189
|
+
# Build a new instance of the class.
|
|
190
|
+
#
|
|
191
|
+
# @param [Google::Cloud::Tasks::V2beta3::Task] resp The GCP Cloud Task response
|
|
192
|
+
#
|
|
193
|
+
def initialize(gcp_task)
|
|
194
|
+
@gcp_task = gcp_task
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
#
|
|
198
|
+
# Return the relative queue (queue name minus prefix) the task is in.
|
|
199
|
+
#
|
|
200
|
+
# @return [String] The relative queue name
|
|
201
|
+
#
|
|
202
|
+
def relative_queue
|
|
203
|
+
gcp_task
|
|
204
|
+
.name
|
|
205
|
+
.match(%r{/queues/([^/]+)})
|
|
206
|
+
&.captures
|
|
207
|
+
&.first
|
|
208
|
+
&.sub(/^#{self.class.config.gcp_queue_prefix}-/, '')
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
#
|
|
212
|
+
# Return a hash description of the task.
|
|
213
|
+
#
|
|
214
|
+
# @return [Hash] A hash description of the task.
|
|
215
|
+
#
|
|
216
|
+
def to_h
|
|
217
|
+
{
|
|
218
|
+
id: gcp_task.name,
|
|
219
|
+
http_request: gcp_task.to_h[:http_request],
|
|
220
|
+
schedule_time: gcp_task.to_h.dig(:schedule_time, :seconds).to_i,
|
|
221
|
+
dispatch_deadline: gcp_task.to_h.dig(:dispatch_deadline, :seconds).to_i,
|
|
222
|
+
retries: gcp_task.to_h[:response_count],
|
|
223
|
+
queue: relative_queue
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'google/cloud/tasks'
|
|
4
|
+
require 'google/protobuf/duration_pb'
|
|
5
|
+
require 'google/protobuf/timestamp_pb'
|
|
6
|
+
require 'retriable'
|
|
7
|
+
|
|
8
|
+
module Cloudtasker
|
|
9
|
+
module Backend
|
|
10
|
+
# Manage tasks pushed to GCP Cloud Task
|
|
11
|
+
class GoogleCloudTaskV2
|
|
12
|
+
attr_accessor :gcp_task
|
|
13
|
+
|
|
14
|
+
#
|
|
15
|
+
# Create the queue configured in Cloudtasker if it does not already exist.
|
|
16
|
+
#
|
|
17
|
+
# @param [String] :name The queue name
|
|
18
|
+
# @param [Integer] :concurrency The queue concurrency
|
|
19
|
+
# @param [Integer] :retries The number of retries for the queue
|
|
20
|
+
#
|
|
21
|
+
# @return [Google::Cloud::Tasks::V2::Queue] The queue
|
|
22
|
+
#
|
|
23
|
+
def self.setup_queue(name: nil, concurrency: nil, retries: nil)
|
|
24
|
+
# Build full queue path
|
|
25
|
+
queue_name = name || Cloudtasker::Config::DEFAULT_JOB_QUEUE
|
|
26
|
+
full_queue_name = queue_path(queue_name)
|
|
27
|
+
|
|
28
|
+
# Try to get existing queue
|
|
29
|
+
client.get_queue(name: full_queue_name)
|
|
30
|
+
rescue Google::Cloud::NotFoundError
|
|
31
|
+
# Extract options
|
|
32
|
+
queue_concurrency = (concurrency || Cloudtasker::Config::DEFAULT_QUEUE_CONCURRENCY).to_i
|
|
33
|
+
queue_retries = (retries || Cloudtasker::Config::DEFAULT_QUEUE_RETRIES).to_i
|
|
34
|
+
|
|
35
|
+
# Create queue on 'not found' error
|
|
36
|
+
client.create_queue(
|
|
37
|
+
parent: client.location_path(project: config.gcp_project_id, location: config.gcp_location_id),
|
|
38
|
+
queue: {
|
|
39
|
+
name: full_queue_name,
|
|
40
|
+
retry_config: { max_attempts: queue_retries },
|
|
41
|
+
rate_limits: { max_concurrent_dispatches: queue_concurrency }
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#
|
|
47
|
+
# Return the Google Cloud Task client.
|
|
48
|
+
#
|
|
49
|
+
# @return [Google::Cloud::Tasks::V2::CloudTasks::Client] The Google Cloud Task client.
|
|
50
|
+
#
|
|
51
|
+
def self.client
|
|
52
|
+
@client ||= ::Google::Cloud::Tasks.cloud_tasks
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# Return the cloudtasker configuration. See Cloudtasker#configure.
|
|
57
|
+
#
|
|
58
|
+
# @return [Cloudtasker::Config] The library configuration.
|
|
59
|
+
#
|
|
60
|
+
def self.config
|
|
61
|
+
Cloudtasker.config
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# Return the fully qualified path for the Cloud Task queue.
|
|
66
|
+
#
|
|
67
|
+
# @param [String] queue_name The relative name of the queue.
|
|
68
|
+
#
|
|
69
|
+
# @return [String] The queue path.
|
|
70
|
+
#
|
|
71
|
+
def self.queue_path(queue_name)
|
|
72
|
+
client.queue_path(
|
|
73
|
+
project: config.gcp_project_id,
|
|
74
|
+
location: config.gcp_location_id,
|
|
75
|
+
queue: [config.gcp_queue_prefix, queue_name].map(&:presence).compact.join('-')
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# Return a protobuf timestamp specifying how to wait
|
|
81
|
+
# before running a task.
|
|
82
|
+
#
|
|
83
|
+
# @param [Integer, nil] schedule_time A unix timestamp.
|
|
84
|
+
#
|
|
85
|
+
# @return [Google::Protobuf::Timestamp, nil] The protobuff timestamp
|
|
86
|
+
#
|
|
87
|
+
def self.format_protobuf_time(schedule_time)
|
|
88
|
+
return nil unless schedule_time
|
|
89
|
+
|
|
90
|
+
# Generate protobuf timestamp
|
|
91
|
+
Google::Protobuf::Timestamp.new.tap { |e| e.seconds = schedule_time.to_i }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#
|
|
95
|
+
# Return a protobuf duration.
|
|
96
|
+
#
|
|
97
|
+
# @param [Integer, nil] duration A duration in seconds.
|
|
98
|
+
#
|
|
99
|
+
# @return [Google::Protobuf::Timestamp, nil] The protobuff timestamp
|
|
100
|
+
#
|
|
101
|
+
def self.format_protobuf_duration(duration)
|
|
102
|
+
return nil unless duration
|
|
103
|
+
|
|
104
|
+
# Generate protobuf timestamp
|
|
105
|
+
Google::Protobuf::Duration.new.tap { |e| e.seconds = duration.to_i }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#
|
|
109
|
+
# Format the job payload sent to Cloud Tasks.
|
|
110
|
+
#
|
|
111
|
+
# @param [Hash] hash The worker payload.
|
|
112
|
+
#
|
|
113
|
+
# @return [Hash] The Cloud Task payload.
|
|
114
|
+
#
|
|
115
|
+
def self.format_task_payload(payload)
|
|
116
|
+
payload = JSON.parse(payload.to_json, symbolize_names: true) # deep dup
|
|
117
|
+
|
|
118
|
+
# Format schedule time to Google::Protobuf::Timestamp
|
|
119
|
+
payload[:schedule_time] = format_protobuf_time(payload[:schedule_time])
|
|
120
|
+
|
|
121
|
+
# Format dispatch_deadline to Google::Protobuf::Duration
|
|
122
|
+
payload[:dispatch_deadline] = format_protobuf_duration(payload[:dispatch_deadline])
|
|
123
|
+
|
|
124
|
+
# Setup headers
|
|
125
|
+
payload[:http_request][:headers] ||= {}
|
|
126
|
+
payload[:http_request][:headers][Cloudtasker::Config::CONTENT_TYPE_HEADER] = 'text/json'
|
|
127
|
+
|
|
128
|
+
# Conditionally encode job content to support UTF-8.
|
|
129
|
+
# Google Cloud Task expect content to be ASCII-8BIT compatible (binary)
|
|
130
|
+
if config.base64_encode_body
|
|
131
|
+
payload[:http_request][:headers][Cloudtasker::Config::ENCODING_HEADER] = 'Base64'
|
|
132
|
+
payload[:http_request][:body] = Base64.encode64(payload[:http_request][:body])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
payload.compact
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
#
|
|
139
|
+
# Find a task by id.
|
|
140
|
+
#
|
|
141
|
+
# @param [String] id The task id.
|
|
142
|
+
#
|
|
143
|
+
# @return [Cloudtasker::Backend::GoogleCloudTaskV2, nil] The retrieved task.
|
|
144
|
+
#
|
|
145
|
+
def self.find(id)
|
|
146
|
+
resp = with_gapi_retries { client.get_task(name: id) }
|
|
147
|
+
resp ? new(resp) : nil
|
|
148
|
+
rescue Google::Cloud::NotFoundError
|
|
149
|
+
# The ID does not exist
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
#
|
|
154
|
+
# Create a new task.
|
|
155
|
+
#
|
|
156
|
+
# @param [Hash] payload The task payload.
|
|
157
|
+
#
|
|
158
|
+
# @return [Cloudtasker::Backend::GoogleCloudTaskV2, nil] The created task.
|
|
159
|
+
#
|
|
160
|
+
def self.create(payload)
|
|
161
|
+
payload = format_task_payload(payload)
|
|
162
|
+
|
|
163
|
+
# Infer full queue name
|
|
164
|
+
relative_queue = payload.delete(:queue)
|
|
165
|
+
full_queue = queue_path(relative_queue)
|
|
166
|
+
|
|
167
|
+
# Create task
|
|
168
|
+
resp = with_gapi_retries { client.create_task(parent: full_queue, task: payload) }
|
|
169
|
+
resp ? new(resp) : nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
#
|
|
173
|
+
# Delete a task by id.
|
|
174
|
+
#
|
|
175
|
+
# @param [String] id The id of the task.
|
|
176
|
+
#
|
|
177
|
+
def self.delete(id)
|
|
178
|
+
with_gapi_retries { client.delete_task(name: id) }
|
|
179
|
+
rescue Google::Cloud::NotFoundError, Google::Cloud::PermissionDeniedError
|
|
180
|
+
# The ID does not exist
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
#
|
|
185
|
+
# Helper method encapsulating the retry strategy for Google API calls
|
|
186
|
+
#
|
|
187
|
+
def self.with_gapi_retries(&block)
|
|
188
|
+
Retriable.retriable(on: [Google::Cloud::UnavailableError], tries: 3, &block)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
#
|
|
192
|
+
# Build a new instance of the class.
|
|
193
|
+
#
|
|
194
|
+
# @param [Google::Cloud::Tasks::V2::Task] resp The GCP Cloud Task response
|
|
195
|
+
#
|
|
196
|
+
def initialize(gcp_task)
|
|
197
|
+
@gcp_task = gcp_task
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
#
|
|
201
|
+
# Return the relative queue (queue name minus prefix) the task is in.
|
|
202
|
+
#
|
|
203
|
+
# @return [String] The relative queue name
|
|
204
|
+
#
|
|
205
|
+
def relative_queue
|
|
206
|
+
gcp_task
|
|
207
|
+
.name
|
|
208
|
+
.match(%r{/queues/([^/]+)})
|
|
209
|
+
&.captures
|
|
210
|
+
&.first
|
|
211
|
+
&.sub(/^#{self.class.config.gcp_queue_prefix}-/, '')
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
#
|
|
215
|
+
# Return a hash description of the task.
|
|
216
|
+
#
|
|
217
|
+
# @return [Hash] A hash description of the task.
|
|
218
|
+
#
|
|
219
|
+
def to_h
|
|
220
|
+
{
|
|
221
|
+
id: gcp_task.name,
|
|
222
|
+
http_request: gcp_task.to_h[:http_request],
|
|
223
|
+
schedule_time: gcp_task.to_h.dig(:schedule_time, :seconds).to_i,
|
|
224
|
+
dispatch_deadline: gcp_task.to_h.dig(:dispatch_deadline, :seconds).to_i,
|
|
225
|
+
retries: gcp_task.to_h[:response_count],
|
|
226
|
+
queue: relative_queue
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|