cloudtasker 0.14.0 → 0.15.rc2

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 (40) 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 +19 -18
  4. data/.gitignore +1 -1
  5. data/.rubocop.yml +15 -12
  6. data/Appraisals +18 -20
  7. data/CHANGELOG.md +12 -1
  8. data/Gemfile +2 -2
  9. data/README.md +56 -0
  10. data/app/controllers/cloudtasker/worker_controller.rb +1 -1
  11. data/cloudtasker.gemspec +1 -1
  12. data/docs/UNIQUE_JOBS.md +1 -0
  13. data/gemfiles/google_cloud_tasks_1.0.gemfile +1 -1
  14. data/gemfiles/google_cloud_tasks_1.1.gemfile +1 -1
  15. data/gemfiles/google_cloud_tasks_1.2.gemfile +1 -1
  16. data/gemfiles/google_cloud_tasks_1.3.gemfile +1 -1
  17. data/gemfiles/google_cloud_tasks_1.4.gemfile +1 -1
  18. data/gemfiles/google_cloud_tasks_1.5.gemfile +1 -1
  19. data/gemfiles/google_cloud_tasks_2.0.gemfile +1 -1
  20. data/gemfiles/google_cloud_tasks_2.1.gemfile +1 -1
  21. data/gemfiles/rails_6.1.gemfile +3 -1
  22. data/gemfiles/rails_7.0.gemfile +1 -1
  23. data/gemfiles/rails_7.1.gemfile +1 -1
  24. data/gemfiles/{rails_5.2.gemfile → rails_8.0.gemfile} +2 -2
  25. data/gemfiles/{rails_6.0.gemfile → rails_8.1.gemfile} +2 -2
  26. data/gemfiles/semantic_logger_3.4.gemfile +1 -1
  27. data/gemfiles/semantic_logger_4.6.gemfile +1 -1
  28. data/gemfiles/semantic_logger_4.7.0.gemfile +1 -1
  29. data/gemfiles/semantic_logger_4.7.2.gemfile +1 -1
  30. data/lib/cloudtasker/backend/memory_task.rb +1 -1
  31. data/lib/cloudtasker/backend/redis_task.rb +3 -3
  32. data/lib/cloudtasker/config.rb +3 -0
  33. data/lib/cloudtasker/redis_client.rb +17 -37
  34. data/lib/cloudtasker/unique_job/lock/until_completed.rb +40 -0
  35. data/lib/cloudtasker/unique_job/middleware.rb +1 -0
  36. data/lib/cloudtasker/version.rb +1 -1
  37. data/lib/cloudtasker/worker.rb +27 -3
  38. data/lib/cloudtasker/worker_logger.rb +2 -2
  39. metadata +6 -6
  40. data/.github/workflows/test_ruby_2.7.yml +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5574bda23d3a128aeb279e7fae03d982cce7788d407d8c627508cb387a1d4d30
4
- data.tar.gz: 24cf8b37d065ce49221d3e118f5579ee8495a09f4ee9be05079536c5c1b497e4
3
+ metadata.gz: 42cff7c50d99de1f66e83e04a3946c7cf370ce03004ee47dc7ee3dcbb46564c8
4
+ data.tar.gz: 26997c5f357a41563a074e2d03ea2724c3824b46a4b034e45b9e5573d4024cbe
5
5
  SHA512:
6
- metadata.gz: f502740061baf6484214dcd23531521e253bbae1194af88b49623734976fd82b0e0e2762a3f4cc5d1585fd8229f6ae4c1262eadeff82a5e3dc0f1c0154e0880f
7
- data.tar.gz: 4b148a1b8cf1b37c02918cf0d4e442e30e34383578538821815453d53656d8f26c873a6b31914cacde63fba9afddbedb3d263fcd959bbfcd09354f2984e67382
6
+ metadata.gz: 0aa102da9cbc6ba0d108eb5d3be521efd954544bc7f4a47c11830401c61d2e590cbad7d7d540448dd4c41df7d11774c663c519a1b044518dc1a7c9f46c2c5906
7
+ data.tar.gz: b415391333360f336f0553297206ce725df0d617a14f3dc0a8fe8c8fb81b7ead8b9fbe92f608ba75214af0469161b2abf1aab65c9b400900a7e5b047d6ed4a18
@@ -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.3.0'
13
+ ruby-version: "3.3.10"
14
14
  bundler-cache: true
