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
@@ -0,0 +1,558 @@
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
+ # Key Namespace used for object saved under this class
10
+ JOBS_NAMESPACE = 'jobs'
11
+ STATES_NAMESPACE = 'states'
12
+
13
+ # List of sub-job statuses taken into account when evaluating
14
+ # if the batch is complete.
15
+ #
16
+ # Batch jobs go through the following states:
17
+ # - scheduled: the parent batch has enqueued a worker for the child job
18
+ # - processing: the child job is running
19
+ # - completed: the child job has completed successfully
20
+ # - errored: the child job has encountered an error and must retry
21
+ # - dead: the child job has exceeded its max number of retries
22
+ #
23
+ # The 'dead' status is considered to be a completion status as it
24
+ # means that the job will never succeed. There is no point in blocking
25
+ # the batch forever so we proceed forward eventually.
26
+ #
27
+ BATCH_STATUSES = %w[scheduled processing completed errored dead all].freeze
28
+ COMPLETION_STATUSES = %w[completed dead].freeze
29
+
30
+ # These callbacks do not need to raise errors on their own
31
+ # because the jobs will be either retried or dropped
32
+ IGNORED_ERRORED_CALLBACKS = %i[on_child_error on_child_dead].freeze
33
+
34
+ # The maximum number of seconds to wait for a batch state lock
35
+ # to be acquired.
36
+ BATCH_MAX_LOCK_WAIT = 60
37
+
38
+ #
39
+ # Return the cloudtasker redis client
40
+ #
41
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
42
+ #
43
+ def self.redis
44
+ @redis ||= RedisClient.new
45
+ end
46
+
47
+ #
48
+ # Find a batch by id.
49
+ #
50
+ # @param [String] batch_id The batch id.
51
+ #
52
+ # @return [Cloudtasker::Batch::Job, nil] The batch.
53
+ #
54
+ def self.find(worker_id)
55
+ return nil unless worker_id
56
+
57
+ # Retrieve related worker
58
+ payload = redis.fetch(key("#{JOBS_NAMESPACE}/#{worker_id}"))
59
+ worker = Cloudtasker::Worker.from_hash(payload)
60
+ return nil unless worker
61
+
62
+ # Build batch job
63
+ self.for(worker)
64
+ end
65
+
66
+ #
67
+ # Return a namespaced key.
68
+ #
69
+ # @param [String, Symbol] val The key to namespace
70
+ #
71
+ # @return [String] The namespaced key.
72
+ #
73
+ def self.key(val)
74
+ return nil if val.nil?
75
+
76
+ [to_s.underscore, val.to_s].join('/')
77
+ end
78
+
79
+ #
80
+ # Attach a batch to a worker
81
+ #
82
+ # @param [Cloudtasker::Worker] worker The worker on which the batch must be attached.
83
+ #
84
+ # @return [Cloudtasker::Batch::Job] The attached batch.
85
+ #
86
+ def self.for(worker)
87
+ # Load extension if not loaded already on the worker class
88
+ worker.class.include(Extension::Worker) unless worker.class <= Extension::Worker
89
+
90
+ # Add batch and parent batch to worker
91
+ worker.batch = new(worker)
92
+ worker.parent_batch = worker.batch.parent_batch
93
+
94
+ # Return the batch
95
+ worker.batch
96
+ end
97
+
98
+ #
99
+ # Build a new instance of the class.
100
+ #
101
+ # @param [Cloudtasker::Worker] worker The batch worker
102
+ #
103
+ def initialize(worker)
104
+ @worker = worker
105
+ end
106
+
107
+ #
108
+ # Return true if the worker has been re-enqueued.
109
+ # Post-process logic should be skipped for re-enqueued jobs.
110
+ #
111
+ # @return [Boolean] Return true if the job was reequeued.
112
+ #
113
+ def reenqueued?
114
+ worker.job_reenqueued
115
+ end
116
+
117
+ #
118
+ # Return the cloudtasker redis client
119
+ #
120
+ # @return [Cloudtasker::RedisClient] The cloudtasker redis client..
121
+ #
122
+ def redis
123
+ self.class.redis
124
+ end
125
+
126
+ #
127
+ # Equality operator.
128
+ #
129
+ # @param [Any] other The object to compare.
130
+ #
131
+ # @return [Boolean] True if the object is equal.
132
+ #
133
+ def ==(other)
134
+ other.is_a?(self.class) && other.batch_id == batch_id
135
+ end
136
+
137
+ #
138
+ # Return a namespaced key.
139
+ #
140
+ # @param [String, Symbol] val The key to namespace
141
+ #
142
+ # @return [String] The namespaced key.
143
+ #
144
+ def key(val)
145
+ self.class.key(val)
146
+ end
147
+
148
+ #
149
+ # Return the parent batch, if any.
150
+ #
151
+ # @return [Cloudtasker::Batch::Job, nil] The parent batch.
152
+ #
153
+ def parent_batch
154
+ return nil unless (parent_id = worker.job_meta.get(key(:parent_id)))
155
+
156
+ @parent_batch ||= self.class.find(parent_id)
157
+ end
158
+
159
+ #
160
+ # Return the worker id.
161
+ #
162
+ # @return [String] The worker id.
163
+ #
164
+ def batch_id
165
+ worker&.job_id
166
+ end
167
+
168
+ #
169
+ # Return the namespaced worker id.
170
+ #
171
+ # @return [String] The worker namespaced id.
172
+ #
173
+ def batch_gid
174
+ key("#{JOBS_NAMESPACE}/#{batch_id}")
175
+ end
176
+
177
+ #
178
+ # Return the key under which the batch state is stored.
179
+ #
180
+ # @return [String] The batch state namespaced id.
181
+ #
182
+ def batch_state_gid
183
+ key("#{STATES_NAMESPACE}/#{batch_id}")
184
+ end
185
+
186
+ #
187
+ # Return the key under which the batch progress is stored
188
+ # for a specific state.
189
+ #
190
+ # @return [String] The batch progress state namespaced id.
191
+ #
192
+ def batch_state_count_gid(state)
193
+ "#{batch_state_gid}/state_count/#{state}"
194
+ end
195
+
196
+ #
197
+ # Return the number of jobs in a given state
198
+ #
199
+ # @return [String] The batch progress state namespaced id.
200
+ #
201
+ def batch_state_count(state)
202
+ redis.get(batch_state_count_gid(state)).to_i
203
+ end
204
+
205
+ #
206
+ # The list of jobs to be enqueued in the batch
207
+ #
208
+ # @return [Array<Cloudtasker::Worker>] The jobs to enqueue at the end of the batch.
209
+ #
210
+ def pending_jobs
211
+ @pending_jobs ||= []
212
+ end
213
+
214
+ #
215
+ # The list of jobs that have been enqueued as part of the batch
216
+ #
217
+ # @return [Array<Cloudtasker::Worker>] The jobs enqueued as part of the batch.
218
+ #
219
+ def enqueued_jobs
220
+ @enqueued_jobs ||= []
221
+ end
222
+
223
+ #
224
+ # Return the batch state
225
+ #
226
+ # @return [Hash] The state of each child worker.
227
+ #
228
+ def batch_state
229
+ migrate_batch_state_to_redis_hash
230
+
231
+ redis.hgetall(batch_state_gid)
232
+ end
233
+
234
+ #
235
+ # Add a worker to the batch
236
+ #
237
+ # @param [Class] worker_klass The worker class.
238
+ # @param [Array<any>] *args The worker arguments.
239
+ #
240
+ # @return [Array<Cloudtasker::Worker>] The updated list of pending jobs.
241
+ #
242
+ def add(worker_klass, *args)
243
+ add_to_queue(worker.job_queue, worker_klass, *args)
244
+ end
245
+
246
+ #
247
+ # Add a worker to the batch using a specific queue.
248
+ #
249
+ # @param [String, Symbol] queue The name of the queue
250
+ # @param [Class] worker_klass The worker class.
251
+ # @param [Array<any>] *args The worker arguments.
252
+ #
253
+ # @return [Array<Cloudtasker::Worker>] The updated list of pending jobs.
254
+ #
255
+ def add_to_queue(queue, worker_klass, *args)
256
+ pending_jobs << worker_klass.new(
257
+ job_args: args,
258
+ job_meta: { key(:parent_id) => batch_id },
259
+ job_queue: queue
260
+ )
261
+ end
262
+
263
+ #
264
+ # This method migrates the batch state to be a Redis hash instead
265
+ # of a hash stored in a string key.
266
+ #
267
+ def migrate_batch_state_to_redis_hash
268
+ return unless redis.type(batch_state_gid) == 'string'
269
+
270
+ # Migrate batch state to Redis hash if it is still using a legacy string key
271
+ # We acquire a lock then check again
272
+ redis.with_lock(batch_state_gid, max_wait: BATCH_MAX_LOCK_WAIT) do
273
+ if redis.type(batch_state_gid) == 'string'
274
+ state = redis.fetch(batch_state_gid)
275
+ redis.del(batch_state_gid)
276
+ redis.hset(batch_state_gid, state) if state.any?
277
+ end
278
+ end
279
+ end
280
+
281
+ #
282
+ # This method initializes the batch job counters if not set already
283
+ #
284
+ def migrate_progress_stats_to_redis_counters
285
+ # Abort if counters have already been set. The 'all' counter acts as a feature flag.
286
+ return if redis.exists?(batch_state_count_gid('all'))
287
+
288
+ # Get all job states
289
+ values = batch_state.values
290
+
291
+ # Count by value
292
+ redis.multi do |m|
293
+ # Per status
294
+ values.tally.each do |k, v|
295
+ m.set(batch_state_count_gid(k), v)
296
+ end
297
+
298
+ # All counter
299
+ m.set(batch_state_count_gid('all'), values.size)
300
+ end
301
+ end
302
+
303
+ #
304
+ # Save serialized version of the worker.
305
+ #
306
+ # This is required to be able to invoke callback methods in the
307
+ # context of the worker (= instantiated worker) when child workers
308
+ # complete (success or failure).
309
+ #
310
+ def save
311
+ redis.write(batch_gid, worker.to_h)
312
+ end
313
+
314
+ #
315
+ # Update the batch state.
316
+ #
317
+ # @param [String] job_id The batch id.
318
+ # @param [String] status The status of the sub-batch.
319
+ #
320
+ def update_state(batch_id, status)
321
+ migrate_batch_state_to_redis_hash
322
+
323
+ # Get current status
324
+ current_status = redis.hget(batch_state_gid, batch_id)
325
+ return if current_status == status.to_s
326
+
327
+ # Update the batch state batch_id entry with the new status
328
+ # and update counters
329
+ redis.multi do |m|
330
+ m.hset(batch_state_gid, batch_id, status)
331
+ m.decr(batch_state_count_gid(current_status))
332
+ m.incr(batch_state_count_gid(status))
333
+ end
334
+ end
335
+
336
+ #
337
+ # Return true if all the child workers have completed.
338
+ #
339
+ # @return [Boolean] True if the batch is complete.
340
+ #
341
+ def complete?
342
+ migrate_batch_state_to_redis_hash
343
+
344
+ # Check that all child jobs have completed
345
+ redis.hvals(batch_state_gid).all? { |e| COMPLETION_STATUSES.include?(e) }
346
+ end
347
+
348
+ #
349
+ # Run worker callback. The error and dead callbacks get
350
+ # silenced should they raise an error.
351
+ #
352
+ # @param [String, Symbol] callback The callback to run.
353
+ # @param [Array<any>] *args The callback arguments.
354
+ #
355
+ # @return [any] The callback return value
356
+ #
357
+ def run_worker_callback(callback, *args)
358
+ worker.try(callback, *args).tap do
359
+ # Enqueue pending jobs if batch was expanded in callback
360
+ # A completed batch cannot receive additional jobs
361
+ schedule_pending_jobs if callback.to_sym != :on_batch_complete
362
+
363
+ # Schedule pending jobs on parent if batch was expanded
364
+ parent_batch&.schedule_pending_jobs
365
+ end
366
+ rescue StandardError => e
367
+ # There is no point in retrying jobs due to failure callbacks failing
368
+ # Only completion callbacks will trigger a re-run of the job because
369
+ # these do matter for batch completion
370
+ raise(e) unless IGNORED_ERRORED_CALLBACKS.include?(callback)
371
+
372
+ # Log error instead
373
+ worker.logger.error(e)
374
+ worker.logger.error("Callback #{callback} failed to run. Skipping to preserve error flow.")
375
+ end
376
+
377
+ #
378
+ # Callback invoked when the batch is complete
379
+ #
380
+ def on_complete(status = :completed)
381
+ # Invoke worker callback
382
+ run_worker_callback(:on_batch_complete) if status == :completed
383
+
384
+ # Propagate event
385
+ parent_batch&.on_child_complete(self, status)
386
+
387
+ # The batch tree is complete. Cleanup the downstream tree.
388
+ cleanup
389
+ end
390
+
391
+ #
392
+ # Callback invoked when a direct child batch is complete.
393
+ #
394
+ # @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
395
+ #
396
+ def on_child_complete(child_batch, status = :completed)
397
+ # Update batch state
398
+ update_state(child_batch.batch_id, status)
399
+
400
+ # Notify the worker that a direct batch child worker has completed
401
+ case status
402
+ when :completed
403
+ run_worker_callback(:on_child_complete, child_batch.worker)
404
+ when :errored
405
+ run_worker_callback(:on_child_error, child_batch.worker)
406
+ when :dead
407
+ run_worker_callback(:on_child_dead, child_batch.worker)
408
+ end
409
+
410
+ # Notify the parent batch that we are done with this batch
411
+ on_complete if status != :errored && complete?
412
+ end
413
+
414
+ #
415
+ # Callback invoked when any batch in the tree gets completed.
416
+ #
417
+ # @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
418
+ #
419
+ def on_batch_node_complete(child_batch, status = :completed)
420
+ return false unless status == :completed
421
+
422
+ # Notify the worker that a batch node worker has completed
423
+ run_worker_callback(:on_batch_node_complete, child_batch.worker)
424
+
425
+ # Notify the parent batch that a node is complete
426
+ parent_batch&.on_batch_node_complete(child_batch)
427
+ end
428
+
429
+ #
430
+ # Remove all batch and sub-batch keys from Redis.
431
+ #
432
+ def cleanup
433
+ migrate_batch_state_to_redis_hash
434
+
435
+ # Delete child batches recursively
436
+ redis.hkeys(batch_state_gid).each { |id| self.class.find(id)&.cleanup }
437
+
438
+ # Delete batch redis entries
439
+ redis.multi do |m|
440
+ m.del(batch_gid)
441
+ m.del(batch_state_gid)
442
+ BATCH_STATUSES.each { |e| m.del(batch_state_count_gid(e)) }
443
+ end
444
+ end
445
+
446
+ #
447
+ # Calculate the progress of the batch.
448
+ #
449
+ # @param [Integer] depth The depth of calculation. Zero (default) means only immediate
450
+ # children will be taken into account.
451
+ #
452
+ # @return [Cloudtasker::Batch::BatchProgress] The batch progress.
453
+ #
454
+ def progress(depth: 0)
455
+ depth = depth.to_i
456
+
457
+ # Initialize counters from batch state. This is only applicable to running batches
458
+ # that started before the counter-based progress was implemented/released.
459
+ migrate_progress_stats_to_redis_counters
460
+
461
+ # Return immediately if we do not need to go down the tree
462
+ return BatchProgress.new([self]) if depth <= 0
463
+
464
+ # Sum batch progress of current batch and sub-batches up to the specified
465
+ # depth
466
+ batch_state.to_h.reduce(BatchProgress.new([self])) do |memo, (child_id, _)|
467
+ memo + (self.class.find(child_id)&.progress(depth: depth - 1) || BatchProgress.new)
468
+ end
469
+ end
470
+
471
+ #
472
+ # Schedule the child workers that were added to the batch
473
+ #
474
+ def schedule_pending_jobs
475
+ ret_list = []
476
+
477
+ while (j = pending_jobs.shift)
478
+ # Schedule the job
479
+ # Skip batch registration if the job was not actually scheduled
480
+ # E.g. the job was evicted due to uniqueness requirements
481
+ next unless j.schedule
482
+
483
+ # Initialize the batch state unless the job has already started (and taken
484
+ # hold of its own status)
485
+ # The batch state is initialized only after the job is scheduled to avoid
486
+ # having never-ending batches - which could occur if a batch was crashing
487
+ # while enqueuing children due to a OOM error and since 'scheduled' is a
488
+ # blocking status.
489
+ redis.multi do |m|
490
+ m.hsetnx(batch_state_gid, j.job_id, 'scheduled')
491
+ m.incr(batch_state_count_gid('scheduled'))
492
+ m.incr(batch_state_count_gid('all'))
493
+ end
494
+
495
+ # Flag job as enqueued
496
+ ret_list << j
497
+ enqueued_jobs << j
498
+ end
499
+
500
+ # Return the list of jobs just enqueued
501
+ ret_list
502
+ end
503
+
504
+ #
505
+ # Save the batch and enqueue all child workers attached to it.
506
+ #
507
+ def setup
508
+ return true if pending_jobs.empty?
509
+
510
+ # Save batch
511
+ save
512
+
513
+ # Schedule all child workers
514
+ schedule_pending_jobs
515
+ end
516
+
517
+ #
518
+ # Post-perform logic. The parent batch is notified if the job is complete.
519
+ #
520
+ def complete(status = :completed)
521
+ return true if reenqueued?
522
+
523
+ # Notify the parent batch that a child is complete
524
+ on_complete(status) if complete?
525
+
526
+ # Notify the parent that a batch node has completed
527
+ parent_batch&.on_batch_node_complete(self, status)
528
+ end
529
+
530
+ #
531
+ # Execute the batch.
532
+ #
533
+ def execute
534
+ # Update parent batch state
535
+ parent_batch&.update_state(batch_id, :processing)
536
+
537
+ # Perform job
538
+ yield
539
+
540
+ # Setup batch
541
+ # Only applicable if the batch has pending_jobs
542
+ setup
543
+
544
+ # Save parent batch if batch was expanded
545
+ parent_batch&.schedule_pending_jobs
546
+
547
+ # Complete batch
548
+ complete(:completed)
549
+ rescue DeadWorkerError => e
550
+ complete(:dead)
551
+ raise(e)
552
+ rescue StandardError => e
553
+ complete(:errored)
554
+ raise(e)
555
+ end
556
+ end
557
+ end
558
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudtasker
4
+ module Batch
5
+ module Middleware
6
+ # Server middleware, invoked when jobs are executed
7
+ class Server
8
+ def call(worker, **_kwargs, &block)
9
+ Job.for(worker).execute(&block)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cloudtasker/redis_client'
4
+
5
+ require_relative 'extension/worker'
6
+ require_relative 'batch_progress'
7
+ require_relative 'job'
8
+
9
+ require_relative 'middleware/server'
10
+
11
+ module Cloudtasker
12
+ module Batch
13
+ # Registration module
14
+ module Middleware
15
+ def self.configure
16
+ Cloudtasker.configure do |config|
17
+ config.server_middleware { |c| c.add(Middleware::Server) }
18
+ end
19
+
20
+ # Inject worker extension on main module
21
+ Cloudtasker::Worker.include(Extension::Worker)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'batch/middleware'
4
+
5
+ Cloudtasker::Batch::Middleware.configure