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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5d0c455a8d5d8f18fda38f1f9f126cc406250d188ee313d0352717c829cd126
4
- data.tar.gz: 208b61ec3b48a7973b160093100b216eafbb96eee604e5e7edc2e7fb67bedfba
3
+ metadata.gz: 22496fe725c6ad616c39f243cf8f53381dee64a5402d3a199c8c604bff2c4eb3
4
+ data.tar.gz: 6d9791dd28f28ddf9317f2770029323754fa32c60403f336f23db8020cdecc51
5
5
  SHA512:
6
- metadata.gz: cac4f2c135e6c01cbdd6cd58c591d726ee8fbb66e623e3d1ed8bfb26e85e6b058e45bd60e3d2ba67b3133666df07d972db8eca75aa05df671b8284c01f45398f
7
- data.tar.gz: 189ba8af30dc9e5a84c064090d5ede8a8f1da4bb56d2249f6ac4130ce3105c9c498f957ad05cf054f2697f83885570013c3c41ed5d197354c992b08518110656
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
- - [ActiveJob concurrency](#activejob-concurrency)
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.5+. JRuby 9.2.13+
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
- ### ActiveJob concurrency
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,12 @@
1
+ # frozen_string_literal: true
2
+ module GoodJob
3
+ class BatchesController < GoodJob::ApplicationController
4
+ def index
5
+ @filter = BatchesFilter.new(params)
6
+ end
7
+
8
+ def show
9
+ @batch = GoodJob::BatchRecord.find(params[:id])
10
+ end
11
+ end
12
+ end
@@ -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