cloudtasker 0.15.rc2 → 0.15.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 +4 -4
- data/.github/workflows/test_ruby_3.x.yml +1 -1
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +15 -2
- data/README.md +15 -3
- data/app/controllers/cloudtasker/worker_controller.rb +13 -1
- data/docs/UNIQUE_JOBS.md +1 -1
- data/lib/cloudtasker/backend/google_cloud_task_v1.rb +8 -4
- data/lib/cloudtasker/backend/google_cloud_task_v2.rb +8 -4
- data/lib/cloudtasker/backend/memory_task.rb +11 -2
- data/lib/cloudtasker/batch/job.rb +49 -8
- data/lib/cloudtasker/cli.rb +2 -2
- data/lib/cloudtasker/cloud_task.rb +13 -1
- data/lib/cloudtasker/config.rb +21 -4
- data/lib/cloudtasker/invalid_worker_error.rb +4 -0
- data/lib/cloudtasker/max_task_size_exceeded_error.rb +1 -1
- data/lib/cloudtasker/testing.rb +51 -0
- data/lib/cloudtasker/unique_job/job.rb +96 -5
- data/lib/cloudtasker/unique_job/lock/until_completed.rb +5 -2
- data/lib/cloudtasker/unique_job/lock/until_executed.rb +5 -2
- data/lib/cloudtasker/unique_job/lock/until_executing.rb +5 -2
- data/lib/cloudtasker/unique_job/middleware/client.rb +2 -3
- data/lib/cloudtasker/unique_job.rb +14 -1
- data/lib/cloudtasker/version.rb +1 -1
- data/lib/cloudtasker/worker.rb +27 -8
- data/lib/cloudtasker/worker_handler.rb +4 -4
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ff26870d0e8805c1d799e77ca9e75904ed73f662fe19fa2296f79e276948b86
|
|
4
|
+
data.tar.gz: ab653c93ebda2b48aac08af8046324e7ee2ff14b368e9bbb656b1a7f79793749
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36ab5b779f9e7a56090983cfb82384c15a299bfd1300ce13fab1074bef6616c465269a766d1bc0b30a536336608d96359ee4cf26ff575590155afd3606ee4b3a
|
|
7
|
+
data.tar.gz: 98a7468f308d8ec6963b4909783a34e0257825c3a307610d05d7dbbd24e67030d880c3b5de273a589b327feee1def92396247c012c580db8eb532fb9628ee572
|
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.appraisal }}.gemfile
|
|
33
33
|
steps:
|
|
34
34
|
- uses: actions/checkout@v2
|
|
35
|
-
- uses:
|
|
35
|
+
- uses: shogo82148/actions-setup-redis@v1
|
|
36
36
|
- uses: ruby/setup-ruby@v1
|
|
37
37
|
with:
|
|
38
38
|
ruby-version: ${{ matrix.ruby }}
|
data/.rubocop.yml
CHANGED
|
@@ -12,7 +12,7 @@ Metrics/ClassLength:
|
|
|
12
12
|
Max: 300
|
|
13
13
|
|
|
14
14
|
Metrics/ModuleLength:
|
|
15
|
-
Max:
|
|
15
|
+
Max: 300
|
|
16
16
|
|
|
17
17
|
Metrics/AbcSize:
|
|
18
18
|
Max: 30
|
|
@@ -68,10 +68,14 @@ RSpec/MessageSpies:
|
|
|
68
68
|
Enabled: false
|
|
69
69
|
|
|
70
70
|
RSpec/MultipleExpectations:
|
|
71
|
+
Max: 5
|
|
71
72
|
Exclude:
|
|
72
73
|
- "examples/**/*"
|
|
73
74
|
- "spec/integration/**/*"
|
|
74
75
|
|
|
76
|
+
RSpec/ExampleLength:
|
|
77
|
+
Max: 10
|
|
78
|
+
|
|
75
79
|
RSpec/AnyInstance:
|
|
76
80
|
Enabled: false
|
|
77
81
|
|
data/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [v0.15.
|
|
3
|
+
## [v0.15.0](https://github.com/keypup-io/cloudtasker/tree/v0.15.0) (2026-06-26)
|
|
4
4
|
|
|
5
|
-
[Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.14.0...v0.15.
|
|
5
|
+
[Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.14.0...v0.15.0)
|
|
6
6
|
|
|
7
7
|
**Improvements:**
|
|
8
|
+
- Local Server: allow retry delay to be configured via the env var `LOCAL_SERVER_RETRY_DELAY`
|
|
9
|
+
- Local Server: fix a syntax error on trap handling, which was re-raising an unnecessary exception.
|
|
10
|
+
- Logging: add job `duration` on error logs
|
|
11
|
+
- Logging: log error message as `reason` on `RetryWorkerError`
|
|
8
12
|
- Queues: support `propagate_queue: true` option on `cloudtasker_options` to make workers enqueued inside a job use the runtime queue instead of the default (class-configured or `default`) queue.
|
|
13
|
+
- Testing: add specific `raise_errors!` mode to raise job errors during testing, without having to specifically use `inline_mode`. Using `inline_mode` will still raise errors.
|
|
9
14
|
- Unique Jobs: add `until_completed` strategy to lock jobs until they are completed or have exhausted all retries (thanks @jam-packed).
|
|
15
|
+
- Workers: capture `job_attempts` on workers, which is captured from `X-CloudTasks-TaskRetryCount`
|
|
16
|
+
- Workers: increase the allowed max task size to 1049KB since Google Tasks supports increased payloads
|
|
10
17
|
- Workers: provide `perform_now` method to perform job inline and align with other job frameworks
|
|
18
|
+
- Workers: support a global configuration option to control whether task payloads are base64-encoded. See `base64_encode_body`
|
|
19
|
+
|
|
20
|
+
**Fixed bugs:**
|
|
21
|
+
- Batch Jobs: prevent rollback of completion status if a completed job is replayed. This may happen if Cloud Tasks times out before the job completes.
|
|
22
|
+
- Batch Jobs: prevent rollback of job statuses due to multiple children completing concurrently and updating the parent job status (race condition in batch completion callback).
|
|
23
|
+
- Unique Jobs: avoid long-lock on OOM/error while scheduling jobs. An initial short-lived lock is now acquired before scheduling the job, which is then turned into a long-lived lock after the job has been scheduled. This prevents jobs from being enqueued for a fair amount of time after a scheduling crash.
|
|
11
24
|
|
|
12
25
|
**Maintenance:**
|
|
13
26
|
- Supported rubies: drop support for ruby `2.7`. Cloudtasker now requires ruby `3.0` and above.
|
data/README.md
CHANGED
|
@@ -122,7 +122,7 @@ Open a Rails console and enqueue some jobs
|
|
|
122
122
|
DummyWorker.perform_in(60, 'foo')
|
|
123
123
|
|
|
124
124
|
# Process job immediately, inline
|
|
125
|
-
# Supported since: v0.15.
|
|
125
|
+
# Supported since: v0.15.0
|
|
126
126
|
DummyWorker.perform_now('foo')
|
|
127
127
|
```
|
|
128
128
|
|
|
@@ -446,6 +446,18 @@ Cloudtasker.configure do |config|
|
|
|
446
446
|
# Default: true
|
|
447
447
|
#
|
|
448
448
|
# config.local_server_ssl_verify = true
|
|
449
|
+
|
|
450
|
+
#
|
|
451
|
+
# Enable/disable the base64 encoding of task payloads when sent to Google Cloud Tasks.
|
|
452
|
+
#
|
|
453
|
+
# Base64-encoding is required for job payloads containing special characters.
|
|
454
|
+
# The downside is that the Google Cloud Task UI will therefore obfuscate the payload and force users to manually decode the payload to see the actual content.
|
|
455
|
+
#
|
|
456
|
+
# Supported since: v0.15.0
|
|
457
|
+
#
|
|
458
|
+
# Default: true
|
|
459
|
+
#
|
|
460
|
+
# config.base64_encode_body = true
|
|
449
461
|
end
|
|
450
462
|
```
|
|
451
463
|
|
|
@@ -481,7 +493,7 @@ MyWorker.schedule(args: [arg1, arg2], time_in: 5 * 60, queue: 'critical')
|
|
|
481
493
|
|
|
482
494
|
# Perform worker immediately, inline. This will not send the job to
|
|
483
495
|
# the processing queue. Middlewares such as Unique Job, Batch Jobs will still be invoked.
|
|
484
|
-
# Supported since: v0.15.
|
|
496
|
+
# Supported since: v0.15.0
|
|
485
497
|
MyWorker.perform_now(arg1, arg2)
|
|
486
498
|
```
|
|
487
499
|
|
|
@@ -555,7 +567,7 @@ CriticalWorker.schedule(args: [1], queue: :important)
|
|
|
555
567
|
```
|
|
556
568
|
|
|
557
569
|
### Propagating the queue in child workers
|
|
558
|
-
**Supported since:** `v0.15.
|
|
570
|
+
**Supported since:** `v0.15.0`
|
|
559
571
|
|
|
560
572
|
You can specify `propagate_queue: true` via the `cloudtasker_options` to make workers enqueued inside a job use the runtime queue instead of the default (class-configured or `default`) queue:
|
|
561
573
|
|
|
@@ -63,7 +63,9 @@ module Cloudtasker
|
|
|
63
63
|
#
|
|
64
64
|
def payload
|
|
65
65
|
# Return content parsed as JSON and add job retries count
|
|
66
|
-
@payload ||= JSON.parse(json_payload).merge(
|
|
66
|
+
@payload ||= JSON.parse(json_payload).merge(
|
|
67
|
+
job_retries: job_retries, job_attempts: job_attempts, task_id: task_id
|
|
68
|
+
)
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
#
|
|
@@ -75,6 +77,16 @@ module Cloudtasker
|
|
|
75
77
|
request.headers[Cloudtasker::Config::RETRY_HEADER].to_i
|
|
76
78
|
end
|
|
77
79
|
|
|
80
|
+
#
|
|
81
|
+
# Extract the number of times this task was attempted at runtime.
|
|
82
|
+
# This includes all attempts (including 50x errors).
|
|
83
|
+
#
|
|
84
|
+
# @return [Integer] The number of attempts.
|
|
85
|
+
#
|
|
86
|
+
def job_attempts
|
|
87
|
+
request.headers[Cloudtasker::Config::ATTEMPT_HEADER].to_i
|
|
88
|
+
end
|
|
89
|
+
|
|
78
90
|
#
|
|
79
91
|
# Return the Google Cloud Task ID from headers.
|
|
80
92
|
#
|
data/docs/UNIQUE_JOBS.md
CHANGED
|
@@ -70,7 +70,7 @@ For each lock strategy the table specifies the lock period (start/end) and which
|
|
|
70
70
|
| `until_executing` | The job is scheduled | The job starts processing | `reject` (default) or `raise` |
|
|
71
71
|
| `while_executing` | The job starts processing | The job ends processing | `reject` (default), `reschedule` or `raise` |
|
|
72
72
|
| `until_executed` | The job is scheduled | The job ends processing | `reject` (default) or `raise` |
|
|
73
|
-
| `until_completed` | The job is scheduled | The job completes successfully or a `DeadWorkerError` is raised. Supported since `v0.15.
|
|
73
|
+
| `until_completed` | The job is scheduled | The job completes successfully or a `DeadWorkerError` is raised. Supported since `v0.15.0`. | `reject` (default) or `raise` |
|
|
74
74
|
|
|
75
75
|
## Available conflict strategies
|
|
76
76
|
|
|
@@ -119,12 +119,16 @@ module Cloudtasker
|
|
|
119
119
|
# Format dispatch_deadline to Google::Protobuf::Duration
|
|
120
120
|
payload[:dispatch_deadline] = format_protobuf_duration(payload[:dispatch_deadline])
|
|
121
121
|
|
|
122
|
-
#
|
|
123
|
-
# expect content to be ASCII-8BIT compatible (binary)
|
|
122
|
+
# Setup headers
|
|
124
123
|
payload[:http_request][:headers] ||= {}
|
|
125
124
|
payload[:http_request][:headers][Cloudtasker::Config::CONTENT_TYPE_HEADER] = 'text/json'
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
128
132
|
|
|
129
133
|
payload.compact
|
|
130
134
|
end
|
|
@@ -121,12 +121,16 @@ module Cloudtasker
|
|
|
121
121
|
# Format dispatch_deadline to Google::Protobuf::Duration
|
|
122
122
|
payload[:dispatch_deadline] = format_protobuf_duration(payload[:dispatch_deadline])
|
|
123
123
|
|
|
124
|
-
#
|
|
125
|
-
# Google Cloud Task expect content to be ASCII-8BIT compatible (binary)
|
|
124
|
+
# Setup headers
|
|
126
125
|
payload[:http_request][:headers] ||= {}
|
|
127
126
|
payload[:http_request][:headers][Cloudtasker::Config::CONTENT_TYPE_HEADER] = 'text/json'
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
130
134
|
|
|
131
135
|
payload.compact
|
|
132
136
|
end
|
|
@@ -17,6 +17,15 @@ module Cloudtasker
|
|
|
17
17
|
defined?(Cloudtasker::Testing) && Cloudtasker::Testing.inline?
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
#
|
|
21
|
+
# Return true if errors must be raised immediately
|
|
22
|
+
#
|
|
23
|
+
# @return [Boolean] True if raise error mode is enabled.
|
|
24
|
+
#
|
|
25
|
+
def self.raise_errors?
|
|
26
|
+
defined?(Cloudtasker::Testing) && Cloudtasker::Testing.raise_errors?
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
#
|
|
21
30
|
# Return the task queue. A worker class name
|
|
22
31
|
#
|
|
@@ -172,10 +181,10 @@ module Cloudtasker
|
|
|
172
181
|
resp
|
|
173
182
|
rescue DeadWorkerError => e
|
|
174
183
|
self.class.delete(id)
|
|
175
|
-
raise(e) if self.class.
|
|
184
|
+
raise(e) if self.class.raise_errors?
|
|
176
185
|
rescue StandardError => e
|
|
177
186
|
self.job_retries += 1
|
|
178
|
-
raise(e) if self.class.
|
|
187
|
+
raise(e) if self.class.raise_errors?
|
|
179
188
|
end
|
|
180
189
|
|
|
181
190
|
#
|
|
@@ -35,6 +35,10 @@ module Cloudtasker
|
|
|
35
35
|
# to be acquired.
|
|
36
36
|
BATCH_MAX_LOCK_WAIT = 60
|
|
37
37
|
|
|
38
|
+
# TTL for the completion flag that prevents concurrent children from
|
|
39
|
+
# triggering multiple on_complete calls.
|
|
40
|
+
BATCH_COMPLETION_TTL = 100
|
|
41
|
+
|
|
38
42
|
#
|
|
39
43
|
# Return the cloudtasker redis client
|
|
40
44
|
#
|
|
@@ -193,6 +197,15 @@ module Cloudtasker
|
|
|
193
197
|
"#{batch_state_gid}/state_count/#{state}"
|
|
194
198
|
end
|
|
195
199
|
|
|
200
|
+
#
|
|
201
|
+
# Return the key used to track batch completion.
|
|
202
|
+
#
|
|
203
|
+
# @return [String] The batch completion key.
|
|
204
|
+
#
|
|
205
|
+
def batch_completion_gid
|
|
206
|
+
"#{batch_state_gid}/completed"
|
|
207
|
+
end
|
|
208
|
+
|
|
196
209
|
#
|
|
197
210
|
# Return the number of jobs in a given state
|
|
198
211
|
#
|
|
@@ -314,21 +327,34 @@ module Cloudtasker
|
|
|
314
327
|
#
|
|
315
328
|
# Update the batch state.
|
|
316
329
|
#
|
|
317
|
-
# @param [String]
|
|
330
|
+
# @param [String] batch_id The batch id.
|
|
318
331
|
# @param [String] status The status of the sub-batch.
|
|
332
|
+
# @param [Boolean] force Force update the status even if the registered status is a completion status.
|
|
319
333
|
#
|
|
320
|
-
def update_state(batch_id, status)
|
|
334
|
+
def update_state(batch_id, status, force: false)
|
|
321
335
|
migrate_batch_state_to_redis_hash
|
|
322
336
|
|
|
323
|
-
# Get
|
|
324
|
-
|
|
325
|
-
return if
|
|
337
|
+
# Get stored status and abort if no changes
|
|
338
|
+
stored_status = redis.hget(batch_state_gid, batch_id)
|
|
339
|
+
return if stored_status == status.to_s
|
|
340
|
+
|
|
341
|
+
# Abort if the job has already been flagged as completed
|
|
342
|
+
#
|
|
343
|
+
# A job may be duplicated and run concurrently if Cloud Task times out
|
|
344
|
+
# before the job completes.
|
|
345
|
+
#
|
|
346
|
+
# In this case, the original job keeps running in the background while Cloud Task triggers a retry.
|
|
347
|
+
# This retry runs in parallel of the hung job. The retry may complete before the hung job.
|
|
348
|
+
# The hung job may eventually raise an error (e.g. timeout error) after the retried job has completed,
|
|
349
|
+
# which would leave the job status as "errored" instead of "completed in the batch state without
|
|
350
|
+
# the failsafe below.
|
|
351
|
+
return if COMPLETION_STATUSES.include?(stored_status) && !force
|
|
326
352
|
|
|
327
353
|
# Update the batch state batch_id entry with the new status
|
|
328
354
|
# and update counters
|
|
329
355
|
redis.multi do |m|
|
|
330
356
|
m.hset(batch_state_gid, batch_id, status)
|
|
331
|
-
m.decr(batch_state_count_gid(
|
|
357
|
+
m.decr(batch_state_count_gid(stored_status))
|
|
332
358
|
m.incr(batch_state_count_gid(status))
|
|
333
359
|
end
|
|
334
360
|
end
|
|
@@ -407,8 +433,22 @@ module Cloudtasker
|
|
|
407
433
|
run_worker_callback(:on_child_dead, child_batch.worker)
|
|
408
434
|
end
|
|
409
435
|
|
|
410
|
-
|
|
411
|
-
|
|
436
|
+
return if status == :errored
|
|
437
|
+
return unless complete?
|
|
438
|
+
|
|
439
|
+
# Notify the parent batch that we are done with this batch. Use SETNX to ensure
|
|
440
|
+
# only the first concurrent child to complete triggers on_complete.
|
|
441
|
+
return unless redis.set(batch_completion_gid, true, nx: true, ex: BATCH_COMPLETION_TTL)
|
|
442
|
+
|
|
443
|
+
begin
|
|
444
|
+
on_complete
|
|
445
|
+
rescue StandardError
|
|
446
|
+
# Clear key on error so completion can be reattempted
|
|
447
|
+
redis.del(batch_completion_gid)
|
|
448
|
+
|
|
449
|
+
# Re-raise
|
|
450
|
+
raise
|
|
451
|
+
end
|
|
412
452
|
end
|
|
413
453
|
|
|
414
454
|
#
|
|
@@ -439,6 +479,7 @@ module Cloudtasker
|
|
|
439
479
|
redis.multi do |m|
|
|
440
480
|
m.del(batch_gid)
|
|
441
481
|
m.del(batch_state_gid)
|
|
482
|
+
m.del(batch_completion_gid)
|
|
442
483
|
BATCH_STATUSES.each { |e| m.del(batch_state_count_gid(e)) }
|
|
443
484
|
end
|
|
444
485
|
end
|
data/lib/cloudtasker/cli.rb
CHANGED
|
@@ -82,7 +82,7 @@ module Cloudtasker
|
|
|
82
82
|
logger.info "[Cloudtasker/Server] Booted Rails #{::Rails.version} application in #{environment} environment"
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
# Get internal read/write
|
|
85
|
+
# Get internal read/write pipe
|
|
86
86
|
self_read, self_write = IO.pipe
|
|
87
87
|
|
|
88
88
|
# Setup signals to trap
|
|
@@ -104,7 +104,7 @@ module Cloudtasker
|
|
|
104
104
|
local_server.start(opts)
|
|
105
105
|
|
|
106
106
|
while (readable_io = read_pipe.wait_readable)
|
|
107
|
-
signal = readable_io.first
|
|
107
|
+
signal = readable_io.first.chomp
|
|
108
108
|
handle_signal(signal)
|
|
109
109
|
end
|
|
110
110
|
rescue Interrupt
|
|
@@ -82,12 +82,24 @@ module Cloudtasker
|
|
|
82
82
|
# @return [Cloudtasker::CloudTask] The created task.
|
|
83
83
|
#
|
|
84
84
|
def self.create(payload)
|
|
85
|
-
raise MaxTaskSizeExceededError if payload
|
|
85
|
+
raise MaxTaskSizeExceededError if payload_size(payload) > Config::MAX_TASK_SIZE
|
|
86
86
|
|
|
87
87
|
resp = backend.create(payload)&.to_h
|
|
88
88
|
resp ? new(**resp) : nil
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
#
|
|
92
|
+
# Calculate the size of a task payload.
|
|
93
|
+
# The size of the task will be inflated if Base64 encoding is used.
|
|
94
|
+
#
|
|
95
|
+
# @param [Hash] payload The payload of the task
|
|
96
|
+
#
|
|
97
|
+
# @return [Integer] The size of of the payload, in bytes.
|
|
98
|
+
#
|
|
99
|
+
def self.payload_size(payload)
|
|
100
|
+
(Cloudtasker.config.base64_encode_body ? Base64.encode64(payload.to_json) : payload.to_json).bytesize
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
#
|
|
92
104
|
# Delete a cloud task by id.
|
|
93
105
|
#
|
data/lib/cloudtasker/config.rb
CHANGED
|
@@ -8,18 +8,22 @@ module Cloudtasker
|
|
|
8
8
|
attr_accessor :redis, :store_payloads_in_redis, :gcp_queue_prefix
|
|
9
9
|
attr_writer :secret, :gcp_location_id, :gcp_project_id,
|
|
10
10
|
:processor_path, :logger, :mode, :max_retries,
|
|
11
|
-
:dispatch_deadline, :on_error, :on_dead, :oidc, :local_server_ssl_verify
|
|
11
|
+
:dispatch_deadline, :on_error, :on_dead, :oidc, :local_server_ssl_verify,
|
|
12
|
+
:base64_encode_body
|
|
12
13
|
|
|
13
14
|
# Max Cloud Task size in bytes
|
|
14
|
-
|
|
15
|
+
# The GCP limit is 1MiB, which is 1049KB.
|
|
16
|
+
# The task formatting and headers add about 20KB.
|
|
17
|
+
MAX_TASK_SIZE = 1000 * 1000 # 1000 KB
|
|
15
18
|
|
|
16
19
|
# Retry header in Cloud Task responses
|
|
17
20
|
#
|
|
18
21
|
# Definitions:
|
|
19
|
-
# X-CloudTasks-TaskRetryCount: total number of retries (including
|
|
20
|
-
# X-CloudTasks-TaskExecutionCount: number of non-
|
|
22
|
+
# X-CloudTasks-TaskRetryCount: total number of retries (including 50x errors)
|
|
23
|
+
# X-CloudTasks-TaskExecutionCount: number of non-50x retries (= actual number of job failures)
|
|
21
24
|
#
|
|
22
25
|
RETRY_HEADER = 'X-Cloudtasks-Taskexecutioncount'
|
|
26
|
+
ATTEMPT_HEADER = 'X-CloudTasks-TaskRetryCount'
|
|
23
27
|
|
|
24
28
|
# Cloud Task ID header
|
|
25
29
|
TASK_ID_HEADER = 'X-CloudTasks-TaskName'
|
|
@@ -56,6 +60,9 @@ module Cloudtasker
|
|
|
56
60
|
# Default on_error Proc
|
|
57
61
|
DEFAULT_ON_ERROR = ->(error, worker) {}
|
|
58
62
|
|
|
63
|
+
# Default base64 encoding flag
|
|
64
|
+
DEFAULT_BASE64_ENCODE_BODY = true
|
|
65
|
+
|
|
59
66
|
# Cache key prefix used to store workers in cache and retrieve
|
|
60
67
|
# them later.
|
|
61
68
|
WORKER_STORE_PREFIX = 'worker_store'
|
|
@@ -301,5 +308,15 @@ module Cloudtasker
|
|
|
301
308
|
def local_server_ssl_verify
|
|
302
309
|
@local_server_ssl_verify.nil? ? DEFAULT_LOCAL_SERVER_SSL_VERIFY_MODE : @local_server_ssl_verify
|
|
303
310
|
end
|
|
311
|
+
|
|
312
|
+
#
|
|
313
|
+
# Return whether to base64 encode the task body when sending to Cloud Tasks.
|
|
314
|
+
# Encoding is enabled by default to support UTF-8 content.
|
|
315
|
+
#
|
|
316
|
+
# @return [Boolean] Whether to base64 encode the body.
|
|
317
|
+
#
|
|
318
|
+
def base64_encode_body
|
|
319
|
+
@base64_encode_body.nil? ? DEFAULT_BASE64_ENCODE_BODY : @base64_encode_body
|
|
320
|
+
end
|
|
304
321
|
end
|
|
305
322
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Cloudtasker
|
|
4
|
+
# Error raised when a worker class cannot be instantiated.
|
|
4
5
|
class InvalidWorkerError < StandardError
|
|
6
|
+
def initialize(worker_name = nil)
|
|
7
|
+
super(worker_name ? "Invalid worker: #{worker_name}" : 'Invalid worker')
|
|
8
|
+
end
|
|
5
9
|
end
|
|
6
10
|
end
|
|
@@ -5,7 +5,7 @@ module Cloudtasker
|
|
|
5
5
|
# See: https://cloud.google.com/appengine/quotas#Task_Queue
|
|
6
6
|
#
|
|
7
7
|
class MaxTaskSizeExceededError < StandardError
|
|
8
|
-
MSG =
|
|
8
|
+
MSG = "The size of Cloud Tasks must not exceed #{Config::MAX_TASK_SIZE / 1000}KB".freeze
|
|
9
9
|
|
|
10
10
|
def initialize(msg = MSG)
|
|
11
11
|
super
|
data/lib/cloudtasker/testing.rb
CHANGED
|
@@ -29,6 +29,28 @@ module Cloudtasker
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
#
|
|
33
|
+
# Set the error mode, either permanently or
|
|
34
|
+
# temporarily (via block).
|
|
35
|
+
#
|
|
36
|
+
# @param [Symbol] mode The error mode.
|
|
37
|
+
#
|
|
38
|
+
# @return [Symbol] The error mode.
|
|
39
|
+
#
|
|
40
|
+
def switch_error_mode(mode)
|
|
41
|
+
if block_given?
|
|
42
|
+
current_mode = @error_mode
|
|
43
|
+
begin
|
|
44
|
+
@error_mode = mode
|
|
45
|
+
yield
|
|
46
|
+
ensure
|
|
47
|
+
@error_mode = current_mode
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
@error_mode = mode
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
32
54
|
#
|
|
33
55
|
# Set cloudtasker to real mode temporarily
|
|
34
56
|
#
|
|
@@ -81,6 +103,35 @@ module Cloudtasker
|
|
|
81
103
|
@test_mode == :inline
|
|
82
104
|
end
|
|
83
105
|
|
|
106
|
+
#
|
|
107
|
+
# Temporarily raise errors in the same manner
|
|
108
|
+
# inline! does it.
|
|
109
|
+
#
|
|
110
|
+
# This is used when you want to manually drain the jobs
|
|
111
|
+
# but still want to surface errors at runtime, instead of
|
|
112
|
+
# using the retry mechanic.
|
|
113
|
+
#
|
|
114
|
+
def raise_errors!(&block)
|
|
115
|
+
switch_error_mode(:raise, &block)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#
|
|
119
|
+
# Temporarily silence errors. Job will follow the retry logic.
|
|
120
|
+
#
|
|
121
|
+
def silence_errors!(&block)
|
|
122
|
+
switch_error_mode(:silence, &block)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# Return true if jobs should raise errors immediately
|
|
127
|
+
# without relying on retries.
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] True if jobs are run inline.
|
|
130
|
+
#
|
|
131
|
+
def raise_errors?
|
|
132
|
+
@test_mode == :inline || @error_mode == :raise
|
|
133
|
+
end
|
|
134
|
+
|
|
84
135
|
#
|
|
85
136
|
# Return true if tasks should be managed in memory.
|
|
86
137
|
#
|
|
@@ -10,6 +10,12 @@ module Cloudtasker
|
|
|
10
10
|
# The default lock strategy to use. Defaults to "no lock".
|
|
11
11
|
DEFAULT_LOCK = UniqueJob::Lock::NoOp
|
|
12
12
|
|
|
13
|
+
# Warning message when final lock cannot be acquired after scheduling
|
|
14
|
+
LOCK_FINALIZATION_WARNING = 'A provisional lock was acquired before enqueuing the job but the ' \
|
|
15
|
+
'lock could not be finalized after enqueuing the job. This means that ' \
|
|
16
|
+
'it took longer than lock_provisional_ttl to enqueue the job. See ' \
|
|
17
|
+
'Worker#lock_provisional_ttl option.'
|
|
18
|
+
|
|
13
19
|
#
|
|
14
20
|
# Build a new instance of the class.
|
|
15
21
|
#
|
|
@@ -18,7 +24,7 @@ module Cloudtasker
|
|
|
18
24
|
#
|
|
19
25
|
def initialize(worker, opts = {})
|
|
20
26
|
@worker = worker
|
|
21
|
-
@call_opts = opts
|
|
27
|
+
@call_opts = opts.to_h
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
#
|
|
@@ -63,8 +69,26 @@ module Cloudtasker
|
|
|
63
69
|
scheduled_at = [call_opts[:time_at].to_i, now].compact.max
|
|
64
70
|
lock_duration = (options[:lock_ttl] || Cloudtasker::UniqueJob.lock_ttl).to_i
|
|
65
71
|
|
|
66
|
-
# Return TTL
|
|
67
|
-
scheduled_at + lock_duration - now
|
|
72
|
+
# Return the TTL, which is the configured lock_duration at minima
|
|
73
|
+
[lock_duration, scheduled_at + lock_duration - now].max
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
#
|
|
77
|
+
# A provisional lock uses a very short duration and aims
|
|
78
|
+
# at covering the time it takes for the job to be enqueued through
|
|
79
|
+
# the client middleware chain.
|
|
80
|
+
#
|
|
81
|
+
# If the application crashes during this
|
|
82
|
+
# time (e.g. OOM), at least the job won't be locked for an extended period
|
|
83
|
+
# of time (which may span across a parent job retry, for instance)
|
|
84
|
+
#
|
|
85
|
+
# This TTL can be configured via the `lock_provisional_ttl` option on
|
|
86
|
+
# the job itself.
|
|
87
|
+
#
|
|
88
|
+
# @return [Integer] The TTL in seconds
|
|
89
|
+
#
|
|
90
|
+
def lock_provisional_ttl
|
|
91
|
+
(options[:lock_provisional_ttl] || Cloudtasker::UniqueJob.lock_provisional_ttl).to_i
|
|
68
92
|
end
|
|
69
93
|
|
|
70
94
|
#
|
|
@@ -93,6 +117,29 @@ module Cloudtasker
|
|
|
93
117
|
worker.try(:unique_args, worker.job_args) || worker.job_args
|
|
94
118
|
end
|
|
95
119
|
|
|
120
|
+
#
|
|
121
|
+
# The base unique scope generated from lock options
|
|
122
|
+
#
|
|
123
|
+
# @return [Hash] A scope hash
|
|
124
|
+
#
|
|
125
|
+
def base_unique_scope
|
|
126
|
+
if options[:lock_per_batch] && defined?(Cloudtasker::Batch::Job)
|
|
127
|
+
key = Cloudtasker::Batch::Job.key(:parent_id).to_sym
|
|
128
|
+
worker.job_meta.to_h.slice(key)
|
|
129
|
+
else
|
|
130
|
+
{}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
#
|
|
135
|
+
# Return a scope to be included in the digest hash
|
|
136
|
+
#
|
|
137
|
+
# @return [Hash] A scope hash
|
|
138
|
+
#
|
|
139
|
+
def unique_scope
|
|
140
|
+
base_unique_scope.to_h.merge(worker.try(:unique_scope).to_h)
|
|
141
|
+
end
|
|
142
|
+
|
|
96
143
|
#
|
|
97
144
|
# Return a unique description of the job in hash format.
|
|
98
145
|
#
|
|
@@ -101,8 +148,9 @@ module Cloudtasker
|
|
|
101
148
|
def digest_hash
|
|
102
149
|
@digest_hash ||= {
|
|
103
150
|
class: worker.class.to_s,
|
|
104
|
-
unique_args: unique_args
|
|
105
|
-
|
|
151
|
+
unique_args: unique_args,
|
|
152
|
+
unique_scope: unique_scope.presence
|
|
153
|
+
}.compact
|
|
106
154
|
end
|
|
107
155
|
|
|
108
156
|
#
|
|
@@ -156,6 +204,49 @@ module Cloudtasker
|
|
|
156
204
|
raise(LockError) unless lock_acquired || lock_already_acquired
|
|
157
205
|
end
|
|
158
206
|
|
|
207
|
+
#
|
|
208
|
+
# Acquire a provisional lock, yield, then set a final lock.
|
|
209
|
+
#
|
|
210
|
+
# This method is designed for scheduling operations where you need to:
|
|
211
|
+
# 1. Acquire a provisional lock to prevent concurrent scheduling
|
|
212
|
+
# 2. Perform the scheduling operation (yield)
|
|
213
|
+
# 3. Set a final lock with proper TTL after scheduling succeeds
|
|
214
|
+
#
|
|
215
|
+
# Raises a `Cloudtasker::UniqueJob::LockError` if the provisional lock
|
|
216
|
+
# cannot be acquired.
|
|
217
|
+
#
|
|
218
|
+
# @return [Any] The return value of the block
|
|
219
|
+
#
|
|
220
|
+
def lock_for_scheduling!
|
|
221
|
+
# Step 1: Acquire provisional lock
|
|
222
|
+
# Check if the lock is already acquired from a previous run
|
|
223
|
+
acquired = redis.get(unique_gid) == id
|
|
224
|
+
|
|
225
|
+
# Set the lock exclusively, if not acquired already.
|
|
226
|
+
# Refresh the duration otherwise.
|
|
227
|
+
lock_acquired = redis.set(unique_gid, id, nx: !acquired, ex: lock_provisional_ttl)
|
|
228
|
+
raise(LockError) unless lock_acquired
|
|
229
|
+
|
|
230
|
+
# Step 2: Yield to perform scheduling operation
|
|
231
|
+
result = yield
|
|
232
|
+
|
|
233
|
+
# Step 3: Set final lock
|
|
234
|
+
# Check if the lock is still held by this job
|
|
235
|
+
acquired = redis.get(unique_gid) == id
|
|
236
|
+
|
|
237
|
+
# Set the lock with final duration
|
|
238
|
+
# If already acquired, refresh with final TTL
|
|
239
|
+
# If not acquired (expired or taken), try to acquire exclusively
|
|
240
|
+
final_lock_acquired = redis.set(unique_gid, id, nx: !acquired, ex: lock_ttl)
|
|
241
|
+
|
|
242
|
+
# Log a warning if final lock could not be acquired
|
|
243
|
+
# The job has already been enqueued at this point, so raising an error is useless
|
|
244
|
+
worker.logger.warn(LOCK_FINALIZATION_WARNING) unless final_lock_acquired
|
|
245
|
+
|
|
246
|
+
# Return the result of the block
|
|
247
|
+
result
|
|
248
|
+
end
|
|
249
|
+
|
|
159
250
|
#
|
|
160
251
|
# Delete the job lock.
|
|
161
252
|
#
|
|
@@ -12,10 +12,13 @@ module Cloudtasker
|
|
|
12
12
|
# if the lock could not be acquired.
|
|
13
13
|
#
|
|
14
14
|
def schedule(&block)
|
|
15
|
-
job.
|
|
16
|
-
yield
|
|
15
|
+
job.lock_for_scheduling!(&block)
|
|
17
16
|
rescue LockError
|
|
18
17
|
conflict_instance.on_schedule(&block)
|
|
18
|
+
rescue StandardError
|
|
19
|
+
# Unlock the job if any error arises during scheduling
|
|
20
|
+
job.unlock!
|
|
21
|
+
raise
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
#
|
|
@@ -11,10 +11,13 @@ module Cloudtasker
|
|
|
11
11
|
# if the lock could not be acquired.
|
|
12
12
|
#
|
|
13
13
|
def schedule(&block)
|
|
14
|
-
job.
|
|
15
|
-
yield
|
|
14
|
+
job.lock_for_scheduling!(&block)
|
|
16
15
|
rescue LockError
|
|
17
16
|
conflict_instance.on_schedule(&block)
|
|
17
|
+
rescue StandardError
|
|
18
|
+
# Unlock the job if any error arises during scheduling
|
|
19
|
+
job.unlock!
|
|
20
|
+
raise
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
#
|
|
@@ -11,10 +11,13 @@ module Cloudtasker
|
|
|
11
11
|
# if the lock could not be acquired.
|
|
12
12
|
#
|
|
13
13
|
def schedule(&block)
|
|
14
|
-
job.
|
|
15
|
-
yield
|
|
14
|
+
job.lock_for_scheduling!(&block)
|
|
16
15
|
rescue LockError
|
|
17
16
|
conflict_instance.on_schedule(&block)
|
|
17
|
+
rescue StandardError
|
|
18
|
+
# Unlock the job if any error arises during scheduling
|
|
19
|
+
job.unlock!
|
|
20
|
+
raise
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
#
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
module Cloudtasker
|
|
4
4
|
module UniqueJob
|
|
5
5
|
module Middleware
|
|
6
|
-
# TODO: kwargs to job otherwise it won't get the time_at
|
|
7
6
|
# Client middleware, invoked when jobs are scheduled
|
|
8
7
|
class Client
|
|
9
|
-
def call(worker,
|
|
10
|
-
Job.new(worker).lock_instance.schedule(&block)
|
|
8
|
+
def call(worker, opts = {}, &block)
|
|
9
|
+
Job.new(worker, opts).lock_instance.schedule(&block)
|
|
11
10
|
end
|
|
12
11
|
end
|
|
13
12
|
end
|
|
@@ -11,8 +11,12 @@ module Cloudtasker
|
|
|
11
11
|
# after schedule time.
|
|
12
12
|
DEFAULT_LOCK_TTL = 10 * 60 # 10 minutes
|
|
13
13
|
|
|
14
|
+
# The maximum duration of the provisional lock while
|
|
15
|
+
# enqueuing a job.
|
|
16
|
+
DEFAULT_LOCK_PROVISIONAL_TTL = 3
|
|
17
|
+
|
|
14
18
|
class << self
|
|
15
|
-
attr_writer :lock_ttl
|
|
19
|
+
attr_writer :lock_ttl, :lock_provisional_ttl
|
|
16
20
|
|
|
17
21
|
# Configure the middleware
|
|
18
22
|
def configure
|
|
@@ -27,6 +31,15 @@ module Cloudtasker
|
|
|
27
31
|
def lock_ttl
|
|
28
32
|
@lock_ttl || DEFAULT_LOCK_TTL
|
|
29
33
|
end
|
|
34
|
+
|
|
35
|
+
#
|
|
36
|
+
# Return the provisional TTL for locks
|
|
37
|
+
#
|
|
38
|
+
# @return [Integer] The lock TTL.
|
|
39
|
+
#
|
|
40
|
+
def lock_provisional_ttl
|
|
41
|
+
@lock_provisional_ttl || DEFAULT_LOCK_PROVISIONAL_TTL
|
|
42
|
+
end
|
|
30
43
|
end
|
|
31
44
|
end
|
|
32
45
|
end
|
data/lib/cloudtasker/version.rb
CHANGED
data/lib/cloudtasker/worker.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Cloudtasker
|
|
|
8
8
|
base.extend(ClassMethods)
|
|
9
9
|
base.attr_writer :job_queue
|
|
10
10
|
base.attr_accessor :job_args, :job_id, :job_meta, :job_reenqueued, :job_retries,
|
|
11
|
-
:perform_started_at, :perform_ended_at, :task_id
|
|
11
|
+
:job_attempts, :perform_started_at, :perform_ended_at, :task_id
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
#
|
|
@@ -47,7 +47,10 @@ module Cloudtasker
|
|
|
47
47
|
return nil unless worker_klass.include?(self)
|
|
48
48
|
|
|
49
49
|
# Return instantiated worker
|
|
50
|
-
worker_klass.new(**payload.slice(
|
|
50
|
+
worker_klass.new(**payload.slice(
|
|
51
|
+
:job_queue, :job_args, :job_id, :job_meta,
|
|
52
|
+
:job_retries, :job_attempts, :task_id
|
|
53
|
+
))
|
|
51
54
|
rescue NameError
|
|
52
55
|
nil
|
|
53
56
|
end
|
|
@@ -141,7 +144,9 @@ module Cloudtasker
|
|
|
141
144
|
# @return [Any] The result of the worker perform method.
|
|
142
145
|
#
|
|
143
146
|
def perform_now(*args)
|
|
144
|
-
|
|
147
|
+
# Serialize/deserialize arguments to mimic job enqueueing and produce a similar context
|
|
148
|
+
job_args = JSON.parse(args.to_json)
|
|
149
|
+
new(job_args: job_args).execute
|
|
145
150
|
end
|
|
146
151
|
|
|
147
152
|
#
|
|
@@ -174,11 +179,13 @@ module Cloudtasker
|
|
|
174
179
|
# @param [Array<any>] job_args The list of perform args.
|
|
175
180
|
# @param [String] job_id A unique ID identifying this job.
|
|
176
181
|
#
|
|
177
|
-
def initialize(job_queue: nil, job_args: nil, job_id: nil, job_meta: {}, job_retries: 0,
|
|
182
|
+
def initialize(job_queue: nil, job_args: nil, job_id: nil, job_meta: {}, job_retries: 0, job_attempts: 0,
|
|
183
|
+
task_id: nil)
|
|
178
184
|
@job_args = job_args || []
|
|
179
185
|
@job_id = job_id || SecureRandom.uuid
|
|
180
186
|
@job_meta = MetaStore.new(job_meta)
|
|
181
187
|
@job_retries = job_retries || 0
|
|
188
|
+
@job_attempts = job_attempts || 0
|
|
182
189
|
@job_queue = job_queue
|
|
183
190
|
@task_id = task_id
|
|
184
191
|
end
|
|
@@ -243,16 +250,18 @@ module Cloudtasker
|
|
|
243
250
|
resp = execute_middleware_chain
|
|
244
251
|
|
|
245
252
|
# Log job completion and return result
|
|
246
|
-
logger.info("Job done after #{job_duration}s") { { duration:
|
|
253
|
+
logger.info("Job done after #{job_duration}s") { { duration: job_duration_ms } }
|
|
247
254
|
resp
|
|
248
255
|
rescue DeadWorkerError => e
|
|
249
|
-
logger.info("Job dead after #{job_duration}s and #{job_retries} retries") { { duration:
|
|
256
|
+
logger.info("Job dead after #{job_duration}s and #{job_retries} retries") { { duration: job_duration_ms } }
|
|
250
257
|
raise(e)
|
|
251
258
|
rescue RetryWorkerError => e
|
|
252
|
-
logger.info("Job done after #{job_duration}s (retry requested)")
|
|
259
|
+
logger.info("Job done after #{job_duration}s (retry requested)") do
|
|
260
|
+
{ duration: job_duration_ms, reason: e.message }
|
|
261
|
+
end
|
|
253
262
|
raise(e)
|
|
254
263
|
rescue StandardError => e
|
|
255
|
-
logger.info("Job failed after #{job_duration}s") { { duration:
|
|
264
|
+
logger.info("Job failed after #{job_duration}s") { { duration: job_duration_ms } }
|
|
256
265
|
raise(e)
|
|
257
266
|
end
|
|
258
267
|
|
|
@@ -327,6 +336,7 @@ module Cloudtasker
|
|
|
327
336
|
job_args: job_args,
|
|
328
337
|
job_meta: job_meta.to_h,
|
|
329
338
|
job_retries: job_retries,
|
|
339
|
+
job_attempts: job_attempts,
|
|
330
340
|
job_queue: job_queue,
|
|
331
341
|
task_id: task_id
|
|
332
342
|
}
|
|
@@ -418,6 +428,15 @@ module Cloudtasker
|
|
|
418
428
|
@job_duration ||= (perform_ended_at - perform_started_at).ceil(3)
|
|
419
429
|
end
|
|
420
430
|
|
|
431
|
+
#
|
|
432
|
+
# Return the job_duration in milliseconds
|
|
433
|
+
#
|
|
434
|
+
# @return [Float] The time taken in milliseconds as a floating point number.
|
|
435
|
+
#
|
|
436
|
+
def job_duration_ms
|
|
437
|
+
job_duration * 1000
|
|
438
|
+
end
|
|
439
|
+
|
|
421
440
|
#
|
|
422
441
|
# Run worker callback.
|
|
423
442
|
#
|
|
@@ -60,8 +60,8 @@ module Cloudtasker
|
|
|
60
60
|
# Worker will be nil on InvalidWorkerError - in that case we use generic logging
|
|
61
61
|
logger = worker&.logger || Cloudtasker.logger
|
|
62
62
|
|
|
63
|
-
# Log error
|
|
64
|
-
logger.error(error)
|
|
63
|
+
# Log error with duration
|
|
64
|
+
logger.error(error) { { duration: worker&.job_duration_ms }.compact }
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
#
|
|
@@ -92,7 +92,7 @@ module Cloudtasker
|
|
|
92
92
|
args_payload_key = extracted_payload[:args_payload_key]
|
|
93
93
|
|
|
94
94
|
# Build worker
|
|
95
|
-
worker = Cloudtasker::Worker.from_hash(payload) || raise(InvalidWorkerError)
|
|
95
|
+
worker = Cloudtasker::Worker.from_hash(payload) || raise(InvalidWorkerError, payload[:worker])
|
|
96
96
|
|
|
97
97
|
# Yied worker
|
|
98
98
|
resp = yield(worker)
|
|
@@ -178,7 +178,7 @@ module Cloudtasker
|
|
|
178
178
|
#
|
|
179
179
|
def store_payload_in_redis?
|
|
180
180
|
Cloudtasker.config.redis_payload_storage_threshold &&
|
|
181
|
-
worker.job_args.to_json.bytesize > (Cloudtasker.config.redis_payload_storage_threshold *
|
|
181
|
+
worker.job_args.to_json.bytesize > (Cloudtasker.config.redis_payload_storage_threshold * 1000)
|
|
182
182
|
end
|
|
183
183
|
|
|
184
184
|
#
|
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.15.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Arnaud Lachaume
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-06-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -237,7 +237,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
237
237
|
- !ruby/object:Gem::Version
|
|
238
238
|
version: '0'
|
|
239
239
|
requirements: []
|
|
240
|
-
rubygems_version: 3.5.
|
|
240
|
+
rubygems_version: 3.5.22
|
|
241
241
|
signing_key:
|
|
242
242
|
specification_version: 4
|
|
243
243
|
summary: Background jobs for Ruby using Google Cloud Tasks (beta)
|