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,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