15
15
  - run: bundle exec rubocop
@@ -8,25 +8,26 @@ jobs:
8
8
  strategy:
9
9
  matrix:
10
10
  ruby:
11
- - '3.0'
12
- - '3.1'
13
- - '3.2'
14
- - '3.3'
11
+ - "3.0"
12
+ - "3.1"
13
+ - "3.2"
14
+ - "3.3"
15
+ - "3.4"
15
16
  appraisal:
16
- - 'google_cloud_tasks_1.0'
17
- - 'google_cloud_tasks_1.1'
18
- - 'google_cloud_tasks_1.2'
19
- - 'google_cloud_tasks_1.3'
20
- - 'google_cloud_tasks_1.4'
21
- - 'google_cloud_tasks_1.5'
22
- - 'google_cloud_tasks_2.0'
23
- - 'google_cloud_tasks_2.1'
24
- - 'rails_6.1'
25
- - 'rails_7.0'
26
- - 'semantic_logger_3.4'
27
- - 'semantic_logger_4.6'
28
- - 'semantic_logger_4.7.0'
29
- - 'semantic_logger_4.7.2'
17
+ - "google_cloud_tasks_1.0"
18
+ - "google_cloud_tasks_1.1"
19
+ - "google_cloud_tasks_1.2"
20
+ - "google_cloud_tasks_1.3"
21
+ - "google_cloud_tasks_1.4"
22
+ - "google_cloud_tasks_1.5"
23
+ - "google_cloud_tasks_2.0"
24
+ - "google_cloud_tasks_2.1"
25
+ - "rails_6.1"
26
+ - "rails_7.0"
27
+ - "semantic_logger_3.4"
28
+ - "semantic_logger_4.6"
29
+ - "semantic_logger_4.7.0"
30
+ - "semantic_logger_4.7.2"
30
31
  env:
31
32
  BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.appraisal }}.gemfile
32
33
  steps:
data/.gitignore CHANGED
@@ -3,7 +3,7 @@
3
3
  /_yardoc/
4
4
  /coverage/
5
5
  /doc/
6
- /examples/rails/log/*.log
6
+ /examples/*/log/*.log
7
7
  /examples/rails/tmp/
8
8
  /gemfiles/*.gemfile.lock
9
9
  /log/
data/.rubocop.yml CHANGED
@@ -3,10 +3,10 @@ require: rubocop-rspec
3
3
  AllCops:
4
4
  NewCops: enable
5
5
  SuggestExtensions: false
6
- TargetRubyVersion: 2.7
6
+ TargetRubyVersion: 3.0
7
7
  Exclude:
8
- - 'gemfiles/**/*'
9
- - 'vendor/**/*'
8
+ - "gemfiles/**/*"
9
+ - "vendor/**/*"
10
10
 
11
11
  Metrics/ClassLength:
12
12
  Max: 300
@@ -17,7 +17,7 @@ Metrics/ModuleLength:
17
17
  Metrics/AbcSize:
18
18
  Max: 30
19
19
  Exclude:
20
- - 'spec/support/*'
20
+ - "spec/support/*"
21
21
 
22
22
  Metrics/PerceivedComplexity:
23
23
  Max: 20
@@ -30,7 +30,7 @@ Metrics/MethodLength:
30
30
 
31
31
  RSpec/DescribeClass:
32
32
  Exclude:
33
- - 'spec/integration/**/*_spec.rb'
33
+ - "spec/integration/**/*_spec.rb"
34
34
 
35
35
  RSpec/ExpectInHook:
36
36
  Enabled: false
@@ -44,12 +44,15 @@ RSpec/ScatteredSetup:
44
44
  Metrics/BlockLength:
45
45
  Exclude:
