cloudtasker 0.13.2 → 0.14.rc1

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint_rubocop.yml +1 -1
  3. data/.github/workflows/test_ruby_3.x.yml +1 -0
  4. data/.gitignore +4 -1
  5. data/.rubocop.yml +37 -9
  6. data/Appraisals +0 -12
  7. data/CHANGELOG.md +24 -0
  8. data/Gemfile +12 -0
  9. data/README.md +145 -6
  10. data/app/controllers/cloudtasker/worker_controller.rb +30 -9
  11. data/cloudtasker.gemspec +3 -10
  12. data/docs/CRON_JOBS.md +23 -0
  13. data/docs/STORABLE_JOBS.md +68 -0
  14. data/exe/cloudtasker +5 -2
  15. data/gemfiles/google_cloud_tasks_1.0.gemfile +10 -1
  16. data/gemfiles/google_cloud_tasks_1.1.gemfile +10 -1
  17. data/gemfiles/google_cloud_tasks_1.2.gemfile +10 -1
  18. data/gemfiles/google_cloud_tasks_1.3.gemfile +10 -1
  19. data/gemfiles/google_cloud_tasks_1.4.gemfile +10 -1
  20. data/gemfiles/google_cloud_tasks_1.5.gemfile +10 -1
  21. data/gemfiles/google_cloud_tasks_2.0.gemfile +10 -1
  22. data/gemfiles/google_cloud_tasks_2.1.gemfile +10 -1
  23. data/gemfiles/rails_5.2.gemfile +10 -0
  24. data/gemfiles/rails_6.0.gemfile +10 -0
  25. data/gemfiles/rails_6.1.gemfile +10 -0
  26. data/gemfiles/rails_7.0.gemfile +10 -0
  27. data/gemfiles/semantic_logger_3.4.gemfile +9 -1
  28. data/gemfiles/semantic_logger_4.6.gemfile +9 -1
  29. data/gemfiles/semantic_logger_4.7.0.gemfile +9 -1
  30. data/gemfiles/semantic_logger_4.7.2.gemfile +9 -1
  31. data/lib/active_job/queue_adapters/cloudtasker_adapter.rb +8 -1
  32. data/lib/cloudtasker/authenticator.rb +35 -0
  33. data/lib/cloudtasker/backend/google_cloud_task_v1.rb +2 -4
  34. data/lib/cloudtasker/backend/google_cloud_task_v2.rb +3 -5
  35. data/lib/cloudtasker/backend/memory_task.rb +1 -1
  36. data/lib/cloudtasker/backend/redis_task.rb +10 -4
  37. data/lib/cloudtasker/batch/batch_progress.rb +18 -14
  38. data/lib/cloudtasker/batch/job.rb +124 -31
  39. data/lib/cloudtasker/batch/middleware/server.rb +2 -2
  40. data/lib/cloudtasker/cli.rb +5 -7
  41. data/lib/cloudtasker/cloud_task.rb +16 -20
  42. data/lib/cloudtasker/config.rb +43 -10
  43. data/lib/cloudtasker/cron/middleware/server.rb +2 -2
  44. data/lib/cloudtasker/cron/schedule.rb +5 -2
  45. data/lib/cloudtasker/middleware/chain.rb +1 -1
  46. data/lib/cloudtasker/redis_client.rb +1 -4
  47. data/lib/cloudtasker/retry_worker_error.rb +6 -0
  48. data/lib/cloudtasker/storable/worker.rb +78 -0
  49. data/lib/cloudtasker/storable.rb +3 -0
  50. data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +4 -2
  51. data/lib/cloudtasker/unique_job/lock/until_executed.rb +4 -4
  52. data/lib/cloudtasker/unique_job/lock/until_executing.rb +2 -2
  53. data/lib/cloudtasker/unique_job/lock/while_executing.rb +2 -2
  54. data/lib/cloudtasker/unique_job/middleware/client.rb +2 -2
  55. data/lib/cloudtasker/unique_job/middleware/server.rb +2 -2
  56. data/lib/cloudtasker/version.rb +1 -1
  57. data/lib/cloudtasker/worker.rb +38 -15
  58. data/lib/cloudtasker/worker_handler.rb +25 -19
  59. data/lib/cloudtasker/worker_logger.rb +48 -0
  60. data/lib/cloudtasker.rb +4 -1
  61. data/lib/tasks/setup_queue.rake +6 -6
  62. metadata +9 -145
  63. data/.github/workflows/test_ruby_2.6.yml +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f26efb724bb305aed27041f6e2edbd8ffe82d4b07db39d3e4bf602415a4b7ef
