cloudtasker 0.12.rc2 → 0.12.rc7
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 +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +7 -3
- data/README.md +1 -1
- data/app/controllers/cloudtasker/worker_controller.rb +1 -1
- data/docs/BATCH_JOBS.md +24 -3
- data/lib/cloudtasker/backend/redis_task.rb +1 -0
- data/lib/cloudtasker/batch/job.rb +52 -20
- data/lib/cloudtasker/redis_client.rb +6 -2
- data/lib/cloudtasker/version.rb +1 -1
- data/lib/cloudtasker/worker.rb +17 -6
- data/lib/cloudtasker/worker_handler.rb +1 -1
- data/lib/cloudtasker/worker_logger.rb +29 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96524dc4a6825a3760a462277f7b0a710d779521e759bce44374ea044dbd649d
|
4
|
+
data.tar.gz: 908f1497b7ad316549f4f347c363b183181f0e0a17a92bda3f50ac9c34e2247c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e18579d27321e09f1e997ad3fdd3e3e0d1d0c344cd5fc553b64f665937fbe510cd11957381eee1fee5c100e2f14d8390434f26c60307fec4ecf4a681b15885d8
|
7
|
+
data.tar.gz: 3b688838f1a518f2f5c7b914a590f035d8a828c2c2b67838173a1712b8765e2ce34cddb8ddf1214aaefc26ce8ec43b7cc1dbe4eb450ccb24feb9e06bcd19bac7
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,15 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## Latest RC [v0.12.
|
3
|
+
## Latest RC [v0.12.rc7](https://github.com/keypup-io/cloudtasker/tree/v0.12.rc7) (2021-03-31)
|
4
4
|
|
5
|
-
[Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.11.0...v0.12.
|
5
|
+
[Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.11.0...v0.12.rc7)
|
6
6
|
|
7
7
|
**Improvements:**
|
8
8
|
- ActiveJob: do not double log errors (ActiveJob has its own error logging)
|
9
|
+
- Cron jobs: Use Redis Sets instead of key pattern matching for resource listing
|
9
10
|
- Error logging: Use worker logger so as to include context (job args etc.)
|
10
11
|
- Error logging: Do not log exception and stack trace separately, combine them instead.
|
11
12
|
- Batch callbacks: Retry jobs when completion callback fails
|
12
|
-
-
|
13
|
+
- Batch state: use native Redis hashes to store batch state instead of a serialized hash in a string key
|
14
|
+
- 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.
|
15
|
+
- Local server: Use Redis Sets instead of key pattern matching for resource listing
|
16
|
+
- Worker: raise DeadWorkerError instead of MissingWorkerArgumentsError when arguments are missing. This is more consistent with what middlewares expect.
|
13
17
|
|
14
18
|
**Fixed bugs:**
|
15
19
|
- 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
@@ -136,7 +136,7 @@ That's it! Your job was picked up by the Cloudtasker local server and sent for p
|
|
136
136
|
Now jump to the next section to configure your app to use Google Cloud Tasks as a backend.
|
137
137
|
|
138
138
|
## Get started with Rails & ActiveJob
|
139
|
-
**Note**: ActiveJob is supported since `0.11.0`
|
139
|
+
**Note**: ActiveJob is supported since `0.11.0`
|
140
140
|
**Note**: Cloudtasker extensions (cron, batch and unique jobs) are not available when using cloudtasker via ActiveJob.
|
141
141
|
|
142
142
|
Cloudtasker is pre-integrated with ActiveJob. Follow the steps below to get started.
|
@@ -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
|
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
|
-
|
88
|
-
logger.info("
|
89
|
-
logger.info("
|
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
|
```
|
@@ -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
|
-
|
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.
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
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 [
|
270
|
+
# @return [Boolean] True if the batch is complete.
|
245
271
|
#
|
246
272
|
def complete?
|
247
|
-
|
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
|
-
|
278
|
+
redis.hvals(batch_state_gid).all? { |e| COMPLETION_STATUSES.include?(e) }
|
253
279
|
end
|
254
280
|
end
|
255
281
|
|
@@ -331,11 +357,10 @@ module Cloudtasker
|
|
331
357
|
# Remove all batch and sub-batch keys from Redis.
|
332
358
|
#
|
333
359
|
def cleanup
|
334
|
-
|
335
|
-
state = batch_state
|
360
|
+
migrate_batch_state_to_redis_hash
|
336
361
|
|
337
362
|
# Delete child batches recursively
|
338
|
-
|
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
|
-
#
|
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
|
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
|
430
|
+
# Save batch (if child workers have been enqueued)
|
399
431
|
setup
|
400
432
|
|
401
433
|
# Complete batch
|
@@ -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:
|
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
|
data/lib/cloudtasker/version.rb
CHANGED
data/lib/cloudtasker/worker.rb
CHANGED
@@ -332,6 +332,22 @@ module Cloudtasker
|
|
332
332
|
job_retries > job_max_retries
|
333
333
|
end
|
334
334
|
|
335
|
+
#
|
336
|
+
# Return true if the job arguments are missing.
|
337
|
+
#
|
338
|
+
# This may happen if a job
|
339
|
+
# was successfully run but retried due to Cloud Task dispatch deadline
|
340
|
+
# exceeded. If the arguments were stored in Redis then they may have
|
341
|
+
# been flushed already after the successful completion.
|
342
|
+
#
|
343
|
+
# If job arguments are missing then the job will simply be declared dead.
|
344
|
+
#
|
345
|
+
# @return [Boolean] True if the arguments are missing.
|
346
|
+
#
|
347
|
+
def arguments_missing?
|
348
|
+
job_args.empty? && [0, -1].exclude?(method(:perform).arity)
|
349
|
+
end
|
350
|
+
|
335
351
|
#
|
336
352
|
# Return the time taken (in seconds) to perform the job. This duration
|
337
353
|
# includes the middlewares and the actual perform method.
|
@@ -384,14 +400,9 @@ module Cloudtasker
|
|
384
400
|
Cloudtasker.config.server_middleware.invoke(self) do
|
385
401
|
# Immediately abort the job if it is already dead
|
386
402
|
flag_as_dead if job_dead?
|
403
|
+
flag_as_dead(MissingWorkerArgumentsError.new('worker arguments are missing')) if arguments_missing?
|
387
404
|
|
388
405
|
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
406
|
# Perform the job
|
396
407
|
perform(*job_args)
|
397
408
|
rescue StandardError => e
|
@@ -107,7 +107,7 @@ module Cloudtasker
|
|
107
107
|
redis.expire(args_payload_key, ARGS_PAYLOAD_CLEANUP_TTL) if args_payload_key && !worker.job_reenqueued
|
108
108
|
|
109
109
|
resp
|
110
|
-
rescue DeadWorkerError
|
110
|
+
rescue DeadWorkerError => e
|
111
111
|
# Delete stored args payload if job is dead
|
112
112
|
redis.expire(args_payload_key, ARGS_PAYLOAD_CLEANUP_TTL) if args_payload_key
|
113
113
|
log_execution_error(worker, e)
|
@@ -51,6 +51,26 @@ module Cloudtasker
|
|
51
51
|
Cloudtasker.logger
|
52
52
|
end
|
53
53
|
|
54
|
+
#
|
55
|
+
# Format the log message as string.
|
56
|
+
#
|
57
|
+
# @param [Object] msg The log message or object.
|
58
|
+
#
|
59
|
+
# @return [String] The formatted message
|
60
|
+
#
|
61
|
+
def formatted_message_as_string(msg)
|
62
|
+
# Format message
|
63
|
+
msg_content = if msg.is_a?(Exception)
|
64
|
+
[msg.inspect, msg.backtrace].flatten(1).join("\n")
|
65
|
+
elsif msg.is_a?(String)
|
66
|
+
msg
|
67
|
+
else
|
68
|
+
msg.inspect
|
69
|
+
end
|
70
|
+
|
71
|
+
"[Cloudtasker][#{worker.class}][#{worker.job_id}] #{msg_content}"
|
72
|
+
end
|
73
|
+
|
54
74
|
#
|
55
75
|
# Format main log message.
|
56
76
|
#
|
@@ -59,7 +79,12 @@ module Cloudtasker
|
|
59
79
|
# @return [String] The formatted log message
|
60
80
|
#
|
61
81
|
def formatted_message(msg)
|
62
|
-
|
82
|
+
if msg.is_a?(String)
|
83
|
+
formatted_message_as_string(msg)
|
84
|
+
else
|
85
|
+
# Delegate object formatting to logger
|
86
|
+
msg
|
87
|
+
end
|
63
88
|
end
|
64
89
|
|
65
90
|
#
|
@@ -147,7 +172,9 @@ module Cloudtasker
|
|
147
172
|
# ActiveSupport::Logger does not support passing a payload through a block on top
|
148
173
|
# of a message.
|
149
174
|
if defined?(ActiveSupport::Logger) && logger.is_a?(ActiveSupport::Logger)
|
150
|
-
logger
|
175
|
+
# The logger is fairly basic in terms of formatting. All inputs get converted
|
176
|
+
# as regular strings.
|
177
|
+
logger.send(level) { "#{formatted_message_as_string(msg)} -- #{payload_block.call}" }
|
151
178
|
else
|
152
179
|
logger.send(level, formatted_message(msg), &payload_block)
|
153
180
|
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.
|
4
|
+
version: 0.12.rc7
|
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-
|
11
|
+
date: 2021-03-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|