46
46
  - cloudtasker.gemspec
47
- - 'spec/**/*'
47
+ - "spec/**/*"
48
48
 
49
49
  Style/Documentation:
50
50
  Exclude:
51
- - 'examples/**/*'
52
- - 'spec/**/*'
51
+ - "examples/**/*"
52
+ - "spec/**/*"
53
+
54
+ Style/SafeNavigationChainLength:
55
+ Enabled: false
53
56
 
54
57
  Metrics/ParameterLists:
55
58
  CountKeywordArgs: false
@@ -59,15 +62,15 @@ Metrics/CyclomaticComplexity:
59
62
 
60
63
  Lint/EmptyBlock:
61
64
  Exclude:
62
- - 'examples/rails/config/routes.rb'
65
+ - "examples/rails/config/routes.rb"
63
66
 
64
67
  RSpec/MessageSpies:
65
68
  Enabled: false
66
69
 
67
70
  RSpec/MultipleExpectations:
68
71
  Exclude:
69
- - 'examples/**/*'
70
- - 'spec/integration/**/*'
72
+ - "examples/**/*"
73
+ - "spec/integration/**/*"
71
74
 
72
75
  RSpec/AnyInstance:
73
76
  Enabled: false
@@ -90,4 +93,4 @@ RSpec/VerifiedDoubles:
90
93
  Exclude:
91
94
  - spec/cloudtasker/cloud_task_spec.rb
92
95
  - spec/cloudtasker/backend/google_cloud_task_v1_spec.rb
93
- - spec/cloudtasker/backend/google_cloud_task_v2_spec.rb
96
+ - spec/cloudtasker/backend/google_cloud_task_v2_spec.rb
data/Appraisals CHANGED
@@ -32,33 +32,31 @@ appraise 'google_cloud_tasks_2.1' do
32
32
  gem 'google-cloud-tasks', '~> 2.1.0'
33
33
  end
34
34
 
35
- if RUBY_VERSION < '3'
36
- appraise 'rails_5.2' do
37
- gem 'rails', '~> 5.2.0'
38
- gem 'rspec-rails'
39
- end
35
+ appraise 'rails_6.1' do
36
+ gem 'rails', '~> 6.1.0'
37
+ gem 'rspec-rails'
38
+ gem 'mutex_m'
39
+ gem 'drb'
40
+ end
40
41
 
41
- appraise 'rails_6.0' do
42
- gem 'rails', '~> 6.0.0'
43
- gem 'rspec-rails'
44
- end
42
+ appraise 'rails_7.0' do
43
+ gem 'rails', '~> 7.0.0'
44
+ gem 'rspec-rails'
45
45
  end
46
46
 
47
- appraise 'rails_6.1' do
48
- gem 'rails', '~> 6.1.0'
47
+ appraise 'rails_7.1' do
48
+ gem 'rails', '~> 7.1'
49
49
  gem 'rspec-rails'
50
50
  end
51
51
 
52
- if RUBY_VERSION >= '2.7'
53
- appraise 'rails_7.0' do
54
- gem 'rails', '~> 7.0.0'
55
- gem 'rspec-rails'
56
- end
52
+ appraise 'rails_8.0' do
53
+ gem 'rails', '~> 8.0'
54
+ gem 'rspec-rails'
55
+ end
57
56
 
58
- appraise 'rails_7.1' do
59
- gem 'rails', '~> 7.1'
60
- gem 'rspec-rails'
61
- end
57
+ appraise 'rails_8.1' do
58
+ gem 'rails', '~> 8.1'
59
+ gem 'rspec-rails'
62
60
  end
63
61
 