4
- data.tar.gz: 117ea1a8772a4a6512b1198a9429122dad98f379b82d49a6daf926323dc31987
3
+ metadata.gz: ca57b71a4e3552693d0a64720f423bf7ff1168d5078018a0177c65f09ad681d8
4
+ data.tar.gz: f886fe29dec2c4ce12286b3e5af843a02e4ea426fbef6db0d2cfcb300418ea34
5
5
  SHA512:
6
- metadata.gz: 6ce293af0e108913d4f7161ed9a714e4a76bdb7b6078982d00e17d543be1acc826566f7af15652ccec17f4ca2a7420af4651ea78e8ee028159d3e39fab2029a3
7
- data.tar.gz: 66c6bf2443ac5dcd61939eb0cf22a6e0ee87258a2cb2997b8f3905471bdce99eaf0dfd560968e8ba7f61d2220b62ac42f68e25f97eba808e141c0985f64be6e2
6
+ metadata.gz: 8312536629168911d321d0630a126f0671eb9e7e6bdb51c86f675ac159d7294f8a1a87bf89d4b4948dae23f4e9411e5673718bc6edac8b73d10592f1a8527744
7
+ data.tar.gz: a6aeb550354e06413bcd21e3ce47f0b136184c236b4c908104ee1ae51412f4fbcad6cdc3ab392bf991046fe9a1747f32770ca1a0f8d3bddf35dd54813f8b646e
@@ -10,6 +10,6 @@ jobs:
10
10
  - uses: zhulik/redis-action@1.1.0
11
11
  - uses: ruby/setup-ruby@v1
12
12
  with:
13
- ruby-version: '3.0.1'
13
+ ruby-version: '3.3.0'
14
14
  bundler-cache: true
15
15
  - run: bundle exec rubocop
@@ -11,6 +11,7 @@ jobs:
11
11
  - '3.0'
12
12
  - '3.1'
13
13
  - '3.2'
14
+ - '3.3'
14
15
  appraisal:
15
16
  - 'google_cloud_tasks_1.0'
16
17
  - 'google_cloud_tasks_1.1'
data/.gitignore CHANGED
@@ -14,7 +14,10 @@
14
14
  /tmp/
15
15
 
16
16
  # Ignore lock files (e.g. Gemfile.lock)
17
- *.lock
17
+ /Gemfile.lock
18
18
 
19
19
  # rspec failure tracking
20
20
  .rspec_status
21
+
22
+ # Dev databases
23
+ *.sqlite3
data/.rubocop.yml CHANGED
@@ -1,27 +1,28 @@
1
1
  require: rubocop-rspec
2
2
 
3
3
  AllCops:
4
+ NewCops: enable
5
+ SuggestExtensions: false
6
+ TargetRubyVersion: 2.7
4
7
  Exclude:
5
8
  - 'gemfiles/**/*'
6
9
  - 'vendor/**/*'
7
10
 
8
- # Ruby 3.0: curly braces around last argument has meaning
9
- # See: https://github.com/rubocop/rubocop/issues/7641
10
- Style/BracesAroundHashParameters:
11
- Enabled: false
12
-
13
11
  Metrics/ClassLength:
14
- Max: 200
12
+ Max: 300
15
13
 
16
14
  Metrics/ModuleLength:
17
15
  Max: 150
18
16
 
19
17
  Metrics/AbcSize:
20
- Max: 25
18
+ Max: 30
21
19
  Exclude:
22
20
  - 'spec/support/*'
23
21
 
24
- Metrics/LineLength:
22
+ Metrics/PerceivedComplexity:
23
+ Max: 20
24
+
25
+ Layout/LineLength:
25
26
  Max: 120
26
27
 
27
28
  Metrics/MethodLength:
@@ -53,6 +54,13 @@ Style/Documentation:
53
54
  Metrics/ParameterLists:
54
55
  CountKeywordArgs: false
55
56
 
57
+ Metrics/CyclomaticComplexity:
58
+ Max: 15
59
+
60
+ Lint/EmptyBlock:
61
+ Exclude:
62
+ - 'examples/rails/config/routes.rb'
63
+
56
64
  RSpec/MessageSpies:
57
65
  Enabled: false
58
66
 
@@ -62,4 +70,24 @@ RSpec/MultipleExpectations:
62
70
  - 'spec/integration/**/*'
63
71
 
64
72
  RSpec/AnyInstance:
