cloudtasker 0.12.rc4 → 0.12.rc9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21f0b0582c4e3b2f54cae9a7086da5a4c62f238d3f4fdaf54d78bccd7e27eff2
4
- data.tar.gz: 6811ae33e4d082fafa62bb0f8021bb0cf6f36e63aa50d1683f409c3176f8a10f
3
+ metadata.gz: 50cd6021ad7511d7b5b385a6086eb315d9381a4f43909a6e1e9a0defbc679ea4
4
+ data.tar.gz: dc16cc1330b14b46a5d6ade9f9c171981fd3aef5b3db67dd474e29548acc177f
5
5
  SHA512:
6
- metadata.gz: e155f136b3da0480e0644d883f275a013bd4694517d880c18a68527b16940362ee07caa1d3f8ebd303de6078fee565cf7088b425a389f0be53ee847ead91e678
7
- data.tar.gz: f682af3f433e48739b0a631e593d285fe5854e789e08f93bbd2670eb08474f6d5a3dd12019f428ff596ef3c5a67b384b36d22dcd786d2c20f827669b92454b2b
6
+ metadata.gz: 30feac9331b9e8113e71af6c44a20d2f47fcbd41652a0d345ab8808d272bd14271ebb4a98711e42b238f6202fa8023a37c543e0f4ec7d9f9f7a46cc2732d96aa
7
+ data.tar.gz: 9b70a3c0733b2c510fafe48232c65bb79f5ad4da6b90fbcff86c0736fee590fab5f89846df9c41855dc7084460425660ee42799975975775b2239b9b8997171a
data/.rubocop.yml CHANGED
@@ -6,13 +6,13 @@ AllCops:
6
6
  - 'vendor/**/*'
7
7
 
8
8
  Metrics/ClassLength:
9
- Max: 150
9
+ Max: 200
10
10
 
11
11
  Metrics/ModuleLength:
12
12
  Max: 150
13
13
 
14
14
  Metrics/AbcSize:
15
- Max: 20
15
+ Max: 25
16
16
  Exclude:
17
17
  - 'spec/support/*'
18
18
 
data/CHANGELOG.md CHANGED
@@ -1,15 +1,22 @@
1
1
  # Changelog
2
2
 
3
- ## Latest RC [v0.12.rc4](https://github.com/keypup-io/cloudtasker/tree/v0.12.rc4) (2021-03-29)
3
+ ## Latest RC [v0.12.rc8](https://github.com/keypup-io/cloudtasker/tree/v0.12.rc8) (2021-04-06)
4
4
 
5
- [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.11.0...v0.12.rc4)
5
+ [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.11.0...v0.12.rc8)
6
6
 
7
7
  **Improvements:**
8
8
  - ActiveJob: do not double log errors (ActiveJob has its own error logging)
9
+ - Batch callbacks: Retry jobs when completion callback fails
10
+ - Batch state: use native Redis hashes to store batch state instead of a serialized hash in a string key
11
+ - Batch progress: restrict calculation to direct children by default. Allow depth to be specified. Calculating progress using all tree jobs created significant delays on large batches.
12
+ - Batch redis usage: cleanup batches as they get completed or become dead to avoid excessive redis usage with large batches.
13
+ - Configuration: allow configuration of Cloud Tasks `dispatch deadline` at global and worker level
14
+ - Cron jobs: Use Redis Sets instead of key pattern matching for resource listing
9
15
  - Error logging: Use worker logger so as to include context (job args etc.)
10
16
  - Error logging: Do not log exception and stack trace separately, combine them instead.
11
- - Batch callbacks: Retry jobs when completion callback fails
12
- - Redis: Use Redis Sets instead of key pattern matching for listing methods (Cron jobs and Local Server)
17
+ - Local server: Use Redis Sets instead of key pattern matching for resource listing
18
+ - Worker: raise DeadWorkerError instead of MissingWorkerArgumentsError when arguments are missing. This is more consistent with what middlewares expect.
19
+ - Worker redis usage: delete redis payload storage once the job is successful or dead instead of expiring the key.
13
20
 
14
21
  **Fixed bugs:**