64
62
  appraise 'semantic_logger_3.4' do
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.15.rc2](https://github.com/keypup-io/cloudtasker/tree/v0.15.rc2) (2025-11-13)
4
+
5
+ [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.14.0...v0.15.rc2)
6
+
7
+ **Improvements:**
8
+ - 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.
9
+ - Unique Jobs: add `until_completed` strategy to lock jobs until they are completed or have exhausted all retries (thanks @jam-packed).
10
+ - Workers: provide `perform_now` method to perform job inline and align with other job frameworks
11
+
12
+ **Maintenance:**
13
+ - Supported rubies: drop support for ruby `2.7`. Cloudtasker now requires ruby `3.0` and above.
14
+
3
15
  ## [v0.14.0](https://github.com/keypup-io/cloudtasker/tree/v0.14.0) (2025-02-11)
4
16
 
5
17
  [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.13.2...v0.14.0)
@@ -23,7 +35,6 @@
23
35
  - Rails: Use `skip_forgery_protection` instead of `skip_before_action`. The later was causing occasional issues on some setups.
24
36
 
25
37
 
26
-
27
38
  ## [v0.13.2](https://github.com/keypup-io/cloudtasker/tree/v0.13.2) (2023-07-02)
28
39
 
29
40
  [Full Changelog](https://github.com/keypup-io/cloudtasker/compare/v0.13.1...v0.13.2)
data/Gemfile CHANGED
@@ -6,12 +6,12 @@ source 'https://rubygems.org'
6
6
  gemspec
7
7
 
8
8
  # Dev dependencies
9
- gem 'appraisal', github: 'thoughtbot/appraisal'
9
+ gem 'appraisal'
10
10
  gem 'bundler', '~> 2.0'
11
11
  gem 'rake', '>= 12.3.3'
12
12
  gem 'rspec', '~> 3.0'
13
13
  gem 'rspec-json_expectations', '~> 2.2'
14
- gem 'rubocop', '~> 1.64.1'
14
+ gem 'rubocop', '~> 1.75.2'
15
15
  gem 'rubocop-rspec', '~> 3.0.1'
16
16
  gem 'semantic_logger'
17
17
  gem 'timecop'
data/README.md CHANGED
@@ -120,6 +120,10 @@ Open a Rails console and enqueue some jobs
120
120
 
121
121
  # Process job in 60 seconds
122
122
  DummyWorker.perform_in(60, 'foo')
123
+
124
+ # Process job immediately, inline
125
+ # Supported since: v0.15.rc2
126
+ DummyWorker.perform_now('foo')
123
127
  ```
124
128
 
125
129
  Your Rails logs should display the following:
@@ -474,6 +478,11 @@ MyWorker.perform_at(3.days.from_now, arg1, arg2)
474
478
  MyWorker.schedule(args: [arg1, arg2], time_at: Time.parse('2025-01-01 00:50:00Z'), queue: 'critical')
475
479
  # or
476
480
  MyWorker.schedule(args: [arg1, arg2], time_in: 5 * 60, queue: 'critical')
481
+
482
+ # Perform worker immediately, inline. This will not send the job to
483
+ # the processing queue. Middlewares such as Unique Job, Batch Jobs will still be invoked.
484
+ # Supported since: v0.15.rc2
485
+ MyWorker.perform_now(arg1, arg2)
477
486
  ```
478
487
 
479
488
  Cloudtasker also provides a helper for re-enqueuing jobs. Re-enqueued jobs keep the same job id. Some middlewares may rely on this to track the fact that that a job didn't actually complete (e.g. Cloustasker batch). This is optional and you can always fallback to using exception management (raise an error) to retry/re-enqueue jobs.
@@ -545,6 +554,53 @@ Queues can also be assigned at runtime when scheduling a job:
545
554
  CriticalWorker.schedule(args: [1], queue: :important)
546
555
  ```
547
556
 
557
+ ### Propagating the queue in child workers
558
+ **Supported since:** `v0.15.rc2`
559
+
560
+ 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
+
562
+ ```ruby
563
+ # app/workers/child_worker.rb
564
+
565
+ class ChildWorker
566
+ include Cloudtasker::Worker
567
+
568
+ cloudtasker_options queue: :level2
569
+
570
+ def perform(some_arg)
571
+ logger.info("This is a child job, which is set to run on the level2 queue by default.")
572
+ end
573
+ end
574
+ ```
575
+
576
+ ```ruby
577
+ # app/workers/parent_worker.rb
578
+
579
+ class ParentWorker
580
+ include Cloudtasker::Worker
581
+
582
+ cloudtasker_options queue: :level1, propagate_queue: true
583
+
584
+ def perform(some_arg)
585
+ logger.info("This is a parent job, which is set to run on the level1 queue by default.")
586
+
587
+ # This worker will run on queue 'level1' instead of 'level2' because
588
+ # the "propagate_queue: true" has been specified on the parent.
589
+ ChildWorker.perform_async(some_arg)
590
+
591
+ # This worker will run on queue 'level3' because the queue has been explicitly
592
+ # specified on the scheduling options. It overrides the propagate_queue behaviour.
593
+ ChildWorker.schedule(queue: 'level3', args: [some_arg])
594
+
595
+ # This worker will run on queue 'level4' and the first ChildWorker it enqueues
596
+ # will also run on queue 'level4'. The second ChidlWorker will, however, run
597
+ # on queue 'level3', as explained above.
598
+ ParentWorker.schedule(queue: 'level4', args: [some_arg])
599
+ end
600
+ end
601
+ ```
602
+
603
+
548
604
  ## Extensions
549
605
  **Note**: Extensions are not available when using cloudtasker via ActiveJob.
550
606
 
@@ -30,7 +30,7 @@ module Cloudtasker
30
30
  head :not_found
31
31
  rescue StandardError
32
32
  # 422: Job will be retried
33
- head :unprocessable_entity
33
+ head 422
34
34
  end
35
35
 
36
36
  private
data/cloudtasker.gemspec CHANGED
@@ -28,7 +28,7 @@ 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'
31
+ spec.required_ruby_version = '>= 3.0'
32
32
 
33
33
  spec.add_dependency 'activesupport'
34
34
  spec.add_dependency 'connection_pool'
data/docs/UNIQUE_JOBS.md CHANGED
@@ -70,6 +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.rc1`. | `reject` (default) or `raise` |
73
74
 
74
75
  ## Available conflict strategies
75
76
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -14,5 +14,7 @@ gem "timecop"
14
14
  gem "webmock"
15
15
  gem "rails", "~> 6.1.0"
16
16
  gem "rspec-rails"
17
+ gem "mutex_m"
18
+ gem "drb"
17
19
 
18
20
  gemspec path: "../"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -12,7 +12,7 @@ gem "rubocop-rspec", "~> 3.0.1"
12
12
  gem "semantic_logger"
13
13
  gem "timecop"
14
14
  gem "webmock"
15
- gem "rails", "~> 5.2.0"
15
+ gem "rails", "~> 8.0"
16
16
  gem "rspec-rails"
17
17
 
18
18
  gemspec path: "../"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -12,7 +12,7 @@ gem "rubocop-rspec", "~> 3.0.1"
12
12
  gem "semantic_logger"
13
13
  gem "timecop"
14
14
  gem "webmock"
15
- gem "rails", "~> 6.0.0"
15
+ gem "rails", "~> 8.1"
16
16
  gem "rspec-rails"
17
17
 
18
18
  gemspec path: "../"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", github: "thoughtbot/appraisal"
5
+ gem "appraisal"
6
6
  gem "bundler", "~> 2.0"
7
7
  gem "rake", ">= 12.3.3"
8
8
  gem "rspec", "~> 3.0"
@@ -66,7 +66,7 @@ module Cloudtasker
66
66
  payload = payload.merge(schedule_time: payload[:schedule_time].to_i)
67
67
 
68
68
  # Save task
69
- task = new(**payload.merge(id: id))
69
+ task = new(**payload, id: id)
70
70
  queue << task
71
71
 
72
72
  # Execute task immediately if in testing and inline mode enabled
@@ -9,7 +9,7 @@ module Cloudtasker
9
9
  class RedisTask
10
10
  attr_reader :id, :http_request, :schedule_time, :retries, :queue, :dispatch_deadline
11
11
 
12
- RETRY_INTERVAL = 20 # seconds
12
+ RETRY_INTERVAL = Config::LOCAL_SERVER_RETRY_DELAY
13
13
 
14
14
  #
15
15
  # Return the Cloudtasker redis client
@@ -89,7 +89,7 @@ module Cloudtasker
89
89
  # Save job
90
90
  redis.write(key(id), payload)
91
91
  redis.sadd(key, [id])
92
- new(**payload.merge(id: id))
92
+ new(**payload, id: id)
93
93
  end
94
94
 
95
95
  #
@@ -103,7 +103,7 @@ module Cloudtasker
103
103
  gid = key(id)
104
104
  return nil unless (payload = redis.fetch(gid))
105
105
 
106
- new(**payload.merge(id: id))
106
+ new(**payload, id: id)
107
107
  end
108
108
 
109
109
  #
@@ -71,6 +71,9 @@ module Cloudtasker
71
71
  # failures due to the instance being unreachable.
72
72
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
73
73
 
74
+ # How long to wait between retries in local server mode
75
+ LOCAL_SERVER_RETRY_DELAY = (ENV['CLOUDTASKER_LOCAL_RETRY_DELAY'] || 20).to_i # seconds
76
+
74
77
  PROCESSOR_HOST_MISSING = <<~DOC
75
78
  Missing host for processing.
76
79
  Please specify a processor hostname in form of `https://some-public-dns.example.com`'
@@ -132,45 +132,25 @@ module Cloudtasker
132
132
  list
133
133
  end
134
134
 
135
- if RUBY_VERSION < '3'
136
- #
137
- # Delegate all methods to the redis client.
138
- # Old delegation method.
139
- #
140
- # @param [String, Symbol] name The method to delegate.
141
- # @param [Array<any>] *args The list of method positional arguments.
142
- # @param [Hash<any>] *kwargs The list of method keyword arguments.
143
- # @param [Proc] &block Block passed to the method.
144
- #
145
- # @return [Any] The method return value
146
- #
147
- def method_missing(name, *args, &block)
148
- if Redis.method_defined?(name)
149
- client.with { |c| c.send(name, *args, &block) }
150
- else
151
- super
152
- end
153
- end
154
- else
155
- #
156
- # Delegate all methods to the redis client.
157
- # Ruby 3 delegation method style.
158
- #
159
- # @param [String, Symbol] name The method to delegate.
160
- # @param [Array<any>] *args The list of method positional arguments.
161
- # @param [Hash<any>] *kwargs The list of method keyword arguments.
162
- # @param [Proc] &block Block passed to the method.
163
- #
164
- # @return [Any] The method return value
165
- #
166
- def method_missing(name, *args, **kwargs, &block)
167
- if Redis.method_defined?(name)
168
- client.with { |c| c.send(name, *args, **kwargs, &block) }
169
- else
170
- super
171
- end
135
+ #
136
+ # Delegate all methods to the redis client.
137
+ # Ruby 3 delegation method style.
138
+ #
139
+ # @param [String, Symbol] name The method to delegate.
140
+ # @param [Array<any>] *args The list of method positional arguments.
141
+ # @param [Hash<any>] *kwargs The list of method keyword arguments.
142
+ # @param [Proc] &block Block passed to the method.
143
+ #
144
+ # @return [Any] The method return value
145
+ #
146
+ def method_missing(name, ...)
147
+ if Redis.method_defined?(name)
148
+ client.with { |c| c.send(name, ...) }
149
+ else
150
+ super
172
151
  end
173
152
  end
153
+
174
154
  #
175
155
  # Check if the class respond to a certain method.
176
156
  #
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ module UniqueJob
5
+ module Lock
6
+ # Conflict if any other job with the same args is scheduled or moved to execution
7
+ # while the first job is pending or executing. Unlocks only on successful completion
8
+ # or when a DeadWorkerError is raised.
9
+ class UntilCompleted < BaseLock
10
+ #
11
+ # Acquire a lock for the job and trigger a conflict
12
+ # if the lock could not be acquired.
13
+ #
14
+ def schedule(&block)
15
+ job.lock!
16
+ yield
17
+ rescue LockError
18
+ conflict_instance.on_schedule(&block)
19
+ end
20
+
21
+ #
22
+ # Acquire a lock for the job and trigger a conflict
23
+ # if the lock could not be acquired.
24
+ #
25
+ def execute(&block)
26
+ job.lock!
27
+ yield
28
+ # Unlock on successful completion
29
+ job.unlock!
30
+ rescue LockError
31
+ conflict_instance.on_execute(&block)
32
+ rescue Cloudtasker::DeadWorkerError
33
+ # Unlock when DeadWorkerError is raised
34
+ job.unlock!
35
+ raise
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -14,6 +14,7 @@ require_relative 'lock/no_op'
14
14
  require_relative 'lock/until_executed'
15
15
  require_relative 'lock/until_executing'
16
16
  require_relative 'lock/while_executing'
17
+ require_relative 'lock/until_completed'
17
18
 
18
19
  require_relative 'job'
19
20
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cloudtasker
4
- VERSION = '0.14.0'
4
+ VERSION = '0.15.rc2'
5
5
  end
@@ -96,7 +96,7 @@ module Cloudtasker
96
96
  end
97
97
 
98
98
  #
99
- # Enqueue worker in the backgroundf.
99
+ # Enqueue worker in the background.
100
100
  #
101
101
  # @param [Array<any>] *args List of worker arguments
102
102
  #
@@ -130,6 +130,20 @@ module Cloudtasker
130
130
  schedule(args: args, time_at: time_at)
131
131
  end
132
132
 
133
+ #
134
+ # Perform a worker inline, without sending it to the queue for processing.
135
+ #
136
+ # Middlewares (unique job, batch etc.) will still be invoked as if the job
137
+ # had been scheduled.
138
+ #
139
+ # @param [Array<any>] *args List of worker arguments
140
+ #
141
+ # @return [Any] The result of the worker perform method.
142
+ #
143
+ def perform_now(*args)
144
+ new(job_args: args).execute
145
+ end
146
+
133
147
  #
134
148
  # Enqueue a worker with explicity options.
135
149
  #
@@ -184,7 +198,12 @@ module Cloudtasker
184
198
  # @return [String] The name of queue.
185
199
  #
186
200
  def job_queue
187
- (@job_queue ||= self.class.cloudtasker_options_hash[:queue] || Config::DEFAULT_JOB_QUEUE).to_s
201
+ (
202
+ @job_queue ||=
203
+ Thread.current[:cloudtasker_propagated_queue] ||
204
+ self.class.cloudtasker_options_hash[:queue] ||
205
+ Config::DEFAULT_JOB_QUEUE
206
+ ).to_s
188
207
  end
189
208
 
190
209
  #
@@ -215,7 +234,7 @@ module Cloudtasker
215
234
  #
216
235
  # Execute the worker by calling the `perform` with the args.
217
236
  #
218
- # @return [Any] The result of the perform.
237
+ # @return [Any] The result of the worker perform method.
219
238
  #
220
239
  def execute
221
240
  logger.info('Starting job...')
@@ -436,6 +455,10 @@ module Cloudtasker
436
455
  def execute_middleware_chain
437
456
  self.perform_started_at = Time.now
438
457
 
458
+ # Store the parent worker queue so as to propagate it to the child workers
459
+ # See: #job_queue
460
+ Thread.current[:cloudtasker_propagated_queue] = job_queue if self.class.cloudtasker_options_hash[:propagate_queue]
461
+
439
462
  Cloudtasker.config.server_middleware.invoke(self) do
440
463
  # Immediately abort the job if it is already dead
441
464
  flag_as_dead if job_dead?
@@ -454,6 +477,7 @@ module Cloudtasker
454
477
  end
455
478
  ensure
456
479
  self.perform_ended_at = Time.now
480
+ Thread.current[:cloudtasker_propagated_queue] = nil
457
481
  end
458
482
  end
459
483
  end
@@ -184,9 +184,9 @@ module Cloudtasker
184
184
  #
185
185
  # @return [Any] The method return value
186
186
  #
187
- def method_missing(name, *args, &block)
187
+ def method_missing(name, ...)
188
188
  if logger.respond_to?(name)
189
- logger.send(name, *args, &block)
189
+ logger.send(name, ...)
190
190
  else
191
191
  super
192
192
  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.14.0
4
+ version: 0.15.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arnaud Lachaume
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-11 00:00:00.000000000 Z
11
+ date: 2025-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -117,7 +117,6 @@ extensions: []
117
117
  extra_rdoc_files: []
118
118
  files:
119
119
  - ".github/workflows/lint_rubocop.yml"
120
- - ".github/workflows/test_ruby_2.7.yml"
121
120
  - ".github/workflows/test_ruby_3.x.yml"
122
121
  - ".gitignore"
123
122
  - ".rspec"
@@ -149,11 +148,11 @@ files:
149
148
  - gemfiles/google_cloud_tasks_1.5.gemfile
150
149
  - gemfiles/google_cloud_tasks_2.0.gemfile
151
150
  - gemfiles/google_cloud_tasks_2.1.gemfile
152
- - gemfiles/rails_5.2.gemfile
153
- - gemfiles/rails_6.0.gemfile
154
151
  - gemfiles/rails_6.1.gemfile
155
152
  - gemfiles/rails_7.0.gemfile
156
153
  - gemfiles/rails_7.1.gemfile
154
+ - gemfiles/rails_8.0.gemfile
155
+ - gemfiles/rails_8.1.gemfile
157
156
  - gemfiles/semantic_logger_3.4.gemfile
158
157
  - gemfiles/semantic_logger_4.6.gemfile
159
158
  - gemfiles/semantic_logger_4.7.0.gemfile
@@ -201,6 +200,7 @@ files:
201
200
  - lib/cloudtasker/unique_job/job.rb
202
201
  - lib/cloudtasker/unique_job/lock/base_lock.rb
203
202
  - lib/cloudtasker/unique_job/lock/no_op.rb
203
+ - lib/cloudtasker/unique_job/lock/until_completed.rb
204
204
  - lib/cloudtasker/unique_job/lock/until_executed.rb
205
205
  - lib/cloudtasker/unique_job/lock/until_executing.rb
206
206
  - lib/cloudtasker/unique_job/lock/while_executing.rb
@@ -230,7 +230,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
230
230
  requirements:
231
231
  - - ">="
232
232
  - !ruby/object:Gem::Version
233
- version: 2.7.0
233
+ version: '3.0'
234
234
  required_rubygems_version: !ruby/object:Gem::Requirement
235
235
  requirements:
236
236
  - - ">="
@@ -1,38 +0,0 @@
1
- name: Ruby 2.7
2
-
3
- on: [push, pull_request]
4
-
5
- jobs:
6
- build:
7
- runs-on: ubuntu-latest
8
- strategy:
9
- matrix:
10
- ruby:
11
- - '2.7'
12
- appraisal:
13
- - 'google_cloud_tasks_1.0'
14
- - 'google_cloud_tasks_1.1'
15
- - 'google_cloud_tasks_1.2'
16
- - 'google_cloud_tasks_1.3'
17
- - 'google_cloud_tasks_1.4'
18
- - 'google_cloud_tasks_1.5'
19
- - 'google_cloud_tasks_2.0'
20
- - 'google_cloud_tasks_2.1'
21
- - 'rails_5.2'
22
- - 'rails_6.0'
23
- - 'rails_6.1'
24
- - 'rails_7.0'
25
- - 'semantic_logger_3.4'
26
- - 'semantic_logger_4.6'
27
- - 'semantic_logger_4.7.0'
28
- - 'semantic_logger_4.7.2'
29
- env:
30
- BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.appraisal }}.gemfile
31
- steps:
32
- - uses: actions/checkout@v2
33
- - uses: zhulik/redis-action@1.1.0
34
- - uses: ruby/setup-ruby@v1
35
- with:
36
- ruby-version: ${{ matrix.ruby }}
37
- bundler-cache: true
38
- - run: bundle exec rspec