65
- Enabled: false
73
+ Enabled: false
74
+
75
+ RSpec/MultipleMemoizedHelpers:
76
+ Enabled: false
77
+
78
+ RSpec/NoExpectationExample:
79
+ AllowedPatterns:
80
+ - ^expect_
81
+ - ^assert_
82
+
83
+ RSpec/IndexedLet:
84
+ Enabled: false
85
+
86
+ RSpec/StubbedMock:
87
+ Enabled: false
88
+
89
+ RSpec/VerifiedDoubles:
90
+ Exclude:
91
+ - spec/cloudtasker/cloud_task_spec.rb
92
+ - spec/cloudtasker/backend/google_cloud_task_v1_spec.rb
93
+ - spec/cloudtasker/backend/google_cloud_task_v2_spec.rb
data/Appraisals CHANGED
@@ -1,42 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  appraise 'google_cloud_tasks_1.0' do
4
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
5
4
  gem 'google-cloud-tasks', '~> 1.0.0'
6
5
  end
7
6
 
8
7
  appraise 'google_cloud_tasks_1.1' do
9
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
10
8
  gem 'google-cloud-tasks', '~> 1.1.0'
11
9
  end
12
10
 
13
11
  appraise 'google_cloud_tasks_1.2' do
14
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
15
12
  gem 'google-cloud-tasks', '~> 1.2.0'
16
13
  end
17
14
 
18
15
  appraise 'google_cloud_tasks_1.3' do
19
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
20
16
  gem 'google-cloud-tasks', '~> 1.3.0'
21
17
  end
22
18
 
23
19
  appraise 'google_cloud_tasks_1.4' do
24
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
25
20
  gem 'google-cloud-tasks', '~> 1.4.0'
26
21
  end
27
22
 
28
23
  appraise 'google_cloud_tasks_1.5' do
29
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
30
24
  gem 'google-cloud-tasks', '~> 1.5.0'
31
25
  end
32
26
 
33
27
  appraise 'google_cloud_tasks_2.0' do
34
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
35
28
  gem 'google-cloud-tasks', '~> 2.0.0'
36
29
  end
37
30
 
38
31
  appraise 'google_cloud_tasks_2.1' do
39
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
40
32
  gem 'google-cloud-tasks', '~> 2.1.0'
41
33
  end
42
34
 
@@ -65,21 +57,17 @@ if RUBY_VERSION >= '2.7'
65
57
  end
66
58
 
67
59
  appraise 'semantic_logger_3.4' do
68
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
69
60
  gem 'semantic_logger', '3.4.1'
70
61
  end
71
62
 
72
63
  appraise 'semantic_logger_4.6' do
73
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
74
64
  gem 'semantic_logger', '4.6.1'
75
65
  end
76
66
 
77
67
  appraise 'semantic_logger_4.7.0' do
78
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
79
68
  gem 'semantic_logger', '4.7.0'
80
69
  end
81
70
 
82
71
  appraise 'semantic_logger_4.7.2' do
83
- gem 'activesupport', '~> 6.1.0' # ruby 2.6 compatibility
84
72
  gem 'semantic_logger', '4.7.2'
