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.
- checksums.yaml +7 -0
- data/.github/workflows/lint_rubocop.yml +15 -0
- data/.github/workflows/test_ruby_3.x.yml +40 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/.rubocop.yml +96 -0
- data/Appraisals +76 -0
- data/CHANGELOG.md +248 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +1311 -0
- data/Rakefile +8 -0
- data/_config.yml +1 -0
- data/app/controllers/cloudtasker/worker_controller.rb +107 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cloudtasker.gemspec +42 -0
- data/config/routes.rb +5 -0
- data/docs/BATCH_JOBS.md +144 -0
- data/docs/CRON_JOBS.md +129 -0
- data/docs/STORABLE_JOBS.md +68 -0
- data/docs/UNIQUE_JOBS.md +190 -0
- data/exe/cloudtasker +30 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/google_cloud_tasks_1.0.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.1.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.2.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.3.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.4.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_1.5.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_2.0.gemfile +17 -0
- data/gemfiles/google_cloud_tasks_2.1.gemfile +17 -0
- data/gemfiles/rails_6.1.gemfile +20 -0
- data/gemfiles/rails_7.0.gemfile +18 -0
- data/gemfiles/rails_7.1.gemfile +18 -0
- data/gemfiles/rails_8.0.gemfile +18 -0
- data/gemfiles/rails_8.1.gemfile +18 -0
- data/gemfiles/semantic_logger_3.4.gemfile +16 -0
- data/gemfiles/semantic_logger_4.6.gemfile +16 -0
- data/gemfiles/semantic_logger_4.7.0.gemfile +16 -0
- data/gemfiles/semantic_logger_4.7.2.gemfile +16 -0
- data/lib/active_job/queue_adapters/cloudtasker_adapter.rb +89 -0
- data/lib/cloudtasker/authentication_error.rb +6 -0
- data/lib/cloudtasker/authenticator.rb +90 -0
- data/lib/cloudtasker/backend/google_cloud_task_v1.rb +228 -0
- data/lib/cloudtasker/backend/google_cloud_task_v2.rb +231 -0
- data/lib/cloudtasker/backend/memory_task.rb +202 -0
- data/lib/cloudtasker/backend/redis_task.rb +291 -0
- data/lib/cloudtasker/batch/batch_progress.rb +142 -0
- data/lib/cloudtasker/batch/extension/worker.rb +13 -0
- data/lib/cloudtasker/batch/job.rb +558 -0
- data/lib/cloudtasker/batch/middleware/server.rb +14 -0
- data/lib/cloudtasker/batch/middleware.rb +25 -0
- data/lib/cloudtasker/batch.rb +5 -0
- data/lib/cloudtasker/cli.rb +194 -0
- data/lib/cloudtasker/cloud_task.rb +130 -0
- data/lib/cloudtasker/config.rb +319 -0
- data/lib/cloudtasker/cron/job.rb +205 -0
- data/lib/cloudtasker/cron/middleware/server.rb +14 -0
- data/lib/cloudtasker/cron/middleware.rb +20 -0
- data/lib/cloudtasker/cron/schedule.rb +308 -0
- data/lib/cloudtasker/cron.rb +5 -0
- data/lib/cloudtasker/dead_worker_error.rb +6 -0
- data/lib/cloudtasker/engine.rb +24 -0
- data/lib/cloudtasker/invalid_worker_error.rb +6 -0
- data/lib/cloudtasker/local_server.rb +99 -0
- data/lib/cloudtasker/max_task_size_exceeded_error.rb +14 -0
- data/lib/cloudtasker/meta_store.rb +86 -0
- data/lib/cloudtasker/middleware/chain.rb +250 -0
- data/lib/cloudtasker/missing_worker_arguments_error.rb +6 -0
- data/lib/cloudtasker/redis_client.rb +166 -0
- data/lib/cloudtasker/retry_worker_error.rb +6 -0
- data/lib/cloudtasker/storable/worker.rb +78 -0
- data/lib/cloudtasker/storable.rb +3 -0
- data/lib/cloudtasker/testing.rb +184 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +39 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/raise.rb +28 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/reject.rb +11 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/reschedule.rb +30 -0
- data/lib/cloudtasker/unique_job/job.rb +168 -0
- data/lib/cloudtasker/unique_job/lock/base_lock.rb +70 -0
- data/lib/cloudtasker/unique_job/lock/no_op.rb +11 -0
- data/lib/cloudtasker/unique_job/lock/until_completed.rb +40 -0
- data/lib/cloudtasker/unique_job/lock/until_executed.rb +36 -0
- data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
- data/lib/cloudtasker/unique_job/lock/while_executing.rb +25 -0
- data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
- data/lib/cloudtasker/unique_job/middleware/client.rb +15 -0
- data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
- data/lib/cloudtasker/unique_job/middleware.rb +36 -0
- data/lib/cloudtasker/unique_job.rb +32 -0
- data/lib/cloudtasker/version.rb +5 -0
- data/lib/cloudtasker/worker.rb +487 -0
- data/lib/cloudtasker/worker_handler.rb +250 -0
- data/lib/cloudtasker/worker_logger.rb +231 -0
- data/lib/cloudtasker/worker_wrapper.rb +52 -0
- data/lib/cloudtasker.rb +57 -0
- data/lib/tasks/setup_queue.rake +20 -0
- metadata +241 -0
data/Rakefile
ADDED
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
data/cloudtasker.gemspec
ADDED
|
@@ -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
data/docs/BATCH_JOBS.md
ADDED
|
@@ -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
|
+
```
|