cloudtasker 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/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +27 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +247 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +8 -0
- data/app/controllers/cloudtasker/application_controller.rb +6 -0
- data/app/controllers/cloudtasker/worker_controller.rb +38 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cloudtasker.gemspec +48 -0
- data/config/routes.rb +5 -0
- data/lib/cloudtasker.rb +31 -0
- data/lib/cloudtasker/authentication_error.rb +6 -0
- data/lib/cloudtasker/authenticator.rb +55 -0
- data/lib/cloudtasker/batch.rb +5 -0
- data/lib/cloudtasker/batch/batch_progress.rb +97 -0
- data/lib/cloudtasker/batch/config.rb +11 -0
- data/lib/cloudtasker/batch/extension/worker.rb +13 -0
- data/lib/cloudtasker/batch/job.rb +320 -0
- data/lib/cloudtasker/batch/middleware.rb +24 -0
- data/lib/cloudtasker/batch/middleware/server.rb +14 -0
- data/lib/cloudtasker/config.rb +122 -0
- data/lib/cloudtasker/cron.rb +5 -0
- data/lib/cloudtasker/cron/config.rb +11 -0
- data/lib/cloudtasker/cron/job.rb +207 -0
- data/lib/cloudtasker/cron/middleware.rb +21 -0
- data/lib/cloudtasker/cron/middleware/server.rb +14 -0
- data/lib/cloudtasker/cron/schedule.rb +227 -0
- data/lib/cloudtasker/engine.rb +20 -0
- data/lib/cloudtasker/invalid_worker_error.rb +6 -0
- data/lib/cloudtasker/meta_store.rb +86 -0
- data/lib/cloudtasker/middleware/chain.rb +250 -0
- data/lib/cloudtasker/redis_client.rb +115 -0
- data/lib/cloudtasker/task.rb +175 -0
- data/lib/cloudtasker/unique_job.rb +5 -0
- data/lib/cloudtasker/unique_job/config.rb +10 -0
- data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +37 -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 +136 -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_executed.rb +34 -0
- data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
- data/lib/cloudtasker/unique_job/lock/while_executing.rb +23 -0
- data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
- data/lib/cloudtasker/unique_job/middleware.rb +36 -0
- data/lib/cloudtasker/unique_job/middleware/client.rb +14 -0
- data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
- data/lib/cloudtasker/version.rb +5 -0
- data/lib/cloudtasker/worker.rb +211 -0
- metadata +286 -0
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudtasker
|
4
|
+
# Handle execution of workers
|
5
|
+
class WorkerController < ApplicationController
|
6
|
+
# Authenticate all requests.
|
7
|
+
before_action :authenticate!
|
8
|
+
|
9
|
+
# Return 401 when API Token is invalid
|
10
|
+
rescue_from AuthenticationError do
|
11
|
+
head :unauthorized
|
12
|
+
end
|
13
|
+
|
14
|
+
# POST /cloudtasker/run
|
15
|
+
#
|
16
|
+
# Run a worker from a Cloud Task payload
|
17
|
+
#
|
18
|
+
def run
|
19
|
+
Task.execute_from_payload!(request.params.slice(:worker, :args))
|
20
|
+
head :no_content
|
21
|
+
rescue InvalidWorkerError
|
22
|
+
head :not_found
|
23
|
+
rescue StandardError
|
24
|
+
head :unprocessable_entity
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
#
|
30
|
+
# Authenticate incoming requests using a bearer token
|
31
|
+
#
|
32
|
+
# See Cloudtasker::Authenticator#verification_token
|
33
|
+
#
|
34
|
+
def authenticate!
|
35
|
+
Authenticator.verify!(request.headers['Authorization'].to_s.split(' ').last)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
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,48 @@
|
|
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'
|
9
|
+
spec.version = Cloudtasker::VERSION
|
10
|
+
spec.authors = ['Arnaud Lachaume']
|
11
|
+
spec.email = ['arnaud.lachaume@keypup.io']
|
12
|
+
|
13
|
+
spec.summary = 'Manage GCP Cloud Tasks in your app.'
|
14
|
+
spec.description = 'Manage GCP Cloud Tasks in your app.'
|
15
|
+
spec.homepage = 'https://github.com/keypup-io/cloudtasker'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
19
|
+
|
20
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
21
|
+
spec.metadata['source_code_uri'] = 'https://github.com/keypup-io/cloudtasker'
|
22
|
+
spec.metadata['changelog_uri'] = 'https://github.com/keypup-io/cloudtasker/master/tree/CHANGELOG.md'
|
23
|
+
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
27
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
28
|
+
end
|
29
|
+
spec.bindir = 'exe'
|
30
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ['lib']
|
32
|
+
|
33
|
+
spec.add_dependency 'fugit'
|
34
|
+
spec.add_dependency 'google-cloud-tasks'
|
35
|
+
spec.add_dependency 'jwt'
|
36
|
+
spec.add_dependency 'redis'
|
37
|
+
|
38
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
39
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
40
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
41
|
+
spec.add_development_dependency 'rubocop', '0.76.0'
|
42
|
+
spec.add_development_dependency 'rubocop-rspec'
|
43
|
+
spec.add_development_dependency 'timecop'
|
44
|
+
|
45
|
+
spec.add_development_dependency 'rails'
|
46
|
+
spec.add_development_dependency 'rspec-rails'
|
47
|
+
spec.add_development_dependency 'sqlite3'
|
48
|
+
end
|
data/config/routes.rb
ADDED
data/lib/cloudtasker.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cloudtasker/version'
|
4
|
+
require 'cloudtasker/config'
|
5
|
+
|
6
|
+
require 'cloudtasker/authentication_error'
|
7
|
+
require 'cloudtasker/invalid_worker_error'
|
8
|
+
|
9
|
+
require 'cloudtasker/middleware/chain'
|
10
|
+
require 'cloudtasker/authenticator'
|
11
|
+
require 'cloudtasker/task'
|
12
|
+
require 'cloudtasker/meta_store'
|
13
|
+
require 'cloudtasker/worker'
|
14
|
+
|
15
|
+
# Define and manage Cloud Task based workers
|
16
|
+
module Cloudtasker
|
17
|
+
attr_writer :config
|
18
|
+
|
19
|
+
#
|
20
|
+
# Cloudtasker configurator.
|
21
|
+
#
|
22
|
+
def self.configure
|
23
|
+
yield(config)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.config
|
27
|
+
@config ||= Config.new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'cloudtasker/engine' if defined?(::Rails::Engine)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudtasker
|
4
|
+
# Manage token generation and verification
|
5
|
+
module Authenticator
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# Algorithm used to sign the verification token
|
9
|
+
JWT_ALG = 'HS256'
|
10
|
+
|
11
|
+
#
|
12
|
+
# Return the cloudtasker configuration. See Cloudtasker#configure.
|
13
|
+
#
|
14
|
+
# @return [Cloudtasker::Config] The library configuration.
|
15
|
+
#
|
16
|
+
def config
|
17
|
+
Cloudtasker.config
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# A Json Web Token (JWT) which will be used by the processor
|
22
|
+
# to authenticate the job.
|
23
|
+
#
|
24
|
+
# @return [String] The jwt token
|
25
|
+
#
|
26
|
+
def verification_token
|
27
|
+
JWT.encode({ iat: Time.now.to_i }, config.secret, JWT_ALG)
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Verify a bearer token (jwt token)
|
32
|
+
#
|
33
|
+
# @param [String] bearer_token The token to verify.
|
34
|
+
#
|
35
|
+
# @return [Boolean] Return true if the token is valid
|
36
|
+
#
|
37
|
+
def verify(bearer_token)
|
38
|
+
JWT.decode(bearer_token, config.secret)
|
39
|
+
rescue JWT::VerificationError, JWT::DecodeError
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Verify a bearer token and raise a `Cloudtasker::AuthenticationError`
|
45
|
+
# if the token is invalid.
|
46
|
+
#
|
47
|
+
# @param [String] bearer_token The token to verify.
|
48
|
+
#
|
49
|
+
# @return [Boolean] Return true if the token is valid
|
50
|
+
#
|
51
|
+
def verify!(bearer_token)
|
52
|
+
verify(bearer_token) || raise(AuthenticationError)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fugit'
|
4
|
+
|
5
|
+
module Cloudtasker
|
6
|
+
module Batch
|
7
|
+
# Capture the progress of a batch
|
8
|
+
class BatchProgress
|
9
|
+
attr_reader :batch_state
|
10
|
+
|
11
|
+
#
|
12
|
+
# Build a new instance of the class.
|
13
|
+
#
|
14
|
+
# @param [Hash] batch_state The batch state
|
15
|
+
#
|
16
|
+
def initialize(batch_state = {})
|
17
|
+
@batch_state = batch_state
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Return the total number jobs.
|
22
|
+
#
|
23
|
+
# @return [Integer] The number number of jobs.
|
24
|
+
#
|
25
|
+
def total
|
26
|
+
count
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Return the number of completed jobs.
|
31
|
+
#
|
32
|
+
# @return [Integer] The number of completed jobs.
|
33
|
+
#
|
34
|
+
def completed
|
35
|
+
@completed ||= count('completed')
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Return the number of scheduled jobs.
|
40
|
+
#
|
41
|
+
# @return [Integer] The number of scheduled jobs.
|
42
|
+
#
|
43
|
+
def scheduled
|
44
|
+
@scheduled ||= count('scheduled')
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Return the number of processing jobs.
|
49
|
+
#
|
50
|
+
# @return [Integer] The number of processing jobs.
|
51
|
+
#
|
52
|
+
def processing
|
53
|
+
@processing ||= count('processing')
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Return the number of jobs not completed yet.
|
58
|
+
#
|
59
|
+
# @return [Integer] The number of jobs pending.
|
60
|
+
#
|
61
|
+
def pending
|
62
|
+
total - completed
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Return the batch progress percentage.
|
67
|
+
#
|
68
|
+
# @return [Float] The progress percentage.
|
69
|
+
#
|
70
|
+
def percent
|
71
|
+
return 0 if total.zero?
|
72
|
+
|
73
|
+
pending.to_f / total
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Add a batch progress to another one.
|
78
|
+
#
|
79
|
+
# @param [Cloudtasker::Batch::BatchProgress] progress The progress to add.
|
80
|
+
#
|
81
|
+
# @return [Cloudtasker::Batch::BatchProgress] The sum of the two batch progresses.
|
82
|
+
#
|
83
|
+
def +(other)
|
84
|
+
self.class.new(batch_state.to_h.merge(other.batch_state.to_h))
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Count the number of items in a given status
|
90
|
+
def count(status = nil)
|
91
|
+
return batch_state.to_h.keys.size unless status
|
92
|
+
|
93
|
+
batch_state.to_h.values.count { |e| e == status }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudtasker
|
4
|
+
module Batch
|
5
|
+
module Extension
|
6
|
+
# Include batch related methods onto Cloudtasker::Worker
|
7
|
+
# See: Cloudtasker::Batch::Middleware#configure
|
8
|
+
module Worker
|
9
|
+
attr_accessor :batch
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,320 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudtasker
|
4
|
+
module Batch
|
5
|
+
# Handle batch management
|
6
|
+
class Job
|
7
|
+
attr_reader :worker
|
8
|
+
|
9
|
+
#
|
10
|
+
# Return the cloudtasker redis client
|
11
|
+
#
|
12
|
+
# @return [Class] The redis client.
|
13
|
+
#
|
14
|
+
def self.redis
|
15
|
+
RedisClient
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Find a batch by id.
|
20
|
+
#
|
21
|
+
# @param [String] batch_id The batch id.
|
22
|
+
#
|
23
|
+
# @return [Cloudtasker::Batch::Job, nil] The batch.
|
24
|
+
#
|
25
|
+
def self.find(worker_id)
|
26
|
+
return nil unless worker_id
|
27
|
+
|
28
|
+
# Retrieve parent worker
|
29
|
+
parent_payload = redis.fetch(key(worker_id))
|
30
|
+
parent_worker = Cloudtasker::Worker.from_hash(parent_payload)
|
31
|
+
return nil unless parent_worker
|
32
|
+
|
33
|
+
# Build batch job
|
34
|
+
self.for(parent_worker)
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Return a namespaced key.
|
39
|
+
#
|
40
|
+
# @param [String, Symbol] val The key to namespace
|
41
|
+
#
|
42
|
+
# @return [String] The namespaced key.
|
43
|
+
#
|
44
|
+
def self.key(val)
|
45
|
+
return nil if val.nil?
|
46
|
+
|
47
|
+
[Config::KEY_NAMESPACE, val.to_s].join('/')
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Attach a batch to a worker
|
52
|
+
#
|
53
|
+
# @param [Cloudtasker::Worker] worker The worker on which the batch must be attached.
|
54
|
+
#
|
55
|
+
# @return [Cloudtasker::Batch::Job] The attached batch.
|
56
|
+
#
|
57
|
+
def self.for(worker)
|
58
|
+
worker.batch = new(worker)
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Build a new instance of the class.
|
63
|
+
#
|
64
|
+
# @param [Cloudtasker::Worker] worker The batch worker
|
65
|
+
#
|
66
|
+
def initialize(worker)
|
67
|
+
@worker = worker
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Return true if the worker has been re-enqueued.
|
72
|
+
# Post-process logic should be skipped for re-enqueued jobs.
|
73
|
+
#
|
74
|
+
# @return [Boolean] Return true if the job was reequeued.
|
75
|
+
#
|
76
|
+
def reenqueued?
|
77
|
+
worker.job_reenqueued
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Return the cloudtasker redis client
|
82
|
+
#
|
83
|
+
# @return [Class] The redis client.
|
84
|
+
#
|
85
|
+
def redis
|
86
|
+
self.class.redis
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Equality operator.
|
91
|
+
#
|
92
|
+
# @param [Any] other The object to compare.
|
93
|
+
#
|
94
|
+
# @return [Boolean] True if the object is equal.
|
95
|
+
#
|
96
|
+
def ==(other)
|
97
|
+
other.is_a?(self.class) && other.batch_id == batch_id
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Return a namespaced key.
|
102
|
+
#
|
103
|
+
# @param [String, Symbol] val The key to namespace
|
104
|
+
#
|
105
|
+
# @return [String] The namespaced key.
|
106
|
+
#
|
107
|
+
def key(val)
|
108
|
+
self.class.key(val)
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Return the parent batch, if any.
|
113
|
+
#
|
114
|
+
# @return [Cloudtasker::Batch::Job, nil] The parent batch.
|
115
|
+
#
|
116
|
+
def parent_batch
|
117
|
+
return nil unless (parent_id = worker.job_meta.get(key(:parent_id)))
|
118
|
+
|
119
|
+
@parent_batch ||= self.class.find(parent_id)
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
# Return the worker id.
|
124
|
+
#
|
125
|
+
# @return [String] The worker id.
|
126
|
+
#
|
127
|
+
def batch_id
|
128
|
+
worker&.job_id
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# Return the namespaced worker id.
|
133
|
+
#
|
134
|
+
# @return [String] The worker namespaced id.
|
135
|
+
#
|
136
|
+
def batch_gid
|
137
|
+
key(batch_id)
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Return the key under which the batch state is stored.
|
142
|
+
#
|
143
|
+
# @return [String] The batch state namespaced id.
|
144
|
+
#
|
145
|
+
def batch_state_gid
|
146
|
+
[batch_gid, 'state'].join('/')
|
147
|
+
end
|
148
|
+
|
149
|
+
#
|
150
|
+
# The list of jobs in the batch
|
151
|
+
#
|
152
|
+
# @return [Array<Cloudtasker::Worker>] The jobs to enqueue at the end of the batch.
|
153
|
+
#
|
154
|
+
def jobs
|
155
|
+
@jobs ||= []
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# Return the batch state
|
160
|
+
#
|
161
|
+
# @return [Hash] The state of each child worker.
|
162
|
+
#
|
163
|
+
def batch_state
|
164
|
+
redis.fetch(batch_state_gid)
|
165
|
+
end
|
166
|
+
|
167
|
+
#
|
168
|
+
# Add a worker to the batch
|
169
|
+
#
|
170
|
+
# @param [Class] worker_klass The worker class.
|
171
|
+
# @param [Array<any>] *args The worker arguments.
|
172
|
+
#
|
173
|
+
# @return [Array<Cloudtasker::Worker>] The updated list of jobs.
|
174
|
+
#
|
175
|
+
def add(worker_klass, *args)
|
176
|
+
jobs << worker_klass.new(
|
177
|
+
job_args: args,
|
178
|
+
job_meta: { key(:parent_id) => batch_id }
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# Save the batch.
|
184
|
+
#
|
185
|
+
def save
|
186
|
+
# Save serialized version of the worker. This is required to
|
187
|
+
# be able to invoke callback methods in the context of
|
188
|
+
# the worker (= instantiated worker) when child workers
|
189
|
+
# complete (success or failure).
|
190
|
+
redis.write(batch_gid, worker.to_h)
|
191
|
+
|
192
|
+
# Save list of child workers
|
193
|
+
redis.write(batch_state_gid, jobs.map { |e| [e.job_id, 'scheduled'] }.to_h)
|
194
|
+
end
|
195
|
+
|
196
|
+
#
|
197
|
+
# Update the batch state.
|
198
|
+
#
|
199
|
+
# @param [String] job_id The batch id.
|
200
|
+
# @param [String] status The status of the sub-batch.
|
201
|
+
#
|
202
|
+
# @return [<Type>] <description>
|
203
|
+
#
|
204
|
+
def update_state(batch_id, status)
|
205
|
+
redis.with_lock(batch_state_gid) do
|
206
|
+
state = batch_state
|
207
|
+
state[batch_id.to_sym] = status.to_s if state.key?(batch_id.to_sym)
|
208
|
+
redis.write(batch_state_gid, state)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
#
|
213
|
+
# Return true if all the child workers have completed.
|
214
|
+
#
|
215
|
+
# @return [<Type>] <description>
|
216
|
+
#
|
217
|
+
def complete?
|
218
|
+
redis.with_lock(batch_state_gid) do
|
219
|
+
state = redis.fetch(batch_state_gid)
|
220
|
+
return true unless state
|
221
|
+
|
222
|
+
# Check that all children are complete
|
223
|
+
state.values.all? { |e| e == 'completed' }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
#
|
228
|
+
# Callback invoked when a direct child batch is complete.
|
229
|
+
#
|
230
|
+
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
231
|
+
#
|
232
|
+
def on_child_complete(child_batch)
|
233
|
+
# Update batch state
|
234
|
+
update_state(child_batch.batch_id, :completed)
|
235
|
+
|
236
|
+
# Notify the worker that a direct batch child worker has completed
|
237
|
+
worker.try(:on_child_complete, child_batch.worker)
|
238
|
+
|
239
|
+
# Notify the parent batch that we are done with this batch
|
240
|
+
parent_batch&.on_child_complete(self) if complete?
|
241
|
+
end
|
242
|
+
|
243
|
+
#
|
244
|
+
# Callback invoked when any batch in the tree gets completed.
|
245
|
+
#
|
246
|
+
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
247
|
+
#
|
248
|
+
def on_batch_node_complete(child_batch)
|
249
|
+
# Notify the worker that a batch node worker has completed
|
250
|
+
worker.try(:on_batch_node_complete, child_batch.worker)
|
251
|
+
|
252
|
+
# Notify the parent batch that a node is complete
|
253
|
+
parent_batch&.on_batch_node_complete(child_batch)
|
254
|
+
end
|
255
|
+
|
256
|
+
#
|
257
|
+
# Calculate the progress of the batch.
|
258
|
+
#
|
259
|
+
# @return [Cloudtasker::Batch::BatchProgress] The batch progress.
|
260
|
+
#
|
261
|
+
def progress
|
262
|
+
# Capture batch state
|
263
|
+
state = batch_state
|
264
|
+
|
265
|
+
# Sum batch progress of current batch and all sub-batches
|
266
|
+
state.to_h.reduce(BatchProgress.new(state)) do |memo, (child_id, child_status)|
|
267
|
+
memo + (self.class.find(child_id)&.progress || BatchProgress.new(child_id => child_status))
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
#
|
272
|
+
# Save the batch and enqueue all child workers attached to it.
|
273
|
+
#
|
274
|
+
# @return [Array<Google::Cloud::Tasks::V2beta3::Task>] The Google Task responses
|
275
|
+
#
|
276
|
+
def setup
|
277
|
+
return true if jobs.empty?
|
278
|
+
|
279
|
+
# Save batch
|
280
|
+
save
|
281
|
+
|
282
|
+
# Enqueue all child workers
|
283
|
+
jobs.map(&:schedule)
|
284
|
+
end
|
285
|
+
|
286
|
+
#
|
287
|
+
# Post-perform logic. The parent batch is notified if the job is complete.
|
288
|
+
#
|
289
|
+
def complete
|
290
|
+
return true if reenqueued? || jobs.any?
|
291
|
+
|
292
|
+
# Notify the parent batch that a child is complete
|
293
|
+
if complete?
|
294
|
+
worker.try(:on_batch_complete)
|
295
|
+
parent_batch&.on_child_complete(self)
|
296
|
+
end
|
297
|
+
|
298
|
+
# Notify the parent that a batch node has completed
|
299
|
+
parent_batch&.on_batch_node_complete(self)
|
300
|
+
end
|
301
|
+
|
302
|
+
#
|
303
|
+
# Execute the batch.
|
304
|
+
#
|
305
|
+
def execute
|
306
|
+
# Update parent batch state
|
307
|
+
parent_batch&.update_state(batch_id, :processing)
|
308
|
+
|
309
|
+
# Perform job
|
310
|
+
yield
|
311
|
+
|
312
|
+
# Save batch (if child worker has been enqueued)
|
313
|
+
setup
|
314
|
+
|
315
|
+
# Complete batch
|
316
|
+
complete
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|