85
73
  end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.14.rc1](https://github.com/keypup-io/cloudtasker/tree/v0.14.rc1) (2024-09-22)
4
+
5
+ [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.13.2...v0.14.rc1)
6
+
7
+ **Improvements:**
8
+ - Authentication: To support OIDC and regular Cloudtasker authentication, we moved the Cloudtasker Authentication header from `Authorization` to `X-Cloudtasker-Authorization`. Backward compatibility is maintained for existing jobs.
9
+ - Authentication: Use signature-based authentication instead of plain tokens. The authentication token now HMACs the content of the job. This approach prevents token from being reused.
10
+ - Batch Jobs: Batch job progress and statistics are now calculated using counters, instead of checking every job. This is much faster.
11
+ - Cron Validation: The cron jobs extension now fails epicly if the cron configuration is invalid, instead of failing silently.
12
+ - GCP OIDC Authentification: It is now possible to specify an Open ID Connect (OIDC) service account to run Cloudtasker on private Cloud Run services. OIDC authentication is provided as an extra authentication layer on top of the regular Cloudtasker authentication system (see below). See the OIDC section in the [initializer documentation](https://github.com/keypup-io/cloudtasker?tab=readme-ov-file#cloudtasker-initializer).
13
+ - Job Execution Control: Add ability to conditionally raise `Cloudtasker::RetryWorkerError` to retry jobs. This error does not get logged but the retry count will still be increased. This is a safer approach than using the `reenqueue` helper, which can lead to forever running jobs if not used properly. [Documentation](https://github.com/keypup-io/cloudtasker?tab=readme-ov-file#conditional-reenqueues-using-retry-errors).
14
+ - Log Arguments Truncation: Add `Cloudtasker::WorkerLogger.truncate` helper to truncate large payloads. This is useful to log the top-level attributes of hash/array payloads, without logging the full depth. This case save you significant $$ in logging costs. [Documentation](https://github.com/keypup-io/cloudtasker?tab=readme-ov-file#truncating-log-arguments).
15
+ - Storable Jobs: Add an interface to park Cloudtasker jobs that need to be conditionally run later. This is useful when you need to capture jobs (and their arguments) during a batch but only enqueue them after the batch is completed. This extension requires Redis and is provided as an optional module. [Documentation](https://github.com/keypup-io/cloudtasker/blob/master/docs/STORABLE_JOBS.md).
16
+ - Local Server: Add ability to disable SSL verification on the local server when local HTTPS endpoints are used. See the `local_server_ssl_verify` section in the [initializer documentation](https://github.com/keypup-io/cloudtasker?tab=readme-ov-file#cloudtasker-initializer).
17
+
18
+ **Fixed bugs:**
19
+ - ActiveJob: Support `enqueue_after_transaction_commit?` to be ISO with the ActiveJob interface.
20
+ - Batch Jobs: Do not register batch jobs that were not actually enqueued due to other factors (e.g. Job Uniqueness extension). This issue could lead to never-ending batches.
21
+ - Duration Logging: Specify the unit (`s` for seconds) on the job duration attribute so it gets properly picked up by GCP Logging. GCP Logging was occasionally mixing up seconds and milliseconds.
22
+ - Job Retry Count: GCP fixed their retry count header some time ago. We now use the `X-CloudTasks-TaskExecutionCount` header instead of the `X-CloudTasks-TaskRetryCount`. [See more details here](https://github.com/keypup-io/cloudtasker?tab=readme-ov-file#max-retries).
23
+ - Rails: Use `skip_forgery_protection` instead of `skip_before_action`. The later was causing occasional issues on some setups.
24
+
25
+
26
+
3
27
  ## [v0.13.2](https://github.com/keypup-io/cloudtasker/tree/v0.13.2) (2023-07-02)
4
28
 
5
29
  [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.13.1...v0.13.2)
data/Gemfile CHANGED
@@ -4,3 +4,15 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in cloudtasker.gemspec
6
6
  gemspec
7
+
8
+ # Dev dependencies
9
+ gem 'appraisal', github: 'thoughtbot/appraisal'
10
+ gem 'bundler', '~> 2.0'
11
+ gem 'rake', '>= 12.3.3'
12
+ gem 'rspec', '~> 3.0'
13
+ gem 'rspec-json_expectations', '~> 2.2'
14
+ gem 'rubocop', '~> 1.64.1'
15
+ gem 'rubocop-rspec', '~> 3.0.1'
16
+ gem 'semantic_logger'
17
+ gem 'timecop'
18
+ gem 'webmock'
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  ![Build Status](https://github.com/keypup-io/cloudtasker/workflows/Test/badge.svg) [![Gem Version](https://badge.fury.io/rb/cloudtasker.svg)](https://badge.fury.io/rb/cloudtasker)
2
2
 
3
+ 🚀🚀🚀 Cloudtasker 0.14 release candidate (`v0.14.rc1`) is out and it's quite big ([Changelog](https://github.com/keypup-io/cloudtasker/blob/master/CHANGELOG.md)). Any help testing this release is welcome, and feel free to open issues if you spot any regression.
4
+
3
5
  # Cloudtasker
4
6
 
5
7
  Background jobs for Ruby using Google Cloud Tasks.
@@ -8,7 +10,7 @@ Cloudtasker provides an easy to manage interface to Google Cloud Tasks for backg
8
10
 
9
11
  Cloudtasker is particularly suited for serverless applications only responding to HTTP requests and where running a dedicated job processing server is not an option (e.g. deploy via [Cloud Run](https://cloud.google.com/run)). All jobs enqueued in Cloud Tasks via Cloudtasker eventually get processed by your application via HTTP requests.
10
12
 
11
- Cloudtasker also provides optional modules for running [cron jobs](docs/CRON_JOBS.md), [batch jobs](docs/BATCH_JOBS.md) and [unique jobs](docs/UNIQUE_JOBS.md).
13
+ Cloudtasker also provides optional modules for running [cron jobs](docs/CRON_JOBS.md), [batch jobs](docs/BATCH_JOBS.md), [unique jobs](docs/UNIQUE_JOBS.md) and [storable jobs](docs/STORABLE_JOBS.md).
12
14
 
13
15
  A local processing server is also available for development. This local server processes jobs in lieu of Cloud Tasks and allows you to work offline.
14
16
 
@@ -31,12 +33,15 @@ A local processing server is also available for development. This local server p
31
33
  8. [Logging](#logging)
32
34
  1. [Configuring a logger](#configuring-a-logger)
33
35
  2. [Logging context](#logging-context)
36
+ 3. [Truncating log arguments](#truncating-log-arguments)
37
+ 4. [Searching logs: Job ID vs Task ID](#searching-logs-job-id-vs-task-id)
34
38
  9. [Error Handling](#error-handling)
35
39
  1. [HTTP Error codes](#http-error-codes)
36
40
  2. [Worker callbacks](#worker-callbacks)
37
41
  3. [Global callbacks](#global-callbacks)
38
42
  4. [Max retries](#max-retries)
39
- 5. [Dispatch deadline](#dispatch-deadline)
43
+ 5. [Conditional reenqueues using retry errors](#conditional-reenqueues-using-retry-errors)
44
+ 6. [Dispatch deadline](#dispatch-deadline)
40
45
  10. [Testing](#testing)
41
46
  1. [Test helper setup](#test-helper-setup)
42
47
  2. [In-memory queues](#in-memory-queues)
@@ -137,7 +142,7 @@ Now jump to the next section to configure your app to use Google Cloud Tasks as
137
142
 
138
143
  ## Get started with Rails & ActiveJob
139
144
  **Note**: ActiveJob is supported since `0.11.0`
140
- **Note**: Cloudtasker extensions (cron, batch and unique jobs) are not available when using cloudtasker via ActiveJob.
145
+ **Note**: Cloudtasker extensions (cron, batch, unique jobs and storable) are not available when using cloudtasker via ActiveJob.
141
146
 
142
147
  Cloudtasker is pre-integrated with ActiveJob. Follow the steps below to get started.
143
148
 
@@ -328,7 +333,8 @@ Cloudtasker.configure do |config|
328
333
  # Specify the redis connection hash.
329
334
  #
330
335
  # This is ONLY required in development for the Cloudtasker local server and in
331
- # all environments if you use any cloudtasker extension (unique jobs, cron jobs or batch jobs)
336
+ # all environments if you use any cloudtasker extension (unique jobs, cron jobs,
337
+ # batch jobs or storable jobs)
332
338
  #
333
339
  # See https://github.com/redis/redis-rb for examples of configuration hashes.
334
340
  #
@@ -370,6 +376,8 @@ Cloudtasker.configure do |config|
370
376
  # Supported since: v0.12.0
371
377
  #
372
378
  # Default: 600 seconds (10 minutes)
379
+ # Min: 15 seconds
380
+ # Max: 1800 seconds (30 minutes)
373
381
  #
374
382
  # config.dispatch_deadline = 600
375
383
 
@@ -402,6 +410,40 @@ Cloudtasker.configure do |config|
402
410
  # Default: no operation
403
411
  #
404
412
  # config.on_dead = ->(error, worker) { Rollbar.error(error) }
413
+
414
+ #
415
+ # Specify the Open ID Connect (OIDC) details to connect to a protected GCP service, such
416
+ # as a private Cloud Run application.
417
+ #
418
+ # The configuration supports the following details:
419
+ # - service_account_email: This is the "act as" user. It can be found under the security details
420
+ # of the Cloud Run service.
421
+ # - audience: The audience is usually the publicly accessible host for the Cloud Run service
422
+ # (which is the same value configured as the processor_host). If no audiences are provided
423
+ # it will be set to the processor_host.
424
+ #
425
+ # Note: If the OIDC token is used for a Cloud Run service make sure to include the
426
+ # `iam.serviceAccounts.actAs` permission on the service account.
427
+ #
428
+ # See https://cloud.google.com/tasks/docs/creating-http-target-tasks#sa for more information on
429
+ # setting up service accounts for use with Cloud Tasks.
430
+ #
431
+ # Supported since: v0.14.rc1
432
+ #
433
+ # Default: nil
434
+ #
435
+ # config.oidc = { service_account_email: 'example@gserviceaccount.com' }
436
+ # config.oidc = { service_account_email: 'example@gserviceaccount.com', audience: 'https://api.example.net' }
437
+
438
+ #
439
+ # Enable/disable the verification of SSL certificates on the local processing server when
440
+ # sending tasks to the processor.
441
+ #
442
+ # Set to false to disable SSL verification (OpenSSL::SSL::VERIFY_NONE).
443
+ #
444
+ # Default: true
445
+ #
446
+ # config.local_server_ssl_verify = true
405
447
  end
406
448
  ```
407
449
 
@@ -449,6 +491,8 @@ class FetchResourceWorker
449
491
  # ...do some logic...
450
492
  if some_condition
451
493
  # Stop and re-enqueue the job to be run again in 10 seconds.
494
+ # Also see the section on Cloudtasker::RetryWorkerError for a different
495
+ # approach on reenqueuing.
452
496
  return reenqueue(10)
453
497
  else
454
498
  # ...keep going...
@@ -510,6 +554,7 @@ Cloudtasker comes with three optional features:
510
554
  - Cron Jobs [[docs](docs/CRON_JOBS.md)]: Run jobs at fixed intervals.
511
555
  - Batch Jobs [[docs](docs/BATCH_JOBS.md)]: Run jobs in jobs and track completion of the overall batch.
512
556
  - Unique Jobs [[docs](docs/UNIQUE_JOBS.md)]: Ensure uniqueness of jobs based on job arguments.
557
+ - Storable Jobs [[docs](docs/STORABLE_JOBS.md)]: Park jobs until they are ready to be enqueued.
513
558
 
514
559
  ## Working locally
515
560
 
@@ -652,6 +697,60 @@ end
652
697
 
653
698
  See the [Cloudtasker::Worker class](lib/cloudtasker/worker.rb) for more information on attributes available to be logged in your `log_context_processor` proc.
654
699
 
700
+ ### Truncating log arguments
701
+ **Supported since**: `v0.14.rc1`
702
+
703
+ By default Cloudtasker does not log job arguments as arguments can contain sensitive data and generate voluminous logs, which may lead to noticeable costs with your log provider (e.g. GCP Logging). Also some providers (e.g. GCP Logging) will automatically truncate log entries that are too big and reduce their searchability.
704
+
705
+ Job arguments can be logged for all workers by configuring the following log context processor in your Cloudtasker initializer:
706
+ ```ruby
707
+ Cloudtasker::WorkerLogger.log_context_processor = ->(worker) { worker.to_h }
708
+ ```
709
+
710
+ In order to reduce the size of logged job arguments, the following `truncate` utility is provided by Cloudtasker:
711
+ ```ruby
712
+ # string_limit: The maximum size for strings. Default is 64. Set to -1 to disable.
713
+ # array_limit: The maximum length for arrays. Default is 10. Set to -1 to disable.
714
+ # max_depth: The maximum recursive depth. Default is 3. Set to -1 to disable.
715
+ Cloudtasker::WorkerLogger.truncate(payload, string_limit: 64, array_limit: 10, max_depth: 3)
716
+ ```
717
+
718
+ You may use it the following way:
719
+ ```ruby
720
+ Cloudtasker::WorkerLogger.log_context_processor = lambda do |worker|
721
+ payload = worker.to_h
722
+
723
+ # Using default options
724
+ payload[:job_args] = Cloudtasker::WorkerLogger.truncate(payload[:job_args])
725
+
726
+ # Using custom options
727
+ # payload[:job_args] = Cloudtasker::WorkerLogger.truncate(payload[:job_args], string_limit: 32, array_limit: 5, max_depth: 2)
728
+
729
+ # Return the payload to log
730
+ payload
731
+ end
732
+ ```
733
+
734
+ To further reduce logging cost, you may also log a reasonably complete version of job arguments at start then log a watered down version for the remaining log entries:
735
+ ```ruby
736
+ Cloudtasker::WorkerLogger.log_context_processor = lambda do |worker|
737
+ payload = worker.to_h
738
+
739
+ # Adjust the log payload based on the lifecycle of the job
740
+ payload[:job_args] = if worker.perform_started_at
741
+ # The job start has already been logged. Log the job primitive arguments without depth.
742
+ # Arrays and hashes will be masked.
743
+ Cloudtasker::WorkerLogger.truncate(payload[:job_args], max_depth: 0)
744
+ else
745
+ # This is the job start. Log a more complete version of the job args.
746
+ Cloudtasker::WorkerLogger.truncate(payload[:job_args])
747
+ end
748
+
749
+ # Return the payload to log
750
+ payload
751
+ end
752
+ ```
753
+
655
754
  ### Searching logs: Job ID vs Task ID
656
755
  **Note**: `task_id` field is available in logs starting with `0.10.0`
657
756
 
@@ -735,7 +834,7 @@ By default jobs are retried 25 times - using an exponential backoff - before bei
735
834
 
736
835
  Note that the number of retries set on your Cloud Task queue should be many times higher than the number of retries configured in Cloudtasker because Cloud Task also includes failures to connect to your application. Ideally set the number of retries to `unlimited` in Cloud Tasks.
737
836
 
738
- **Note**: The `X-CloudTasks-TaskExecutionCount` header sent by Google Cloud Tasks and providing the number of retries outside of `HTTP 503` (instance not reachable) is currently bugged and remains at `0` all the time. Starting with `v0.10.0` Cloudtasker uses the `X-CloudTasks-TaskRetryCount` header to detect the number of retries. This header includes `HTTP 503` errors which means that if your application is down at some point, jobs will fail and these failures will be counted toward the maximum number of retries. A [bug report](https://issuetracker.google.com/issues/154532072) has been raised with GCP to address this issue. Once fixed we will revert to using `X-CloudTasks-TaskExecutionCount` to avoid counting `HTTP 503` as job failures.
837
+ **Note**: Versions prior to `v0.14.rc1` use the `X-CloudTasks-TaskRetryCount` header for retries instead of the `X-CloudTasks-TaskExecutionCount` header to detect the number of retries, because there a previous bug on the GCP side which made the `X-CloudTasks-TaskExecutionCount` stay at zero instead of increasing on successive executions. Versions prior to `v0.14.rc1` count any failure as failure, including failures due to the backend being unavailable (`HTTP 503`). Versions `v0.14.rc1` and later only count application failure (`HTTP 4xx`) as failure for retry purpose.
739
838
 
740
839
  E.g. Set max number of retries globally via the cloudtasker initializer.
741
840
  ```ruby
@@ -791,6 +890,46 @@ class SomeErrorWorker
791
890
  end
792
891
  ```
793
892
 
893
+ ### Conditional reenqueues using retry errors
894
+ **Supported since**: `v0.14.rc1`
895
+
896
+ If your worker is waiting for some precondition to occur and you want to re-enqueue it until the condition has been met, you can raise a `Cloudtasker::RetryWorkerError`. This special error will fail your job **without logging an error** while still increasing the number of retries.
897
+
898
+ This is a safer approach than using the `reenqueue` helper, which can lead to forever running jobs if not used properly.
899
+
900
+ ```ruby
901
+ # app/workers/my_worker.rb
902
+
903
+ class MyWorker
904
+ include Cloudtasker::Worker
905
+
906
+ def perform(project_id)
907
+ # Abort if project does not exist
908
+ return unless (project = Project.find_by(id: project_id))
909
+
910
+ # Trigger a retry if the project is still in "discovering" status
911
+ # This error will NOT log an error. It only triggers a retry.
912
+ raise Cloudtasker::RetryWorkerError if project.status == 'discovering'
913
+
914
+ # The previous approach was to use `reenqueue`. This works but since it
915
+ # does not increase the number of retries, you may end up with forever running
916
+ # jobs
917
+ # return reenqueue(10) if project.status == 'discovering'
918
+
919
+ # Do stuff when project is not longer discovering
920
+ do_some_stuff
921
+ end
922
+
923
+ # You can then specify what should be done if we've been waiting for too long
924
+ def on_dead(error)
925
+ logger.error("Looks like the project is forever discovering. Time to give up.")
926
+
927
+ # This is of course an imaginary method
928
+ send_slack_notification_to_internal_support_team(worker: self.class, args: job_args)
929
+ end
930
+ end
931
+ ```
932
+
794
933
  ### Dispatch deadline
795
934
  **Supported since**: `0.12.0`
796
935
 
@@ -1092,7 +1231,7 @@ To size the concurrency of your queues you should therefore take the most limiti
1092
1231
  After checking out the repo, run `bin/setup` to install dependencies.
1093
1232
 
1094
1233
  For tests, run `rake` to run the tests. Note that Rails is not in context by default, which means Rails-specific test will not run.
1095
- For tests including Rails-specific tests, run `bundle exec appraisal rails-7.0 rake`
1234
+ For tests including Rails-specific tests, run `bundle exec appraisal rails_7.0 rake`
1096
1235
  For all context-specific tests (incl. Rails), run the [appraisal tests](Appraisals) using `bundle exec appraisal rake`.
1097
1236
 
1098
1237
  You can run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -4,7 +4,7 @@ module Cloudtasker
4
4
  # Handle execution of workers
5
5
  class WorkerController < ActionController::Base
6
6
  # No need for CSRF verification on API endpoints
7
- skip_before_action :verify_authenticity_token
7
+ skip_forgery_protection
8
8
 
9
9
  # Authenticate all requests.
10
10
  before_action :authenticate!
@@ -36,13 +36,12 @@ module Cloudtasker
36
36
  private
37
37
 
38
38
  #
39
- # Parse the request body and return the actual job
40
- # payload.
39
+ # Parse the request body and return the JSON payload
41
40
  #
42
- # @return [Hash] The job payload
41
+ # @return [String] The JSON payload
43
42
  #
44
- def payload
45
- @payload ||= begin
43
+ def json_payload
44
+ @json_payload ||= begin
46
45
  # Get raw body
47
46
  content = request.body.read
48
47
 
@@ -51,11 +50,22 @@ module Cloudtasker
51
50
  content = Base64.decode64(content)
52
51
  end
53
52
 
54
- # Return content parsed as JSON and add job retries count
55
- JSON.parse(content).merge(job_retries: job_retries, task_id: task_id)
53
+ # Return the content
54
+ content
56
55
  end
57
56
  end
58
57
 
58
+ #
59
+ # Parse the request body and return the actual job
60
+ # payload.
61
+ #
62
+ # @return [Hash] The job payload
63
+ #
64
+ def payload
65
+ # Return content parsed as JSON and add job retries count
66
+ @payload ||= JSON.parse(json_payload).merge(job_retries: job_retries, task_id: task_id)
67
+ end
68
+
59
69
  #
60
70
  # Extract the number of times this task failed at runtime.
61
71
  #
@@ -80,7 +90,18 @@ module Cloudtasker
80
90
  # See Cloudtasker::Authenticator#verification_token
81
91
  #
82
92
  def authenticate!
83
- Authenticator.verify!(request.headers['Authorization'].to_s.split(' ').last)
93
+ if (signature = request.headers[Cloudtasker::Config::CT_SIGNATURE_HEADER])
94
+ # Verify content signature
95
+ Authenticator.verify_signature!(signature, json_payload)
96
+ else
97
+ # Get authorization token from custom header (since v0.14.rc1) or fallback to
98
+ # former authorization header (jobs enqueued by v0.13 and below)
99
+ bearer_token = request.headers[Cloudtasker::Config::CT_AUTHORIZATION_HEADER].to_s.split.last ||
100
+ request.headers[Cloudtasker::Config::OIDC_AUTHORIZATION_HEADER].to_s.split.last
101
+
102
+ # Verify the token
103
+ Authenticator.verify!(bearer_token)
104
+ end
84
105
  end
85
106
  end
86
107
  end
data/cloudtasker.gemspec CHANGED
@@ -28,6 +28,8 @@ Gem::Specification.new do |spec|
28
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
29
  spec.require_paths = ['lib']
30
30
 
31
+ spec.required_ruby_version = '>= 2.7.0'
32
+
31
33
  spec.add_dependency 'activesupport'
32
34
  spec.add_dependency 'connection_pool'
33
35
  spec.add_dependency 'fugit'
@@ -36,14 +38,5 @@ Gem::Specification.new do |spec|
36
38
  spec.add_dependency 'redis'
37
39
  spec.add_dependency 'retriable'
38
40
 
39
- spec.add_development_dependency 'appraisal'
40
- spec.add_development_dependency 'bundler', '~> 2.0'
41
- spec.add_development_dependency 'rake', '>= 12.3.3'
42
- spec.add_development_dependency 'rspec', '~> 3.0'
43
- spec.add_development_dependency 'rspec-json_expectations', '~> 2.2'
44
- spec.add_development_dependency 'rubocop', '0.76.0'
45
- spec.add_development_dependency 'rubocop-rspec', '1.37.0'
46
- spec.add_development_dependency 'semantic_logger'
47
- spec.add_development_dependency 'timecop'
48
- spec.add_development_dependency 'webmock'
41
+ spec.metadata['rubygems_mfa_required'] = 'true'
49
42
  end
data/docs/CRON_JOBS.md CHANGED
@@ -65,6 +65,29 @@ if File.exist?(schedule_file) && !Rails.env.test?
65
65
  end
66
66
  ```
67
67
 
68
+ ## With Puma Cluster-mode
69
+ Due to this issue with gRPC here: https://github.com/grpc/grpc/issues/7951.
70
+
71
+ TLTR:
72
+ > Forking processes and using gRPC across processes is not supported behavior due to very low-level resource issues. Either delay your use of gRPC until you've forked from fresh processes (similar to Python 3's use of a zygote process), or don't expect things to work after a fork.
73
+
74
+ In order to make it works, we should schedule cron jobs (which triggers gPRC calls) once puma is booted.
75
+
76
+ Example:
77
+ ```ruby
78
+ config/puma.rb
79
+
80
+ workers ENV.fetch("WEB_CONCURRENCY") { 2 }
81
+ preload_app!
82
+
83
+ on_booted do
84
+ schedule_file = "config/cloudtasker_cron.yml"
85
+ if File.exist?(schedule_file) && !Rails.env.test?
86
+ Cloudtasker::Cron::Schedule.load_from_hash!(YAML.load_file(schedule_file))
87
+ end
88
+ end
89
+ ```
90
+
68
91
  ## Limitations
69
92
  GCP Cloud Tasks does not allow tasks to be scheduled more than 30 days (720h) in the future. Cron schedules should therefore be limited to 30 days intervals at most.
70
93