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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +27 -0
  5. data/.travis.yml +7 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +247 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +43 -0
  11. data/Rakefile +8 -0
  12. data/app/controllers/cloudtasker/application_controller.rb +6 -0
  13. data/app/controllers/cloudtasker/worker_controller.rb +38 -0
  14. data/bin/console +15 -0
  15. data/bin/setup +8 -0
  16. data/cloudtasker.gemspec +48 -0
  17. data/config/routes.rb +5 -0
  18. data/lib/cloudtasker.rb +31 -0
  19. data/lib/cloudtasker/authentication_error.rb +6 -0
  20. data/lib/cloudtasker/authenticator.rb +55 -0
  21. data/lib/cloudtasker/batch.rb +5 -0
  22. data/lib/cloudtasker/batch/batch_progress.rb +97 -0
  23. data/lib/cloudtasker/batch/config.rb +11 -0
  24. data/lib/cloudtasker/batch/extension/worker.rb +13 -0
  25. data/lib/cloudtasker/batch/job.rb +320 -0
  26. data/lib/cloudtasker/batch/middleware.rb +24 -0
  27. data/lib/cloudtasker/batch/middleware/server.rb +14 -0
  28. data/lib/cloudtasker/config.rb +122 -0
  29. data/lib/cloudtasker/cron.rb +5 -0
  30. data/lib/cloudtasker/cron/config.rb +11 -0
  31. data/lib/cloudtasker/cron/job.rb +207 -0
  32. data/lib/cloudtasker/cron/middleware.rb +21 -0
  33. data/lib/cloudtasker/cron/middleware/server.rb +14 -0
  34. data/lib/cloudtasker/cron/schedule.rb +227 -0
  35. data/lib/cloudtasker/engine.rb +20 -0
  36. data/lib/cloudtasker/invalid_worker_error.rb +6 -0
  37. data/lib/cloudtasker/meta_store.rb +86 -0
  38. data/lib/cloudtasker/middleware/chain.rb +250 -0
  39. data/lib/cloudtasker/redis_client.rb +115 -0
  40. data/lib/cloudtasker/task.rb +175 -0
  41. data/lib/cloudtasker/unique_job.rb +5 -0
  42. data/lib/cloudtasker/unique_job/config.rb +10 -0
  43. data/lib/cloudtasker/unique_job/conflict_strategy/base_strategy.rb +37 -0
  44. data/lib/cloudtasker/unique_job/conflict_strategy/raise.rb +28 -0
  45. data/lib/cloudtasker/unique_job/conflict_strategy/reject.rb +11 -0
  46. data/lib/cloudtasker/unique_job/conflict_strategy/reschedule.rb +30 -0
  47. data/lib/cloudtasker/unique_job/job.rb +136 -0
  48. data/lib/cloudtasker/unique_job/lock/base_lock.rb +70 -0
  49. data/lib/cloudtasker/unique_job/lock/no_op.rb +11 -0
  50. data/lib/cloudtasker/unique_job/lock/until_executed.rb +34 -0
  51. data/lib/cloudtasker/unique_job/lock/until_executing.rb +30 -0
  52. data/lib/cloudtasker/unique_job/lock/while_executing.rb +23 -0
  53. data/lib/cloudtasker/unique_job/lock_error.rb +8 -0
  54. data/lib/cloudtasker/unique_job/middleware.rb +36 -0
  55. data/lib/cloudtasker/unique_job/middleware/client.rb +14 -0
  56. data/lib/cloudtasker/unique_job/middleware/server.rb +14 -0
  57. data/lib/cloudtasker/version.rb +5 -0
  58. data/lib/cloudtasker/worker.rb +211 -0
  59. metadata +286 -0
@@ -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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -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
@@ -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__)
@@ -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,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
@@ -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,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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ class AuthenticationError < StandardError
5
+ end
6
+ end
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'batch/middleware'
4
+
5
+ Cloudtasker::Batch::Middleware.configure
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fugit'
4
+
5
+ module Cloudtasker
6
+ module Batch
7
+ class Config
8
+ KEY_NAMESPACE = 'cloudtasker-batch'
9
+ end
10
+ end
11
+ 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