cloudtasker-tonix 0.1.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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/lint_rubocop.yml +15 -0
  3. data/.github/workflows/test_ruby_3.x.yml +40 -0
  4. data/.gitignore +23 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +96 -0
  7. data/Appraisals +76 -0
  8. data/CHANGELOG.md +248 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +18 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +1311 -0
  13. data/Rakefile +8 -0
  14. data/_config.yml +1 -0
  15. data/app/controllers/cloudtasker/worker_controller.rb +107 -0
  16. data/bin/console +15 -0
  17. data/bin/setup +8 -0
  18. data/cloudtasker.gemspec +42 -0
  19. data/config/routes.rb +5 -0
  20. data/docs/BATCH_JOBS.md +144 -0
  21. data/docs/CRON_JOBS.md +129 -0
  22. data/docs/STORABLE_JOBS.md +68 -0
  23. data/docs/UNIQUE_JOBS.md +190 -0
  24. data/exe/cloudtasker +30 -0
  25. data/gemfiles/.bundle/config +2 -0
  26. data/gemfiles/google_cloud_tasks_1.0.gemfile +17 -0
  27. data/gemfiles/google_cloud_tasks_1.1.gemfile +17 -0
  28. data/gemfiles/google_cloud_tasks_1.2.gemfile +17 -0
  29. data/gemfiles/google_cloud_tasks_1.3.gemfile +17 -0
  30. data/gemfiles/google_cloud_tasks_1.4.gemfile +17 -0
  31. data/gemfiles/google_cloud_tasks_1.5.gemfile +17 -0
  32. data/gemfiles/google_cloud_tasks_2.0.gemfile +17 -0
  33. data/gemfiles/google_cloud_tasks_2.1.gemfile +17 -0
  34. data/gemfiles/rails_6.1.gemfile +20 -0
  35. data/gemfiles/rails_7.0.gemfile +18 -0
  36. data/gemfiles/rails_7.1.gemfile +18 -0
  37. data/gemfiles/rails_8.0.gemfile +18 -0
  38. data/gemfiles/rails_8.1.gemfile +18 -0
  39. data/gemfiles/semantic_logger_3.4.gemfile +16 -0
  40. data/gemfiles/semantic_logger_4.6.gemfile +16 -0
  41. data/gemfiles/semantic_logger_4.7.0.gemfile +16 -0
  42. data/gemfiles/semantic_logger_4.7.2.gemfile +16 -0
  43. data/lib/active_job/queue_adapters/cloudtasker_adapter.rb +89 -0
  44. data/lib/cloudtasker/authentication_error.rb +6 -0
  45. data/lib/cloudtasker/authenticator.rb +90 -0
  46. data/lib/cloudtasker/backend/google_cloud_task_v1.rb +228 -0
  47. data/lib/cloudtasker/backend/google_cloud_task_v2.rb +231 -0
  48. data/lib/cloudtasker/backend/memory_task.rb +202 -0
  49. data/lib/cloudtasker/backend/redis_task.rb +291 -0
  50. data/lib/cloudtasker/batch/batch_progress.rb +142 -0
  51. data/lib/cloudtasker/batch/extension/worker.rb +13 -0
  52. data/lib/cloudtasker/batch/job.rb +558 -0
  53. data/lib/cloudtasker/batch/middleware/server.rb +14 -0
  54. data/lib/cloudtasker/batch/middleware.rb +25 -0
  55. data/lib/cloudtasker/batch.rb +5 -0
  56. data/lib/cloudtasker/cli.rb +194 -0
  57. data/lib/cloudtasker/cloud_task.rb +130 -0
  58. data/lib/cloudtasker/config.rb +319 -0
  59. data/lib/cloudtasker/cron/job.rb +205 -0
  60. data/lib/cloudtasker/cron/middleware/server.rb +14 -0
  61. data/lib/cloudtasker/cron/middleware.rb +20 -0
  62. data/lib/cloudtasker/cron/schedule.rb +308 -0
  63. data/lib/cloudtasker/cron.rb +5 -0
  64. data/lib/cloudtasker/dead_worker_error.rb +6 -0
  65. data/lib/cloudtasker/engine.rb +24 -0
  66. data/lib/cloudtasker/invalid_worker_error.rb +6 -0
  67. data/lib/cloudtasker/local_server.rb +99 -0
  68. data/lib/cloudtasker/max_task_size_exceeded_error.rb +14 -0
  69. data/lib/cloudtasker/meta_store.rb +86 -0
  70. data/lib/cloudtasker/middleware/chain.rb +250 -0
  71. data/lib/cloudtasker/missing_worker_arguments_error.rb +6 -0
  72. data/lib/cloudtasker/redis_client.rb +166 -0
  73. data/lib/cloudtasker/retry_worker_error.rb +6 -0
  74. data/lib/cloudtasker/storable/worker.rb +78 -0
  75. data/lib/cloudtasker/storable.rb +3 -0
  76. data/lib/cloudtasker/testing.rb +184 -0
  77. data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +39 -0
  78. data/lib/cloudtasker/unique_job/conflict_strategy/raise.rb +28 -0
  79. data/lib/cloudtasker/unique_job/conflict_strategy/reject.rb +11 -0
  80. data/lib/cloudtasker/unique_job/conflict_strategy/reschedule.rb +30 -0
  81. data/lib/cloudtasker/unique_job/job.rb +168 -0
  82. data/lib/cloudtasker/unique_job/lock/base_lock.rb +70 -0
  83. data/lib/cloudtasker/unique_job/lock/no_op.rb +11 -0
  84. data/lib/cloudtasker/unique_job/lock/until_completed.rb +40 -0
  85. data/lib/cloudtasker/unique_job/lock/until_executed.rb +36 -0
  86. data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
  87. data/lib/cloudtasker/unique_job/lock/while_executing.rb +25 -0
  88. data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
  89. data/lib/cloudtasker/unique_job/middleware/client.rb +15 -0
  90. data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
  91. data/lib/cloudtasker/unique_job/middleware.rb +36 -0
  92. data/lib/cloudtasker/unique_job.rb +32 -0
  93. data/lib/cloudtasker/version.rb +5 -0
  94. data/lib/cloudtasker/worker.rb +487 -0
  95. data/lib/cloudtasker/worker_handler.rb +250 -0
  96. data/lib/cloudtasker/worker_logger.rb +231 -0
  97. data/lib/cloudtasker/worker_wrapper.rb +52 -0
  98. data/lib/cloudtasker.rb +57 -0
  99. data/lib/tasks/setup_queue.rake +20 -0
  100. metadata +241 -0
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/_config.yml ADDED
@@ -0,0 +1 @@
1
+ theme: jekyll-theme-slate
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ # Handle execution of workers
5
+ class WorkerController < ActionController::Base
6
+ # No need for CSRF verification on API endpoints
7
+ skip_forgery_protection
8
+
9
+ # Authenticate all requests.
10
+ before_action :authenticate!
11
+
12
+ # Return 401 when API Token is invalid
13
+ rescue_from AuthenticationError do
14
+ head :unauthorized
15
+ end
16
+
17
+ # POST /cloudtasker/run
18
+ #
19
+ # Run a worker from a Cloud Task payload
20
+ #
21
+ def run
22
+ # Process payload
23
+ WorkerHandler.execute_from_payload!(payload)
24
+ head :no_content
25
+ rescue DeadWorkerError
26
+ # 205: job will NOT be retried
27
+ head :reset_content
28
+ rescue InvalidWorkerError
29
+ # 404: Job will be retried
30
+ head :not_found
31
+ rescue StandardError
32
+ # 422: Job will be retried
33
+ head 422
34
+ end
35
+
36
+ private
37
+
38
+ #
39
+ # Parse the request body and return the JSON payload
40
+ #
41
+ # @return [String] The JSON payload
42
+ #
43
+ def json_payload
44
+ @json_payload ||= begin
45
+ # Get raw body
46
+ content = request.body.read
47
+
48
+ # Decode content if the body is Base64 encoded
49
+ if request.headers[Cloudtasker::Config::ENCODING_HEADER].to_s.downcase == 'base64'
50
+ content = Base64.decode64(content)
51
+ end
52
+
53
+ # Return the content
54
+ content
55
+ end
56
+ end
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
+
69
+ #
70
+ # Extract the number of times this task failed at runtime.
71
+ #
72
+ # @return [Integer] The number of failures.
73
+ #
74
+ def job_retries
75
+ request.headers[Cloudtasker::Config::RETRY_HEADER].to_i
76
+ end
77
+
78
+ #
79
+ # Return the Google Cloud Task ID from headers.
80
+ #
81
+ # @return [String] The task ID.
82
+ #
83
+ def task_id
84
+ request.headers[Cloudtasker::Config::TASK_ID_HEADER]
85
+ end
86
+
87
+ #
88
+ # Authenticate incoming requests using a bearer token
89
+ #
90
+ # See Cloudtasker::Authenticator#verification_token
91
+ #
92
+ def authenticate!
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.0) 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
105
+ end
106
+ end
107
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'cloudtasker'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'cloudtasker/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'cloudtasker-tonix'
9
+ spec.version = Cloudtasker::VERSION
10
+ spec.authors = ['Meriton Xhymshiti']
11
+ spec.email = ['meritonii1998@gmail.com']
12
+
13
+ spec.summary = 'Fork of keypup-io/cloudtasker with custom changes'
14
+ spec.description = 'Background jobs for Ruby using Google Cloud Tasks (beta)'
15
+ spec.homepage = 'https://github.com/Tonixhymshiti/cloudtasker'
16
+ spec.license = 'MIT'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/Tonixhymshiti/cloudtasker'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/Tonixhymshiti/cloudtasker/master/tree/CHANGELOG.md'
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(examples|test|spec|features)/}) }
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.required_ruby_version = '>= 3.0'
32
+
33
+ spec.add_dependency 'activesupport'
34
+ spec.add_dependency 'connection_pool'
35
+ spec.add_dependency 'fugit'
36
+ spec.add_dependency 'google-cloud-tasks'
37
+ spec.add_dependency 'jwt'
38
+ spec.add_dependency 'redis'
39
+ spec.add_dependency 'retriable'
40
+
41
+ spec.metadata['rubygems_mfa_required'] = 'true'
42
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Cloudtasker::Engine.routes.draw do
4
+ post '/run', to: 'worker#run'
5
+ end
@@ -0,0 +1,144 @@
1
+ # Cloudtasker Batch Jobs
2
+
3
+ **Note**: this extension requires redis
4
+
5
+ The Cloudtasker batch job extension allows to add sub-jobs to regular jobs. This adds the ability to enqueue a list of jobs and track their overall progression as a group of jobs (a "batch"). This extension allows jobs to define callbacks in their worker to track completion of the batch and take actions based on that.
6
+
7
+ ## Configuration
8
+
9
+ You can enable batch jobs by adding the following to your cloudtasker initializer:
10
+ ```ruby
11
+ # The batch job extension is optional and must be explicitly required
12
+ require 'cloudtasker/batch'
13
+
14
+ Cloudtasker.configure do |config|
15
+ # Specify your redis url.
16
+ # Defaults to `redis://localhost:6379/0` if unspecified
17
+ config.redis = { url: 'redis://some-host:6379/0' }
18
+ end
19
+ ```
20
+
21
+ ## Example: Creating a new batch
22
+
23
+ The following example defines a worker that adds itself to the batch with different arguments then monitors the success of the batch.
24
+
25
+ ```ruby
26
+ class BatchWorker
27
+ include Cloudtasker::Worker
28
+
29
+ def perform(level, instance)
30
+ 3.times { |n| batch.add(self.class, level + 1, n) } if level < 2
31
+ end
32
+
33
+ # Invoked when any descendant (e.g. sub-sub job) is complete
34
+ def on_batch_node_complete(child)
35
+ logger.info("Direct or Indirect child complete: #{child.job_id}")
36
+ end
37
+
38
+ # Invoked when a direct descendant is complete
39
+ def on_child_complete(child)
40
+ logger.info("Direct child complete: #{child.job_id}")
41
+ end
42
+
43
+ # Invoked when all chidren have finished
44
+ def on_batch_complete
45
+ Rails.logger.info("Batch complete")
46
+ end
47
+ end
48
+ ```
49
+
50
+ ## Example: Expanding the parent batch
51
+ **Note**: `parent_batch` is available since `0.12.0`
52
+
53
+ ```ruby
54
+ # All the jobs will be attached to the top parent batch.
55
+ class BatchWorker
56
+ include Cloudtasker::Worker
57
+
58
+ def perform(level, instance)
59
+ # Use existing parent_batch or create a new one
60
+ current_batch = parent_batch || batch
61
+
62
+ 3.times { |n| current_batch.add(self.class, level + 1, n) } if level < 2
63
+ end
64
+
65
+ # Invoked when any descendant (e.g. sub-sub job) is complete
66
+ def on_batch_node_complete(child)
67
+ logger.info("Direct or Indirect child complete: #{child.job_id}")
68
+ end
69
+
70
+ # Invoked when a direct descendant is complete
71
+ def on_child_complete(child)
72
+ logger.info("Direct child complete: #{child.job_id}")
73
+ end
74
+
75
+ # Invoked when all chidren have finished
76
+ def on_batch_complete
77
+ Rails.logger.info("Batch complete")
78
+ end
79
+ end
80
+ ```
81
+
82
+ ## Available callbacks
83
+
84
+ The following callbacks are available on your workers to track the progress of the batch:
85
+
86
+ | Callback | Argument | Description |
87
+ |------|-------------|-----------|
88
+ | `on_batch_node_complete` | `The child job` | Invoked when any descendant (e.g. sub-sub job) successfully completes |
89
+ | `on_child_complete` | `The child job` | Invoked when a direct descendant successfully completes |
90
+ | `on_child_error` | `The child job` | Invoked when a child fails |
91
+ | `on_child_dead` | `The child job` | Invoked when a child has exhausted all of its retries |s
92
+ | `on_batch_complete` | none | Invoked when all chidren have finished or died |
93
+
94
+ ## Queue management
95
+
96
+ Jobs added to a batch inherit the queue of the parent. It is possible to specify a different queue when adding a job to a batch using `add_to_queue` batch method.
97
+
98
+ E.g.
99
+
100
+ ```ruby
101
+ def perform
102
+ batch.add_to_queue(:critical, SubWorker, arg1, arg2, arg3)
103
+ end
104
+ ```
105
+
106
+ ## Batch completion
107
+
108
+ Batches complete when all children have successfully completed or died (all retries exhausted).
109
+
110
+ Jobs that fail in a batch will be retried based on the `max_retries` setting configured globally or on the worker itself. The batch will be considered `pending` while workers retry. Therefore it may be a good idea to reduce the number of retries on your workers using `cloudtasker_options max_retries: 5` to ensure your batches don't hang for too long.
111
+
112
+ ## Batch progress tracking
113
+
114
+ You can access progression statistics in callback using `batch.progress`. See the [BatchProgress](../lib/cloudtasker/batch/batch_progress.rb) class for more details.
115
+
116
+ E.g.
117
+ ```ruby
118
+ def on_batch_node_complete(_child_job)
119
+ progress = batch.progress
120
+ logger.info("Total: #{progress.total}")
121
+ logger.info("Completed: #{progress.completed}")
122
+ logger.info("Progress: #{progress.percent.to_i}%")
123
+ end
124
+ ```
125
+
126
+ **Since:** `v0.12.0`
127
+ By default the `progress` method only considers the direct child jobs to evaluate the batch progress. You can pass `depth: somenumber` to the `progress` method to calculate the actual batch progress in a more granular way. Be careful however that this method recursively calculates progress on the sub-batches and is therefore expensive.
128
+
129
+ E.g.
130
+ ```ruby
131
+ def on_batch_node_complete(_child_job)
132
+ # Considers the children for batch progress calculation
133
+ progress_0 = batch.progress # same as batch.progress(depth: 0)
134
+
135
+ # Considers the children and grand-children for batch progress calculation
136
+ progress_1 = batch.progress(depth: 1)
137
+
138
+ # Considers the children, grand-children and grand-grand-children for batch progress calculation
139
+ progress_2 = batch.progress(depth: 3)
140
+
141
+ logger.info("Progress: #{progress_1.percent.to_i}%")
142
+ logger.info("Progress: #{progress_2.percent.to_i}%")
143
+ end
144
+ ```
data/docs/CRON_JOBS.md ADDED
@@ -0,0 +1,129 @@
1
+ # Cloudtasker Cron Jobs
2
+
3
+ **Note**: this extension requires redis
4
+
5
+ The Cloudtasker cron job extension allows you to register workers to run at fixed intervals, using a cron expression. You can validate your cron expressions using [crontab.guru](https://crontab.guru).
6
+
7
+ ## Configuration
8
+
9
+ You can schedule cron jobs by adding the following to your cloudtasker initializer:
10
+ ```ruby
11
+ # The cron job extension is optional and must be explicitly required
12
+ require 'cloudtasker/cron'
13
+
14
+ Cloudtasker.configure do |config|
15
+ # Specify your redis url.
16
+ # Defaults to `redis://localhost:6379/0` if unspecified
17
+ config.redis = { url: 'redis://some-host:6379/0' }
18
+ end
19
+
20
+ # Specify all your cron jobs below. This will synchronize your list of cron jobs (cron jobs previously created and not listed below will be removed).
21
+ unless Rails.env.test?
22
+ Cloudtasker::Cron::Schedule.load_from_hash!(
23
+ # Run job every minute
24
+ some_schedule_name: {
25
+ worker: 'SomeCronWorker',
26
+ cron: '* * * * *'
27
+ },
28
+ # Run job every hour on the fifteenth minute
29
+ other_cron_schedule: {
30
+ worker: 'OtherCronWorker',
31
+ cron: '15 * * * *',
32
+ queue: 'critical'
33
+ args: ['foo', 'bar']
34
+ }
35
+ )
36
+ end
37
+ ```
38
+
39
+ ## Using a configuration file
40
+
41
+ You can maintain the list of cron jobs in a YAML file inside your config folder if you prefer:
42
+ ```yml
43
+ # config/cloudtasker_cron.yml
44
+
45
+ # Run job every minute
46
+ some_schedule_name:
47
+ worker: 'SomeCronWorker'
48
+ cron: '* * * * *'
49
+
50
+ # Run job every hour on the fifteenth minute
51
+ other_cron_schedule:
52
+ worker: 'OtherCronWorker'
53
+ cron: '15 * * * *'
54
+ ```
55
+
56
+ Then register the jobs inside your Cloudtasker initializer this way:
57
+ ```ruby
58
+ # config/initializers/cloudtasker.rb
59
+
60
+ # ... Cloudtasker configuration ...
61
+
62
+ schedule_file = 'config/cloudtasker_cron.yml'
63
+ if File.exist?(schedule_file) && !Rails.env.test?
64
+ Cloudtasker::Cron::Schedule.load_from_hash!(YAML.load_file(schedule_file))
65
+ end
66
+ ```
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
+
91
+ ## Limitations
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.
93
+
94
+ If you need to schedule a job to run on a monthly basis (e.g. on the first of the month), schedule this job to run every day then add the following logic in your job:
95
+ ```ruby
96
+ #
97
+ # Cron schedule (8am UTC every day): 0 8 * * *
98
+ #
99
+ class MyMonthlyWorker
100
+ include Cloudtasker::Worker
101
+
102
+ def perform(*args)
103
+ # Abort unless we're the first of the month
104
+ return unless Time.current.day == 1
105
+
106
+ # ... job logic
107
+ end
108
+ end
109
+ ```
110
+
111
+ The same approach can be used to schedule a job every quarter.
112
+ ```ruby
113
+ #
114
+ # Cron schedule (8am UTC every day): 0 8 * * *
115
+ #
116
+ class MyQuarterlyWorker
117
+ include Cloudtasker::Worker
118
+
119
+ def perform(*args)
120
+ # Abort unless we're the first month of a quarter (Jan, Apr, Jul, Oct)
121
+ return unless Time.current.month == 1
122
+
123
+ # Abort unless we're the first of the month
124
+ return unless Time.current.day == 1
125
+
126
+ # ... job logic
127
+ end
128
+ end
129
+ ```
@@ -0,0 +1,68 @@
1
+ # Cloudtasker Storable Jobs
2
+
3
+ **Supported since**: `v0.14.0`
4
+ **Note**: this extension requires redis
5
+
6
+ The Cloudtasker storage extension allows you to park jobs in a specific garage lane and enqueue (pull) them when specific conditions have been met.
7
+
8
+ This extension is useful when you need to prepare some jobs (e.g. you are retrieving data from an API and must process some of it asynchronously) but only process them when some programmatic conditions have been met (e.g. a series of preliminary preparation jobs have run successfully). Using parked jobs is a leaner (and cheaper) approach than using guard logic in the `perform` method to re-enqueue a job until a set of conditions is satisfied. The latter tends to generate a lot of jobs/logs pollution.
9
+
10
+ ## Configuration
11
+
12
+ You can enable storable jobs by adding the following to your cloudtasker initializer:
13
+ ```ruby
14
+ # The storable extension is optional and must be explicitly required
15
+ require 'cloudtasker/storable'
16
+
17
+ Cloudtasker.configure do |config|
18
+ # Specify your redis url.
19
+ # Defaults to `redis://localhost:6379/0` if unspecified
20
+ config.redis = { url: 'redis://some-host:6379/0' }
21
+ end
22
+ ```
23
+
24
+ Then you can make workers storable by including the `Cloudtasker::Storable::Worker` concern into your workers:
25
+ ```ruby
26
+ class MyWorker
27
+ include Cloudtasker::Worker
28
+ include Cloudtasker::Storable::Worker
29
+
30
+ def perform(...)
31
+ # Do stuff
32
+ end
33
+ end
34
+ ```
35
+
36
+ ## Parking jobs
37
+ You can park jobs to a specific garage lane using the `push_to_store(store_name, *worker_args)` class method:
38
+ ```ruby
39
+ MyWorker.push_to_store('some-customer-reference:some-task-group', job_arg1, job_arg2)
40
+ ```
41
+
42
+ ## Pulling jobs
43
+ You can pull and enqueue jobs using the `pull_all_from_store(store_name)` class method:
44
+ ```ruby
45
+ MyWorker.pull_all_from_store('some-customer-reference:some-task-group')
46
+ ```
47
+
48
+ If you need to enqueue jobs with specific options or using any special means, you can call `pull_all_from_store(store_name)` with a block. When a block is passed the method yield each worker's set of arguments.
49
+ ```ruby
50
+ # Delay the enqueuing of parked jobs by 30 seconds
51
+ MyWorker.pull_all_from_store('some-customer-reference:some-task-group') do |args|
52
+ MyWorker.perform_in(30, *args)
53
+ end
54
+
55
+ # Enqueue parked jobs on a specific queue, with a 10s delay
56
+ MyWorker.pull_all_from_store('some-customer-reference:some-task-group') do |args|
57
+ MyWorker.schedule(args: args, time_in: 10, queue: 'critical')
58
+ end
59
+
60
+ # Enqueue parked jobs as part of a job's current batch (the logic below assumes
61
+ # we are inside a job's `perform` method)
62
+ MyWorker.pull_all_from_store('some-customer-reference:some-task-group') do |args|
63
+ batch.add(MyWorker, *args)
64
+
65
+ # Or with a specific queue
66
+ # batch.add_to_queue('critical', SubWorker, *args)
67
+ end
68
+ ```