good_job 3.8.0 → 3.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -1
- data/README.md +195 -3
- data/app/controllers/good_job/batches_controller.rb +12 -0
- data/app/filters/good_job/batches_filter.rb +21 -0
- data/app/models/good_job/batch.rb +141 -0
- data/app/models/good_job/batch_record.rb +84 -0
- data/app/models/good_job/execution.rb +73 -39
- data/app/models/good_job/job.rb +1 -0
- data/app/views/good_job/batches/_jobs.erb +108 -0
- data/app/views/good_job/batches/_table.erb +61 -0
- data/app/views/good_job/batches/index.html.erb +16 -0
- data/app/views/good_job/batches/show.html.erb +30 -0
- data/app/views/good_job/shared/_navbar.erb +7 -0
- data/config/routes.rb +2 -0
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +19 -0
- data/lib/generators/good_job/templates/update/migrations/04_create_good_job_batches.rb.erb +34 -0
- data/lib/good_job/active_job_extensions/batches.rb +13 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +50 -33
- data/lib/good_job/adapter.rb +69 -1
- data/lib/good_job/bulk.rb +124 -0
- data/lib/good_job/log_subscriber.rb +1 -1
- data/lib/good_job/notifier.rb +27 -23
- data/lib/good_job/scheduler.rb +13 -0
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +17 -6
- metadata +15 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 22496fe725c6ad616c39f243cf8f53381dee64a5402d3a199c8c604bff2c4eb3
|
4
|
+
data.tar.gz: 6d9791dd28f28ddf9317f2770029323754fa32c60403f336f23db8020cdecc51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e182b3aa78b38fd75baa5e0c14704baa99b7aa98f34ff011006543ce7a356e6eae8ac45e605d65d985b8631f6f185666f78e5bb1be1f39feebd62c159535a7e5
|
7
|
+
data.tar.gz: acf8a1f601e07142b0dbaea2f2e229fa3d9b7f8c50dd6232531f8ff50cf888d61f88f2a02ae423ffaa8a9a6533bd1cd0522e7cfa849abb20db8dc538adf75ae3
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,38 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v3.10.0](https://github.com/bensheldon/good_job/tree/v3.10.0) (2023-02-04)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.9.0...v3.10.0)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Implement `GoodJob::Batch` [\#712](https://github.com/bensheldon/good_job/pull/712) ([bensheldon](https://github.com/bensheldon))
|
10
|
+
|
11
|
+
**Closed issues:**
|
12
|
+
|
13
|
+
- Support for Ruby 3.2 [\#785](https://github.com/bensheldon/good_job/issues/785)
|
14
|
+
- Custom table names [\#748](https://github.com/bensheldon/good_job/issues/748)
|
15
|
+
- Health check issue with cron scheduler job [\#741](https://github.com/bensheldon/good_job/issues/741)
|
16
|
+
|
17
|
+
## [v3.9.0](https://github.com/bensheldon/good_job/tree/v3.9.0) (2023-01-31)
|
18
|
+
|
19
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.8.0...v3.9.0)
|
20
|
+
|
21
|
+
**Implemented enhancements:**
|
22
|
+
|
23
|
+
- Abort enqueue when the concurrency limit is reached [\#820](https://github.com/bensheldon/good_job/pull/820) ([TAGraves](https://github.com/TAGraves))
|
24
|
+
- Add bulk enqueue functionality [\#790](https://github.com/bensheldon/good_job/pull/790) ([julik](https://github.com/julik))
|
25
|
+
|
26
|
+
**Merged pull requests:**
|
27
|
+
|
28
|
+
- Bump alex-page/github-project-automation-plus from 0.8.2 to 0.8.3 [\#819](https://github.com/bensheldon/good_job/pull/819) ([dependabot[bot]](https://github.com/apps/dependabot))
|
29
|
+
- Bump concurrent-ruby from 1.1.10 to 1.2.0 [\#818](https://github.com/bensheldon/good_job/pull/818) ([dependabot[bot]](https://github.com/apps/dependabot))
|
30
|
+
- Bump rails from 6.1.7 to 6.1.7.2 [\#817](https://github.com/bensheldon/good_job/pull/817) ([dependabot[bot]](https://github.com/apps/dependabot))
|
31
|
+
- Bump selenium-webdriver from 4.7.1 to 4.8.0 [\#816](https://github.com/bensheldon/good_job/pull/816) ([dependabot[bot]](https://github.com/apps/dependabot))
|
32
|
+
- Bump rubocop from 1.43.0 to 1.44.1 [\#815](https://github.com/bensheldon/good_job/pull/815) ([dependabot[bot]](https://github.com/apps/dependabot))
|
33
|
+
- Ensure that anytime the Notifier uses autoloaded constants \(ActiveRecord\), they are wrapped with a Rails Executor [\#797](https://github.com/bensheldon/good_job/pull/797) ([bensheldon](https://github.com/bensheldon))
|
34
|
+
- Remove support for Ruby 2.5 and JRuby 9.2; reactivate appraisal tests for Rails HEAD [\#756](https://github.com/bensheldon/good_job/pull/756) ([bensheldon](https://github.com/bensheldon))
|
35
|
+
|
3
36
|
## [v3.8.0](https://github.com/bensheldon/good_job/tree/v3.8.0) (2023-01-27)
|
4
37
|
|
5
38
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.7.4...v3.8.0)
|
@@ -68,13 +101,16 @@
|
|
68
101
|
|
69
102
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.7.1...v3.7.2)
|
70
103
|
|
104
|
+
**Fixed bugs:**
|
105
|
+
|
106
|
+
- Ignore ActiveJob::DeserializationError when discarding jobs [\#771](https://github.com/bensheldon/good_job/pull/771) ([nickcampbell18](https://github.com/nickcampbell18))
|
107
|
+
|
71
108
|
**Closed issues:**
|
72
109
|
|
73
110
|
- Unable to discard failed jobs which crashed with `ActiveJob::DeserializationError` [\#770](https://github.com/bensheldon/good_job/issues/770)
|
74
111
|
|
75
112
|
**Merged pull requests:**
|
76
113
|
|
77
|
-
- Ignore ActiveJob::DeserializationError when discarding jobs [\#771](https://github.com/bensheldon/good_job/pull/771) ([nickcampbell18](https://github.com/nickcampbell18))
|
78
114
|
- Bump rubocop from 1.39.0 to 1.40.0 [\#769](https://github.com/bensheldon/good_job/pull/769) ([dependabot[bot]](https://github.com/apps/dependabot))
|
79
115
|
|
80
116
|
## [v3.7.1](https://github.com/bensheldon/good_job/tree/v3.7.1) (2022-12-12)
|
data/README.md
CHANGED
@@ -41,9 +41,11 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
41
41
|
- [Dashboard](#dashboard)
|
42
42
|
- [API-only Rails applications](#api-only-rails-applications)
|
43
43
|
- [Live Polling](#live-polling)
|
44
|
-
- [
|
44
|
+
- [Concurrency controls](#concurrency-controls)
|
45
45
|
- [How concurrency controls work](#how-concurrency-controls-work)
|
46
46
|
- [Cron-style repeating/recurring jobs](#cron-style-repeatingrecurring-jobs)
|
47
|
+
- [Bulk enqueue](#bulk-enqueue)
|
48
|
+
- [Batches](#batches)
|
47
49
|
- [Updating](#updating)
|
48
50
|
- [Upgrading minor versions](#upgrading-minor-versions)
|
49
51
|
- [Upgrading v2 to v3](#upgrading-v2-to-v3)
|
@@ -146,7 +148,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
146
148
|
## Compatibility
|
147
149
|
|
148
150
|
- **Ruby on Rails:** 6.0+
|
149
|
-
- **Ruby:** Ruby 2.
|
151
|
+
- **Ruby:** Ruby 2.6+. JRuby 9.3+
|
150
152
|
- **Postgres:** 10.0+
|
151
153
|
|
152
154
|
## Configuration
|
@@ -391,7 +393,7 @@ end
|
|
391
393
|
|
392
394
|
The Dashboard can be set to automatically refresh by checking "Live Poll" in the Dashboard header, or by setting `?poll=10` with the interval in seconds (default 30 seconds).
|
393
395
|
|
394
|
-
###
|
396
|
+
### Concurrency controls
|
395
397
|
|
396
398
|
GoodJob can extend ActiveJob to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_. Limiting concurrency can help prevent duplicate, double or unnecessary jobs from being enqueued, or race conditions when performing, for example when interacting with 3rd-party APIs.
|
397
399
|
|
@@ -484,6 +486,196 @@ config.good_job.cron = {
|
|
484
486
|
}
|
485
487
|
```
|
486
488
|
|
489
|
+
### Bulk enqueue
|
490
|
+
|
491
|
+
GoodJob's Bulk-enqueue functionality can buffer and enqueue multiple jobs at once, using a single INSERT statement. This can more performant when enqueuing a large number of jobs.
|
492
|
+
|
493
|
+
```ruby
|
494
|
+
# Capture jobs using `.perform_later`:
|
495
|
+
active_jobs = GoodJob::Bulk.enqueue do
|
496
|
+
MyJob.perform_later
|
497
|
+
AnotherJob.perform_later
|
498
|
+
# If an exception is raised within this block, no jobs will be inserted.
|
499
|
+
end
|
500
|
+
|
501
|
+
# All ActiveJob instances are returned from GoodJob::Bulk.enqueue.
|
502
|
+
# Jobs that have been successfully enqueued have a `provider_job_id` set.
|
503
|
+
active_jobs.all?(&:provider_job_id)
|
504
|
+
|
505
|
+
# Bulk enqueue ActiveJob instances directly without using `.perform_later`:
|
506
|
+
GoodJob::Bulk.enqueue(MyJob.new, AnotherJob.new)
|
507
|
+
```
|
508
|
+
|
509
|
+
### Batches
|
510
|
+
|
511
|
+
Batches track a set of jobs, and enqueue an optional callback job when all of the jobs have finished (succeeded or discarded).
|
512
|
+
|
513
|
+
- A simple example that enqueues your `MyBatchCallbackJob` after the two jobs have finished, and passes along the current user as a batch property:
|
514
|
+
|
515
|
+
```ruby
|
516
|
+
GoodJob::Batch.enqueue(on_finish: MyBatchCallbackJob, user: current_user) do
|
517
|
+
MyJob.perform_later
|
518
|
+
OtherJob.perform_later
|
519
|
+
end
|
520
|
+
|
521
|
+
# When these jobs have finished, it will enqueue your `MyBatchCallbackJob.perform_later(batch, options)`
|
522
|
+
class MyBatchCallbackJob < ApplicationJob
|
523
|
+
# Callback jobs must accept a `batch` and `options` argument
|
524
|
+
def perform(batch, params)
|
525
|
+
# The batch object will contain the Batch's properties, which are mutable
|
526
|
+
batch.properties[:user] # => <User id: 1, ...>
|
527
|
+
|
528
|
+
# Params is a hash containing additional context (more may be added in the future)
|
529
|
+
params[:event] # => :finish, :success, :discard
|
530
|
+
end
|
531
|
+
end
|
532
|
+
```
|
533
|
+
|
534
|
+
- Jobs can be added to an existing batch. Jobs in a batch are enqueued and performed immediately/asynchronously. The final callback job will not be enqueued until `GoodJob::Batch#enqueue` is called.
|
535
|
+
|
536
|
+
```ruby
|
537
|
+
batch = GoodJob::Batch.add do
|
538
|
+
10.times { MyJob.perform_later }
|
539
|
+
end
|
540
|
+
batch.add do
|
541
|
+
10.times { OtherJob.perform_later }
|
542
|
+
end
|
543
|
+
batch.enqueue(on_finish: MyBatchCallbackJob, age: 42)
|
544
|
+
```
|
545
|
+
|
546
|
+
- If you need to access the batch within a job that is part of the batch, include [`GoodJob::ActiveJobExtensions::Batches`](lib/good_job/active_job_extensions/batches.rb) in your job class:
|
547
|
+
|
548
|
+
```ruby
|
549
|
+
class MyJob < ApplicationJob
|
550
|
+
include GoodJob::ActiveJobExtensions::Batches
|
551
|
+
|
552
|
+
def perform
|
553
|
+
self.batch # => <GoodJob::Batch id: 1, ...>
|
554
|
+
end
|
555
|
+
end
|
556
|
+
```
|
557
|
+
|
558
|
+
- [`GoodJob::Batch`](app/models/good_job/batch.rb) has a number of assignable attributes and methods:
|
559
|
+
|
560
|
+
```ruby
|
561
|
+
batch = GoodJob::Batch.new
|
562
|
+
batch.description = "My batch"
|
563
|
+
batch.on_finish = "MyBatchCallbackJob" # Callback job when all jobs have finished
|
564
|
+
batch.on_success = "MyBatchCallbackJob" # Callback job when/if all jobs have succeeded
|
565
|
+
batch.on_discard = "MyBatchCallbackJob" # Callback job when the first job in the batch is discarded
|
566
|
+
batch.callback_queue_name = "special_queue" # Optional queue for callback jobs, otherwise will defer to job class
|
567
|
+
batch.callback_priority = 10 # Optional priority name for callback jobs, otherwise will defer to job class
|
568
|
+
batch.properties = { age: 42 } # Custom data and state to attach to the batch
|
569
|
+
batch.add do
|
570
|
+
MyJob.perform_later
|
571
|
+
end
|
572
|
+
batch.enqueue
|
573
|
+
|
574
|
+
batch.discarded? # => Boolean
|
575
|
+
batch.discarded_at # => <DateTime>
|
576
|
+
batch.finished? # => Boolean
|
577
|
+
batch.finished_at # => <DateTime>
|
578
|
+
batch.succeeded? # => Boolean
|
579
|
+
batch.active_jobs # => Array of ActiveJob::Base-inherited jobs that are part of the batch
|
580
|
+
|
581
|
+
batch = GoodJob::Batch.find(batch.id)
|
582
|
+
batch.description = "Updated batch description"
|
583
|
+
batch.save
|
584
|
+
batch.reload
|
585
|
+
```
|
586
|
+
|
587
|
+
### Batch callback jobs
|
588
|
+
|
589
|
+
Batch callbacks are Active Job jobs that are enqueued at certain events during the execution of jobs within the batch:
|
590
|
+
|
591
|
+
- `:finish` - Enqueued when all jobs in the batch have finished, after all retries. Jobs will either be discarded or succeeded.
|
592
|
+
- `:success` - Enqueued only when all jobs in the batch have finished and succeeded.
|
593
|
+
- `:discard` - Enqueued immediately the first time a job in the batch is discarded.
|
594
|
+
|
595
|
+
Callback jobs must accept a `batch` and `params` argument in their `perform` method:
|
596
|
+
|
597
|
+
```ruby
|
598
|
+
class MyBatchCallbackJob < ApplicationJob
|
599
|
+
def perform(batch, params)
|
600
|
+
# The batch object will contain the Batch's properties
|
601
|
+
batch.properties[:user] # => <User id: 1, ...>
|
602
|
+
# Batches are mutable
|
603
|
+
batch.properties[:user] = User.find(2)
|
604
|
+
batch.save
|
605
|
+
|
606
|
+
# Params is a hash containing additional context (more may be added in the future)
|
607
|
+
params[:event] # => :finish, :success, :discard
|
608
|
+
end
|
609
|
+
end
|
610
|
+
```
|
611
|
+
|
612
|
+
#### Complex batches
|
613
|
+
|
614
|
+
Consider a multi-stage batch with both parallel and serial job steps:
|
615
|
+
|
616
|
+
```mermaid
|
617
|
+
graph TD
|
618
|
+
0{"BatchJob\n{ stage: nil }"}
|
619
|
+
0 --> a["WorkJob]\n{ step: a }"]
|
620
|
+
0 --> b["WorkJob]\n{ step: b }"]
|
621
|
+
0 --> c["WorkJob]\n{ step: c }"]
|
622
|
+
a --> 1
|
623
|
+
b --> 1
|
624
|
+
c --> 1
|
625
|
+
1{"BatchJob\n{ stage: 1 }"}
|
626
|
+
1 --> d["WorkJob]\n{ step: d }"]
|
627
|
+
1 --> e["WorkJob]\n{ step: e }"]
|
628
|
+
e --> f["WorkJob]\n{ step: f }"]
|
629
|
+
d --> 2
|
630
|
+
f --> 2
|
631
|
+
2{"BatchJob\n{ stage: 2 }"}
|
632
|
+
```
|
633
|
+
|
634
|
+
This can be implemented with a single, mutable batch job:
|
635
|
+
|
636
|
+
```ruby
|
637
|
+
class WorkJob < ApplicationJob
|
638
|
+
include GoodJob::ActiveJobExtensions::Batches
|
639
|
+
|
640
|
+
def perform(step)
|
641
|
+
# ...
|
642
|
+
if step == 'e'
|
643
|
+
batch.add { WorkJob.perform_later('f') }
|
644
|
+
end
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
class BatchJob < ApplicationJob
|
649
|
+
def perform(batch, options)
|
650
|
+
if batch.properties[:stage].nil?
|
651
|
+
batch.enqueue(stage: 1) do
|
652
|
+
WorkJob.perform_later('a')
|
653
|
+
WorkJob.perform_later('b')
|
654
|
+
WorkJob.perform_later('c')
|
655
|
+
end
|
656
|
+
elsif batch.properties[:stage] == 1
|
657
|
+
batch.enqueue(stage: 2) do
|
658
|
+
WorkJob.perform_later('d')
|
659
|
+
WorkJob.perform_later('e')
|
660
|
+
end
|
661
|
+
elsif batch.properties[:stage] == 2
|
662
|
+
# ...
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
GoodJob::Batch.enqueue(on_finish: BatchJob)
|
668
|
+
```
|
669
|
+
|
670
|
+
#### Other batch details
|
671
|
+
|
672
|
+
- Whether to enqueue a callback job is evaluated once the batch is in an `enqueued?`-state by using `GoodJob::Batch.enqueue` or `batch.enqueue`.
|
673
|
+
- Callback job enqueueing will be re-triggered if additional jobs are `enqueue`'d to the batch; use `add` to add jobs to the batch without retriggering callback jobs.
|
674
|
+
- Callback jobs will be enqueued even if the batch contains no jobs.
|
675
|
+
- Callback jobs perform asynchronously. It's possible that `:finish` and `:success` or `:discard` callback jobs perform at the same time. Keep this in mind when updating batch properties.
|
676
|
+
- Batch properties are serialized using Active Job serialization. This is flexible, but can lead to deserialization errors if a GlobalID record is directly referenced but is subsequently deleted and thus unloadable.
|
677
|
+
- 🚧Batches are a work in progress. Please let us know what would be helpful to improve their functionality and usefulness.
|
678
|
+
|
487
679
|
### Updating
|
488
680
|
|
489
681
|
GoodJob follows semantic versioning, though updates may be encouraged through deprecation warnings in minor versions.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class BatchesFilter < BaseFilter
|
4
|
+
def records
|
5
|
+
after_created_at = params[:after_created_at].present? ? Time.zone.parse(params[:after_created_at]) : nil
|
6
|
+
|
7
|
+
filtered_query.display_all(
|
8
|
+
after_created_at: after_created_at,
|
9
|
+
after_id: params[:after_id]
|
10
|
+
).limit(params.fetch(:limit, DEFAULT_LIMIT))
|
11
|
+
end
|
12
|
+
|
13
|
+
def filtered_query(_filtered_params = params)
|
14
|
+
base_query
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_base_query
|
18
|
+
GoodJob::BatchRecord.all.includes(:jobs)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
# NOTE: This class delegates to {GoodJob::BatchRecord} and is intended to be the public interface for Batches.
|
5
|
+
class Batch
|
6
|
+
include GlobalID::Identification
|
7
|
+
|
8
|
+
thread_cattr_accessor :current_batch_id
|
9
|
+
thread_cattr_accessor :current_batch_callback_id
|
10
|
+
|
11
|
+
PROTECTED_PROPERTIES = %i[
|
12
|
+
on_finish
|
13
|
+
on_success
|
14
|
+
on_discard
|
15
|
+
callback_queue_name
|
16
|
+
callback_priority
|
17
|
+
description
|
18
|
+
properties
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
delegate(
|
22
|
+
:id,
|
23
|
+
:created_at,
|
24
|
+
:updated_at,
|
25
|
+
:persisted?,
|
26
|
+
:enqueued_at,
|
27
|
+
:finished_at,
|
28
|
+
:discarded_at,
|
29
|
+
:enqueued?,
|
30
|
+
:finished?,
|
31
|
+
:succeeded?,
|
32
|
+
:discarded?,
|
33
|
+
:description,
|
34
|
+
:description=,
|
35
|
+
:on_finish,
|
36
|
+
:on_finish=,
|
37
|
+
:on_success,
|
38
|
+
:on_success=,
|
39
|
+
:on_discard,
|
40
|
+
:on_discard=,
|
41
|
+
:callback_queue_name,
|
42
|
+
:callback_queue_name=,
|
43
|
+
:callback_priority,
|
44
|
+
:callback_priority=,
|
45
|
+
:properties,
|
46
|
+
:properties=,
|
47
|
+
:save,
|
48
|
+
:reload,
|
49
|
+
to: :record
|
50
|
+
)
|
51
|
+
|
52
|
+
# Create a new batch and enqueue it
|
53
|
+
# @param on_finish [String, Object] The class name of the callback job to be enqueued after the batch is finished
|
54
|
+
# @param properties [Hash] Additional properties to be stored on the batch
|
55
|
+
# @param block [Proc] Enqueue jobs within the block to add them to the batch
|
56
|
+
# @return [GoodJob::BatchRecord]
|
57
|
+
def self.enqueue(active_jobs = [], **properties, &block)
|
58
|
+
new.tap do |batch|
|
59
|
+
batch.enqueue(active_jobs, **properties, &block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.find(id)
|
64
|
+
new _record: BatchRecord.find(id)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Helper method to enqueue jobs and assign them to a batch
|
68
|
+
def self.within_thread(batch_id: nil, batch_callback_id: nil)
|
69
|
+
original_batch_id = current_batch_id
|
70
|
+
original_batch_callback_id = current_batch_callback_id
|
71
|
+
|
72
|
+
self.current_batch_id = batch_id
|
73
|
+
self.current_batch_callback_id = batch_callback_id
|
74
|
+
|
75
|
+
yield
|
76
|
+
ensure
|
77
|
+
self.current_batch_id = original_batch_id
|
78
|
+
self.current_batch_callback_id = original_batch_callback_id
|
79
|
+
end
|
80
|
+
|
81
|
+
def initialize(_record: nil, **properties) # rubocop:disable Lint/UnderscorePrefixedVariableName
|
82
|
+
self.record = _record || BatchRecord.new
|
83
|
+
assign_properties(properties)
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [Array<ActiveJob::Base>] Active jobs added to the batch
|
87
|
+
def enqueue(active_jobs = [], **properties, &block)
|
88
|
+
assign_properties(properties)
|
89
|
+
record.save!
|
90
|
+
|
91
|
+
active_jobs = add(active_jobs, &block)
|
92
|
+
|
93
|
+
record.finished_at = nil
|
94
|
+
record.enqueued_at = Time.current if enqueued_at.nil?
|
95
|
+
record.save!
|
96
|
+
|
97
|
+
record._continue_discard_or_finish
|
98
|
+
active_jobs
|
99
|
+
end
|
100
|
+
|
101
|
+
# Enqueue jobs and add them to the batch
|
102
|
+
# @param block [Proc] Enqueue jobs within the block to add them to the batch
|
103
|
+
# @return [Array<ActiveJob::Base>] Active jobs added to the batch
|
104
|
+
def add(active_jobs = nil, &block)
|
105
|
+
record.save if record.new_record?
|
106
|
+
|
107
|
+
buffer = Bulk::Buffer.new
|
108
|
+
buffer.add(active_jobs)
|
109
|
+
buffer.capture(&block) if block
|
110
|
+
|
111
|
+
self.class.within_thread(batch_id: id) do
|
112
|
+
buffer.enqueue
|
113
|
+
end
|
114
|
+
|
115
|
+
buffer.active_jobs
|
116
|
+
end
|
117
|
+
|
118
|
+
def active_jobs
|
119
|
+
record.jobs.map(&:head_execution).map(&:active_job)
|
120
|
+
end
|
121
|
+
|
122
|
+
def callback_active_jobs
|
123
|
+
record.callback_jobs.map(&:head_execution).map(&:active_job)
|
124
|
+
end
|
125
|
+
|
126
|
+
def assign_properties(properties)
|
127
|
+
properties = properties.dup
|
128
|
+
batch_attrs = PROTECTED_PROPERTIES.index_with { |key| properties.delete(key) }.compact
|
129
|
+
record.assign_attributes(batch_attrs)
|
130
|
+
self.properties.merge!(properties)
|
131
|
+
end
|
132
|
+
|
133
|
+
def _record
|
134
|
+
record
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
attr_accessor :record
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
class BatchRecord < BaseRecord
|
5
|
+
include Lockable
|
6
|
+
|
7
|
+
self.table_name = 'good_job_batches'
|
8
|
+
|
9
|
+
has_many :jobs, class_name: 'GoodJob::Job', inverse_of: :batch, foreign_key: :batch_id, dependent: nil
|
10
|
+
has_many :executions, class_name: 'GoodJob::Execution', foreign_key: :batch_id, inverse_of: :batch, dependent: nil
|
11
|
+
has_many :callback_jobs, class_name: 'GoodJob::Job', foreign_key: :batch_callback_id, dependent: nil # rubocop:disable Rails/InverseOf
|
12
|
+
|
13
|
+
scope :finished, -> { where.not(finished_at: nil) }
|
14
|
+
scope :discarded, -> { where.not(discarded_at: nil) }
|
15
|
+
scope :not_discarded, -> { where(discarded_at: nil) }
|
16
|
+
scope :succeeded, -> { finished.not_discarded }
|
17
|
+
|
18
|
+
alias_attribute :enqueued?, :enqueued_at
|
19
|
+
alias_attribute :discarded?, :discarded_at
|
20
|
+
alias_attribute :finished?, :finished_at
|
21
|
+
|
22
|
+
scope :display_all, (lambda do |after_created_at: nil, after_id: nil|
|
23
|
+
query = order(created_at: :desc, id: :desc)
|
24
|
+
if after_created_at.present? && after_id.present?
|
25
|
+
query = query.where(Arel.sql('(created_at, id) < (:after_created_at, :after_id)'), after_created_at: after_created_at, after_id: after_id)
|
26
|
+
elsif after_created_at.present?
|
27
|
+
query = query.where(Arel.sql('(after_created_at) < (:after_created_at)'), after_created_at: after_created_at)
|
28
|
+
end
|
29
|
+
query
|
30
|
+
end)
|
31
|
+
|
32
|
+
# Whether the batch has finished and no jobs were discarded
|
33
|
+
# @return [Boolean]
|
34
|
+
def succeeded?
|
35
|
+
!discarded? && finished?
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_batch
|
39
|
+
Batch.new(_record: self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def display_attributes
|
43
|
+
attributes.except('serialized_properties').merge(properties: properties)
|
44
|
+
end
|
45
|
+
|
46
|
+
def _continue_discard_or_finish(execution = nil)
|
47
|
+
execution_discarded = execution && execution.error.present? && execution.retried_good_job_id.nil?
|
48
|
+
with_advisory_lock(function: "pg_advisory_lock") do
|
49
|
+
Batch.within_thread(batch_id: nil, batch_callback_id: id) do
|
50
|
+
if execution_discarded && discarded_at.blank?
|
51
|
+
update(discarded_at: Time.current)
|
52
|
+
on_discard.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :discard }) if on_discard.present?
|
53
|
+
end
|
54
|
+
|
55
|
+
if !finished_at && enqueued_at && jobs.where(finished_at: nil).count.zero?
|
56
|
+
update(finished_at: Time.current)
|
57
|
+
on_success.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :success }) if !discarded_at && on_success.present?
|
58
|
+
on_finish.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :finish }) if on_finish.present?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class PropertySerializer
|
65
|
+
def self.dump(value)
|
66
|
+
ActiveJob::Arguments.serialize([value]).first
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.load(value)
|
70
|
+
ActiveJob::Arguments.deserialize([value]).first
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
attribute :serialized_properties, :json, default: -> { {} } # Can be set as default value in `serialize` as of Rails v6.1
|
75
|
+
serialize :serialized_properties, PropertySerializer
|
76
|
+
alias_attribute :properties, :serialized_properties
|
77
|
+
|
78
|
+
def properties=(value)
|
79
|
+
raise ArgumentError, "Properties must be a Hash" unless value.is_a?(Hash)
|
80
|
+
|
81
|
+
self.serialized_properties = value
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|