15
22
  - Retries: Enforce job retry limit on job processing. There was an edge case where jobs could be retried indefinitely on batch callback errors.
data/README.md CHANGED
@@ -37,6 +37,7 @@ A local processing server is also available for development. This local server p
37
37
  1. [HTTP Error codes](#http-error-codes)
38
38
  2. [Error callbacks](#error-callbacks)
39
39
  3. [Max retries](#max-retries)
40
+ 4. [Dispatch deadline](#dispatch-deadline)
40
41
  10. [Testing](#testing)
41
42
  1. [Test helper setup](#test-helper-setup)
42
43
  2. [In-memory queues](#in-memory-queues)
@@ -351,6 +352,23 @@ Cloudtasker.configure do |config|
351
352
  #
352
353
  # Store all job payloads in Redis exceeding 50 KB:
353
354
  # config.store_payloads_in_redis = 50
355
+
356
+ #
357
+ # Specify the dispatch deadline for jobs in Cloud Tasks, in seconds.
358
+ # Jobs taking longer will be retried by Cloud Tasks, even if they eventually
359
+ # complete on the server side.
360
+ #
361
+ # Note that this option is applied when jobs are enqueued job. Changing this value
362
+ # will not impact already enqueued jobs.
363
+ #
364
+ # This option can also be configured on a per worker basis via
365
+ # the cloudtasker_options directive.
366
+ #
367
+ # Supported since: v0.12.rc8
368
+ #
369
+ # Default: 600 seconds (10 minutes)
370
+ #
371
+ # config.dispatch_deadline = 600
354
372
  end
355
373
  ```
356
374
 
@@ -721,6 +739,48 @@ class SomeErrorWorker
721
739
  end
722
740
  ```
723
741
 
742
+ ### Dispatch deadline
743
+ **Supported since**: `0.12.rc8`
744
+
745
+ By default Cloud Tasks will automatically timeout your jobs after 10 minutes, independently of your server HTTP timeout configuration.
746
+
747
+ You can modify the dispatch deadline for jobs at a global level or on a per job basis.
748
+
749
+ E.g. Set the default dispatch deadline to 20 minutes.
750
+ ```ruby
751
+ # config/initializers/cloudtasker.rb
752
+
753
+ Cloudtasker.configure do |config|
754
+ #
755
+ # Specify the dispatch deadline for jobs in Cloud Tasks, in seconds.
756
+ # Jobs taking longer will be retried by Cloud Tasks, even if they eventually
757
+ # complete on the server side.
758
+ #
759
+ # Note that this option is applied when jobs are enqueued job. Changing this value
760
+ # will not impact already enqueued jobs.
761
+ #
762
+ # Default: 600 (10 minutes)
763
+ #
764
+ config.dispatch_deadline = 20 * 60 # 20 minutes
765
+ end
766
+ ```
767
+
768
+ E.g. Set a dispatch deadline of 5 minutes on a specific worker
769
+ ```ruby
770
+ # app/workers/some_error_worker.rb
771
+
772
+ class SomeFasterWorker
773
+ include Cloudtasker::Worker
774
+
775
+ # This will override the global setting
776
+ cloudtasker_options dispatch_deadline: 5 * 60
777
+
778
+ def perform
779
+ # ... do things ...
780
+ end
781
+ end
782
+ ```
783
+
724
784
  ## Testing
725
785
  Cloudtasker provides several options to test your workers.
726
786
 
@@ -19,7 +19,7 @@ module Cloudtasker
19
19
  # Process payload
20
20
  WorkerHandler.execute_from_payload!(payload)
21
21
  head :no_content
22
- rescue DeadWorkerError, MissingWorkerArgumentsError
22
+ rescue DeadWorkerError
23
23
  # 205: job will NOT be retried
24
24
  head :reset_content
25
25
  rescue InvalidWorkerError
data/docs/BATCH_JOBS.md CHANGED
@@ -84,8 +84,29 @@ You can access progression statistics in callback using `batch.progress`. See th
84
84
  E.g.
85
85
  ```ruby
86
86
  def on_batch_node_complete(_child_job)
87
- logger.info("Total: #{batch.progress.total}")
88
- logger.info("Completed: #{batch.progress.completed}")
89
- logger.info("Progress: #{batch.progress.percent.to_i}%")
87
+ progress = batch.progress
88
+ logger.info("Total: #{progress.total}")
89
+ logger.info("Completed: #{progress.completed}")
90
+ logger.info("Progress: #{progress.percent.to_i}%")
91
+ end
92
+ ```
93
+
94
+ **Since:** `v0.12.rc5`
95
+ By default the `progress` method only considers the direct child jobs to evaluate the batch progress. You can pass `depth: somenumber` to the `progress` method to calculate the actual batch progress in a more granular way. Be careful however that this method recursively calculates progress on the sub-batches and is therefore expensive.
96
+
97
+ E.g.
98
+ ```ruby
99
+ def on_batch_node_complete(_child_job)
100
+ # Considers the children for batch progress calculation
101
+ progress_0 = batch.progress # same as batch.progress(depth: 0)
102
+
103
+ # Considers the children and grand-children for batch progress calculation
104
+ progress_1 = batch.progress(depth: 1)
105
+
106
+ # Considers the children, grand-children and grand-grand-children for batch progress calculation
107
+ progress_2 = batch.progress(depth: 3)
108
+
109
+ logger.info("Progress: #{progress_1.percent.to_i}%")
110
+ logger.info("Progress: #{progress_2.percent.to_i}%")
90
111
  end
91
112
  ```
@@ -113,7 +113,7 @@ module Cloudtasker
113
113
  # @param [Hash] http_request The HTTP request content.
114
114
  # @param [Integer] schedule_time When to run the task (Unix timestamp)
115
115
  #
116
- def initialize(id:, http_request:, schedule_time: nil, queue: nil, job_retries: 0)
116
+ def initialize(id:, http_request:, schedule_time: nil, queue: nil, job_retries: 0, **_xargs)
117
117
  @id = id
118
118
  @http_request = http_request
119
119
  @schedule_time = Time.at(schedule_time || 0)
@@ -7,7 +7,7 @@ module Cloudtasker
7
7
  module Backend
8
8
  # Manage local tasks pushed to Redis
9
9
  class RedisTask
10
- attr_reader :id, :http_request, :schedule_time, :retries, :queue
10
+ attr_reader :id, :http_request, :schedule_time, :retries, :queue, :dispatch_deadline
11
11
 
12
12
  RETRY_INTERVAL = 20 # seconds
13
13
 
@@ -123,13 +123,15 @@ module Cloudtasker
123
123
  # @param [Hash] http_request The HTTP request content.
124
124
  # @param [Integer] schedule_time When to run the task (Unix timestamp)
125
125
  # @param [Integer] retries The number of times the job failed.
126
+ # @param [Integer] dispatch_deadline The dispatch_deadline in seconds.
126
127
  #
127
- def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil)
128
+ def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil, dispatch_deadline: nil)
128
129
  @id = id
129
130
  @http_request = http_request
130
131
  @schedule_time = Time.at(schedule_time || 0)
131
132
  @retries = retries || 0
132
- @queue = queue || Cloudtasker::Config::DEFAULT_JOB_QUEUE
133
+ @queue = queue || Config::DEFAULT_JOB_QUEUE
134
+ @dispatch_deadline = dispatch_deadline || Config::DEFAULT_DISPATCH_DEADLINE
133
135
  end
134
136
 
135
137
  #
@@ -152,7 +154,8 @@ module Cloudtasker
152
154
  http_request: http_request,
153
155
  schedule_time: schedule_time.to_i,
154
156
  retries: retries,
155
- queue: queue
157
+ queue: queue,
158
+ dispatch_deadline: dispatch_deadline
156
159
  }
157
160
  end
158
161
 
@@ -176,7 +179,8 @@ module Cloudtasker
176
179
  retries: is_error ? retries + 1 : retries,
177
180
  http_request: http_request,
178
181
  schedule_time: (Time.now + interval).to_i,
179
- queue: queue
182
+ queue: queue,
183
+ dispatch_deadline: dispatch_deadline
180
184
  )
181
185
  redis.sadd(self.class.key, id)
182
186
  end
@@ -207,6 +211,13 @@ module Cloudtasker
207
211
  end
208
212
 
209
213
  resp
214
+ rescue Net::ReadTimeout
215
+ retry_later(RETRY_INTERVAL)
216
+ Cloudtasker.logger.info(
217
+ format_log_message(
218
+ "Task deadline exceeded (#{dispatch_deadline}s) - Retry in #{RETRY_INTERVAL} seconds..."
219
+ )
220
+ )
210
221
  end
211
222
 
212
223
  #
@@ -242,7 +253,7 @@ module Cloudtasker
242
253
  @http_client ||=
243
254
  begin
244
255
  uri = URI(http_request[:url])
245
- Net::HTTP.new(uri.host, uri.port).tap { |e| e.read_timeout = 60 * 10 }
256
+ Net::HTTP.new(uri.host, uri.port).tap { |e| e.read_timeout = dispatch_deadline }
246
257
  end
247
258
  end
248
259
 
@@ -17,6 +17,10 @@ module Cloudtasker
17
17
  # because the jobs will be either retried or dropped
18
18
  IGNORED_ERRORED_CALLBACKS = %i[on_child_error on_child_dead].freeze
19
19
 
20
+ # The maximum number of seconds to wait for a batch state lock
21
+ # to be acquired.
22
+ BATCH_MAX_LOCK_WAIT = 60
23
+
20
24
  #
21
25
  # Return the cloudtasker redis client
22
26
  #
@@ -176,7 +180,9 @@ module Cloudtasker
176
180
  # @return [Hash] The state of each child worker.
177
181
  #
178
182
  def batch_state
179
- redis.fetch(batch_state_gid)
183
+ migrate_batch_state_to_redis_hash
184
+
185
+ redis.hgetall(batch_state_gid)
180
186
  end
181
187
 
182
188
  #
@@ -208,6 +214,24 @@ module Cloudtasker
208
214
  )
209
215
  end
210
216
 
217
+ #
218
+ # This method migrates the batch state to be a Redis hash instead
219
+ # of a hash stored in a string key.
220
+ #
221
+ def migrate_batch_state_to_redis_hash
222
+ return unless redis.type(batch_state_gid) == 'string'
223
+
224
+ # Migrate batch state to Redis hash if it is still using a legacy string key
225
+ # We acquire a lock then check again
226
+ redis.with_lock(batch_state_gid, max_wait: BATCH_MAX_LOCK_WAIT) do
227
+ if redis.type(batch_state_gid) == 'string'
228
+ state = redis.fetch(batch_state_gid)
229
+ redis.del(batch_state_gid)
230
+ redis.hset(batch_state_gid, state) if state.any?
231
+ end
232
+ end
233
+ end
234
+
211
235
  #
212
236
  # Save the batch.
213
237
  #
@@ -218,8 +242,11 @@ module Cloudtasker
218
242
  # complete (success or failure).
219
243
  redis.write(batch_gid, worker.to_h)
220
244
 
245
+ # Stop there if no jobs to save
246
+ return if jobs.empty?
247
+
221
248
  # Save list of child workers
222
- redis.write(batch_state_gid, jobs.map { |e| [e.job_id, 'scheduled'] }.to_h)
249
+ redis.hset(batch_state_gid, jobs.map { |e| [e.job_id, 'scheduled'] }.to_h)
223
250
  end
224
251
 
225
252
  #
@@ -228,28 +255,27 @@ module Cloudtasker
228
255
  # @param [String] job_id The batch id.
229
256
  # @param [String] status The status of the sub-batch.
230
257
  #
231
- # @return [<Type>] <description>
232
- #
233
258
  def update_state(batch_id, status)
234
- redis.with_lock(batch_state_gid) do
235
- state = batch_state
236
- state[batch_id.to_sym] = status.to_s if state.key?(batch_id.to_sym)
237
- redis.write(batch_state_gid, state)
259
+ migrate_batch_state_to_redis_hash
260
+
261
+ # Update the batch state batch_id entry with the new status
262
+ redis.with_lock("#{batch_state_gid}/#{batch_id}", max_wait: BATCH_MAX_LOCK_WAIT) do
263
+ redis.hset(batch_state_gid, batch_id, status) if redis.hexists(batch_state_gid, batch_id)
238
264
  end
239
265
  end
240
266
 
241
267
  #
242
268
  # Return true if all the child workers have completed.
243
269
  #
244
- # @return [<Type>] <description>
270
+ # @return [Boolean] True if the batch is complete.
245
271
  #
246
272
  def complete?
247
- redis.with_lock(batch_state_gid) do
248
- state = redis.fetch(batch_state_gid)
249
- return true unless state
273
+ migrate_batch_state_to_redis_hash
250
274
 
275
+ # Check that all child jobs have completed
276
+ redis.with_lock(batch_state_gid, max_wait: BATCH_MAX_LOCK_WAIT) do
251
277
  # Check that all children are complete
252
- state.values.all? { |e| COMPLETION_STATUSES.include?(e) }
278
+ redis.hvals(batch_state_gid).all? { |e| COMPLETION_STATUSES.include?(e) }
253
279
  end
254
280
  end
255
281
 
@@ -285,8 +311,8 @@ module Cloudtasker
285
311
  # Propagate event
286
312
  parent_batch&.on_child_complete(self, status)
287
313
 
288
- # The batch tree is complete. Cleanup the tree.
289
- cleanup unless parent_batch
314
+ # The batch tree is complete. Cleanup the downstream tree.
315
+ cleanup
290
316
  end
291
317
 
292
318
  #
@@ -331,11 +357,10 @@ module Cloudtasker
331
357
  # Remove all batch and sub-batch keys from Redis.
332
358
  #
333
359
  def cleanup
334
- # Capture batch state
335
- state = batch_state
360
+ migrate_batch_state_to_redis_hash
336
361
 
337
362
  # Delete child batches recursively
338
- state.to_h.keys.each { |id| self.class.find(id)&.cleanup }
363
+ redis.hkeys(batch_state_gid).each { |id| self.class.find(id)&.cleanup }
339
364
 
340
365
  # Delete batch redis entries
341
366
  redis.del(batch_gid)
@@ -347,13 +372,20 @@ module Cloudtasker
347
372
  #
348
373
  # @return [Cloudtasker::Batch::BatchProgress] The batch progress.
349
374
  #
350
- def progress
375
+ def progress(depth: 0)
376
+ depth = depth.to_i
377
+
351
378
  # Capture batch state
352
379
  state = batch_state
353
380
 
354
- # Sum batch progress of current batch and all sub-batches
381
+ # Return immediately if we do not need to go down the tree
382
+ return BatchProgress.new(state) if depth <= 0
383
+
384
+ # Sum batch progress of current batch and sub-batches up to the specified
385
+ # depth
355
386
  state.to_h.reduce(BatchProgress.new(state)) do |memo, (child_id, child_status)|
356
- memo + (self.class.find(child_id)&.progress || BatchProgress.new(child_id => child_status))
387
+ memo + (self.class.find(child_id)&.progress(depth: depth - 1) ||
388
+ BatchProgress.new(child_id => child_status))
357
389
  end
358
390
  end
359
391
 
@@ -395,7 +427,7 @@ module Cloudtasker
395
427
  # Perform job
396
428
  yield
397
429
 
398
- # Save batch (if child worker has been enqueued)
430
+ # Save batch (if child workers have been enqueued)
399
431
  setup
400
432
 
401
433
  # Complete batch
@@ -3,7 +3,7 @@
3
3
  module Cloudtasker
4
4
  # An interface class to manage tasks on the backend (Cloud Task or Redis)
5
5
  class CloudTask
6
- attr_accessor :id, :http_request, :schedule_time, :retries, :queue
6
+ attr_accessor :id, :http_request, :schedule_time, :retries, :queue, :dispatch_deadline
7
7
 
8
8
  #
9
9
  # The backend to use for cloud tasks.
@@ -73,12 +73,13 @@ module Cloudtasker
73
73
  # @param [Integer] retries The number of times the job failed.
74
74
  # @param [String] queue The queue the task is in.
75
75
  #
76
- def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil)
76
+ def initialize(id:, http_request:, schedule_time: nil, retries: 0, queue: nil, dispatch_deadline: nil)
77
77
  @id = id
78
78
  @http_request = http_request
79
79
  @schedule_time = schedule_time
80
80
  @retries = retries || 0
81
81
  @queue = queue
82
+ @dispatch_deadline = dispatch_deadline
82
83
  end
83
84
 
84
85
  #
@@ -7,7 +7,7 @@ module Cloudtasker
7
7
  class Config
8
8
  attr_accessor :redis, :store_payloads_in_redis
9
9
  attr_writer :secret, :gcp_location_id, :gcp_project_id,
10
- :gcp_queue_prefix, :processor_path, :logger, :mode, :max_retries
10
+ :gcp_queue_prefix, :processor_path, :logger, :mode, :max_retries, :dispatch_deadline
11
11
 
12
12
  # Max Cloud Task size in bytes
13
13
  MAX_TASK_SIZE = 100 * 1024 # 100 KB
@@ -46,6 +46,11 @@ module Cloudtasker
46
46
  DEFAULT_QUEUE_CONCURRENCY = 10
47
47
  DEFAULT_QUEUE_RETRIES = -1 # unlimited
48
48
 
49
+ # Job timeout configuration for Cloud Tasks
50
+ DEFAULT_DISPATCH_DEADLINE = 10 * 60 # 10 minutes
51
+ MIN_DISPATCH_DEADLINE = 15 # seconds
52
+ MAX_DISPATCH_DEADLINE = 30 * 60 # 30 minutes
53
+
49
54
  # The number of times jobs will be attempted before declaring them dead.
50
55
  #
51
56
  # With the default retry configuration (maxDoublings = 16 and minBackoff = 0.100s)
@@ -207,6 +212,16 @@ module Cloudtasker
207
212
  @gcp_location_id || DEFAULT_LOCATION_ID
208
213
  end
209
214
 
215
+ #
216
+ # Return the Dispatch deadline duration. Cloud Tasks will timeout the job after
217
+ # this duration is elapsed.
218
+ #
219
+ # @return [Integer] The value in seconds.
220
+ #
221
+ def dispatch_deadline
222
+ @dispatch_deadline || DEFAULT_DISPATCH_DEADLINE
223
+ end
224
+
210
225
  #
211
226
  # Return the secret to use to sign the verification tokens
212
227
  # attached to tasks.
@@ -75,14 +75,18 @@ module Cloudtasker
75
75
  # end
76
76
  #
77
77
  # @param [String] cache_key The cache key to access.
78
+ # @param [Integer] max_wait The number of seconds after which the lock will be cleared anyway.
78
79
  #
79
- def with_lock(cache_key)
80
+ def with_lock(cache_key, max_wait: nil)
80
81
  return nil unless cache_key
81
82
 
83
+ # Set max wait
84
+ max_wait = (max_wait || LOCK_DURATION).to_i
85
+
82
86
  # Wait to acquire lock
83
87
  lock_key = [LOCK_KEY_PREFIX, cache_key].join('/')
84
88
  client.with do |conn|
85
- sleep(LOCK_WAIT_DURATION) until conn.set(lock_key, true, nx: true, ex: LOCK_DURATION)
89
+ sleep(LOCK_WAIT_DURATION) until conn.set(lock_key, true, nx: true, ex: max_wait)
86
90
  end
87
91
 
88
92
  # yield content
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cloudtasker
4
- VERSION = '0.12.rc4'
4
+ VERSION = '0.12.rc9'
5
5
  end
@@ -167,6 +167,22 @@ module Cloudtasker
167
167
  (@job_queue ||= self.class.cloudtasker_options_hash[:queue] || Config::DEFAULT_JOB_QUEUE).to_s
168
168
  end
169
169
 
170
+ #
171
+ # Return the Dispatch deadline duration. Cloud Tasks will timeout the job after
172
+ # this duration is elapsed.
173
+ #
174
+ # @return [Integer] The value in seconds.
175
+ #
176
+ def dispatch_deadline
177
+ @dispatch_deadline ||= [
178
+ [
179
+ Config::MIN_DISPATCH_DEADLINE,
180
+ (self.class.cloudtasker_options_hash[:dispatch_deadline] || Cloudtasker.config.dispatch_deadline).to_i
181
+ ].max,
182
+ Config::MAX_DISPATCH_DEADLINE
183
+ ].min
184
+ end
185
+
170
186
  #
171
187
  # Return the Cloudtasker logger instance.
172
188
  #
@@ -332,6 +348,22 @@ module Cloudtasker
332
348
  job_retries > job_max_retries
333
349
  end
334
350
 
351
+ #
352
+ # Return true if the job arguments are missing.
353
+ #
354
+ # This may happen if a job
355
+ # was successfully run but retried due to Cloud Task dispatch deadline
356
+ # exceeded. If the arguments were stored in Redis then they may have
357
+ # been flushed already after the successful completion.
358
+ #
359
+ # If job arguments are missing then the job will simply be declared dead.
360
+ #
361
+ # @return [Boolean] True if the arguments are missing.
362
+ #
363
+ def arguments_missing?
364
+ job_args.empty? && [0, -1].exclude?(method(:perform).arity)
365
+ end
366
+
335
367
  #
336
368
  # Return the time taken (in seconds) to perform the job. This duration
337
369
  # includes the middlewares and the actual perform method.
@@ -384,14 +416,9 @@ module Cloudtasker
384
416
  Cloudtasker.config.server_middleware.invoke(self) do
385
417
  # Immediately abort the job if it is already dead
386
418
  flag_as_dead if job_dead?
419
+ flag_as_dead(MissingWorkerArgumentsError.new('worker arguments are missing')) if arguments_missing?
387
420
 
388
421
  begin
389
- # Abort if arguments are missing. This may happen with redis arguments storage
390
- # if Cloud Tasks times out on a job but the job still succeeds
391
- if job_args.empty? && [0, -1].exclude?(method(:perform).arity)
392
- raise(MissingWorkerArgumentsError, 'worker arguments are missing')
393
- end
394
-
395
422
  # Perform the job
396
423
  perform(*job_args)
397
424
  rescue StandardError => e
@@ -14,12 +14,6 @@ module Cloudtasker
14
14
  # payloads in Redis
15
15
  REDIS_PAYLOAD_NAMESPACE = 'payload'
16
16
 
17
- # Arg payload cache keys get expired instead of deleted
18
- # in case jobs are re-processed due to connection interruption
19
- # (job is successful but Cloud Task considers it as failed due
20
- # to network interruption)
21
- ARGS_PAYLOAD_CLEANUP_TTL = 3600 # 1 hour
22
-
23
17
  #
24
18
  # Return a namespaced key
25
19
  #
@@ -100,16 +94,13 @@ module Cloudtasker
100
94
  # Yied worker
101
95
  resp = yield(worker)
102
96
 
103
- # Schedule args payload deletion after job has been successfully processed
104
- # Note: we expire the key instead of deleting it immediately in case the job
105
- # succeeds but is considered as failed by Cloud Task due to network interruption.
106
- # In such case the job is likely to be re-processed soon after.
107
- redis.expire(args_payload_key, ARGS_PAYLOAD_CLEANUP_TTL) if args_payload_key && !worker.job_reenqueued
97
+ # Delete stored args payload if job has completed
98
+ redis.del(args_payload_key) if args_payload_key && !worker.job_reenqueued
108
99
 
109
100
  resp
110
- rescue DeadWorkerError, MissingWorkerArgumentsError => e
101
+ rescue DeadWorkerError => e
111
102
  # Delete stored args payload if job is dead
112
- redis.expire(args_payload_key, ARGS_PAYLOAD_CLEANUP_TTL) if args_payload_key
103
+ redis.del(args_payload_key) if args_payload_key
113
104
  log_execution_error(worker, e)
114
105
  raise(e)
115
106
  rescue StandardError => e
@@ -165,6 +156,7 @@ module Cloudtasker
165
156
  },
166
157
  body: worker_payload.to_json
167
158
  },
159
+ dispatch_deadline: worker.dispatch_deadline.to_i,
168
160
  queue: worker.job_queue
169
161
  }
170
162
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudtasker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.rc4
4
+ version: 0.12.rc9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arnaud Lachaume
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-03-29 00:00:00.000000000 Z
11
+ date: 2021-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport