angry_batch 1.0.1 → 1.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +98 -2
- data/lib/angry_batch/batch.rb +65 -16
- data/lib/angry_batch/batchable.rb +17 -0
- data/lib/angry_batch/builder.rb +19 -18
- data/lib/angry_batch/handle.rb +11 -16
- data/lib/angry_batch/helper.rb +41 -0
- data/lib/angry_batch/version.rb +1 -1
- data/lib/angry_batch.rb +4 -0
- data/lib/generators/angry_batch/install_generator.rb +10 -1
- data/lib/generators/angry_batch/templates/add_metadata_and_counters_to_angry_batch_tables.rb +34 -0
- data/lib/generators/angry_batch/templates/create_angry_batch_tables.rb +5 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1f0b7ce33e9b643c38801a188cb1a5734f30d652a8e3cd7cdd0b3b65f88340a
|
|
4
|
+
data.tar.gz: fbace43e4e2a6740786caa4e04117bbdf62b40191e6834e9d5eb1e5db3e37692
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 72de753a921085e998d060d0cffa1018b3914def85adff93c9b7adbb61009f9d0f7bb295060519b2bbcd0c72db041b03e334f6d6ff170d19a9daf4d062ffb691
|
|
7
|
+
data.tar.gz: adfa172d9219a8f86ba12838c15426a5a12b725dec24a815bccf93b5423afe550e3a2b062cb0f8ad1e3230c82199d62c08ee0ec713011536e98bb323daf3cbe1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Version 1.1.0 - 2026-09-05
|
|
4
|
+
|
|
5
|
+
- New migration — run `rails generate angry_batch:install` and `rails db:migrate` when upgrading
|
|
6
|
+
- Removed the `scheduling` state and the `AngryBatch::Batch.scheduling` scope; new batches start as `pending`
|
|
7
|
+
- Jobs and handlers can access their batch via `batch`
|
|
8
|
+
- Add jobs to a running batch with `batch.enqueue`
|
|
9
|
+
- Attach `metadata:` to a batch, readable via `batch.metadata`
|
|
10
|
+
- Track progress with `progress`, `pending_jobs_count`, `completed_jobs_count` and `failed_jobs_count`
|
|
11
|
+
- `perform_later` returns the `AngryBatch::Batch` record
|
|
12
|
+
- Jobs are enqueued after the surrounding transaction commits (Rails 7.2+)
|
|
13
|
+
- Bug fixes
|
|
14
|
+
|
|
3
15
|
## Version 1.0.1 - 2026-05-11
|
|
4
16
|
|
|
5
17
|
- Handle job failures via `after_discard` hook — marks job as `failed` and stores error message
|
data/README.md
CHANGED
|
@@ -33,6 +33,17 @@ rails generate angry_batch:install
|
|
|
33
33
|
rails db:migrate
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
### Upgrading from 1.0
|
|
37
|
+
|
|
38
|
+
Version 1.1 adds columns to the batches table. Run the generator again; it detects the existing install and only creates the upgrade migration:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
rails generate angry_batch:install
|
|
42
|
+
rails db:migrate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The migration backfills the new counters from the existing job records. Jobs that finish on 1.0 code after the migration has run are not counted, so run the migration while your workers are stopped or drained, and restart them on 1.1 before enqueuing new batches.
|
|
46
|
+
|
|
36
47
|
## Usage
|
|
37
48
|
|
|
38
49
|
```ruby
|
|
@@ -42,7 +53,7 @@ class SomeJob
|
|
|
42
53
|
end
|
|
43
54
|
|
|
44
55
|
# Step 2: Create new batch queue
|
|
45
|
-
queue = AngryBatch.new(label: 'Debug label')
|
|
56
|
+
queue = AngryBatch.new(label: 'Debug label', metadata: { account: account })
|
|
46
57
|
|
|
47
58
|
# Step 3: Add completion handler
|
|
48
59
|
# `on_complete` job will be called when all other queue jobs have completed
|
|
@@ -58,9 +69,94 @@ queue.enqueue SomeJob, argument2
|
|
|
58
69
|
queue.enqueue SomeJob, argument3
|
|
59
70
|
|
|
60
71
|
# Step 5: Trigger all jobs in the queue
|
|
61
|
-
|
|
72
|
+
# returns the AngryBatch::Batch record
|
|
73
|
+
batch = queue.perform_later
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Accessing the batch from a job
|
|
77
|
+
|
|
78
|
+
Every job that includes `AngryBatch::Batchable` can call `batch` while performing. It returns the `AngryBatch::Batch` record, or `nil` when the job was enqueued outside of a batch (or the batch record has already been cleaned up).
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
class SomeJob < ApplicationJob
|
|
82
|
+
include AngryBatch::Batchable
|
|
83
|
+
|
|
84
|
+
def perform(argument)
|
|
85
|
+
batch.label # => 'Debug label'
|
|
86
|
+
batch.metadata # => { account: #<Account> }
|
|
87
|
+
batch.progress # => 33
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Completion and failure handlers get the same access when they include `AngryBatch::Batchable`. Handlers are not part of the batch, so including the module in them does not affect the batch counters.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
class ToBeCalledWhenAllOtherJobsAreCompletedJob < ApplicationJob
|
|
96
|
+
include AngryBatch::Batchable
|
|
97
|
+
|
|
98
|
+
def perform(argument)
|
|
99
|
+
batch.metadata[:account]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
62
102
|
```
|
|
63
103
|
|
|
104
|
+
### Metadata
|
|
105
|
+
|
|
106
|
+
`metadata:` accepts a hash and is stored using ActiveJob argument serialization. Symbols, dates, nested hashes, and ActiveRecord models (via GlobalID) round-trip as-is. Values ActiveJob cannot serialize raise `ActiveJob::SerializationError` when the queue is created.
|
|
107
|
+
|
|
108
|
+
Metadata is read-only after the batch is created. If a model referenced in metadata is deleted before a job reads it, `batch.metadata` raises `ActiveJob::DeserializationError`, the same way job arguments do.
|
|
109
|
+
|
|
110
|
+
### Adding jobs from inside a job
|
|
111
|
+
|
|
112
|
+
A running job can add more jobs to its own batch. The batch does not complete until they finish, and jobs added this way can add jobs themselves.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
class ExportProjectJob < ApplicationJob
|
|
116
|
+
include AngryBatch::Batchable
|
|
117
|
+
|
|
118
|
+
def perform(project)
|
|
119
|
+
project.files.find_each do |file|
|
|
120
|
+
batch.enqueue ExportFileJob, file
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`batch.enqueue` uses the same validation as the queue: the job must be an ActiveJob and include `AngryBatch::Batchable`. It can also be called on any pending `AngryBatch::Batch` record outside of a job.
|
|
127
|
+
|
|
128
|
+
Once a batch is completed or failed, `batch.enqueue` raises `AngryBatch::BatchFinishedError`. To run a second stage from a completion handler, create a new batch instead.
|
|
129
|
+
|
|
130
|
+
If a job raises after adding jobs, the added jobs still run. The batch ends `failed` once they are done, and the `on_failure` handlers run then. Note that a retried job (`retry_on`) runs `batch.enqueue` again and adds the jobs a second time.
|
|
131
|
+
|
|
132
|
+
If a job cannot be enqueued, whether a `before_enqueue` callback aborted it or your queue backend raised, the error is raised to the caller. A queue that cannot accept jobs is an infrastructure problem rather than a failed unit of work, so it belongs in your exception tracker and not in `on_failure`. The batch stays `pending` until you deal with it, and is reaped by `AngryBatch::CleanupCronJob` if you never do.
|
|
133
|
+
|
|
134
|
+
### Transactions
|
|
135
|
+
|
|
136
|
+
Jobs are pushed to your queue after the surrounding database transaction commits, so a worker never sees a job before its batch record exists. If the transaction rolls back, the batch and its jobs are discarded together and nothing is pushed. This applies to `perform_later` and to `batch.enqueue` alike.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
Account.transaction do
|
|
140
|
+
account.update! exporting: true
|
|
141
|
+
|
|
142
|
+
queue.perform_later # pushed once this transaction commits
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
On Rails 7.1 there is no hook for this and jobs are pushed immediately, so avoid enqueuing inside your own transaction there.
|
|
147
|
+
|
|
148
|
+
### Progress
|
|
149
|
+
|
|
150
|
+
Batches keep `jobs_count`, `completed_jobs_count`, and `failed_jobs_count` up to date as jobs finish. `pending_jobs_count` and `progress` (0 to 100) are derived from them.
|
|
151
|
+
|
|
152
|
+
The counters on a record are a snapshot. Call `batch.reload` to refresh them, for example after `batch.enqueue` on the same instance.
|
|
153
|
+
|
|
154
|
+
### Errors
|
|
155
|
+
|
|
156
|
+
A job is marked `failed` when ActiveJob discards it: `discard_on`, exhausted `retry_on`, or an unhandled exception. The batch is `failed` as soon as one job failed and every job has finished. A job only transitions once; a later successful run of a job already marked `failed` does not change it.
|
|
157
|
+
|
|
158
|
+
Prefer `retry_on` over adapter-level retries. With adapter-level retries (for example Sidekiq's own retry), the first unhandled exception marks the job `failed` even if a later attempt succeeds.
|
|
159
|
+
|
|
64
160
|
### Cleaning completed jobs
|
|
65
161
|
|
|
66
162
|
`AngryBatch` stores jobs in the database. You have to run `AngryBatch::CleanupCronJob` in a cron to clean the records.
|
data/lib/angry_batch/batch.rb
CHANGED
|
@@ -4,15 +4,18 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Table name: angry_batch_batches
|
|
6
6
|
#
|
|
7
|
-
# id
|
|
8
|
-
# complete_handlers
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
7
|
+
# id :bigint(8) not null, primary key
|
|
8
|
+
# complete_handlers :jsonb not null
|
|
9
|
+
# completed_jobs_count :integer default(0), not null
|
|
10
|
+
# failed_jobs_count :integer default(0), not null
|
|
11
|
+
# failure_handlers :jsonb not null
|
|
12
|
+
# finished_at :datetime
|
|
13
|
+
# jobs_count :integer default(0), not null
|
|
14
|
+
# label :string
|
|
15
|
+
# metadata :jsonb not null
|
|
16
|
+
# state :string default("pending"), not null
|
|
17
|
+
# created_at :datetime not null
|
|
18
|
+
# updated_at :datetime not null
|
|
16
19
|
#
|
|
17
20
|
# Indexes
|
|
18
21
|
#
|
|
@@ -24,7 +27,6 @@ class AngryBatch::Batch < ActiveRecord::Base
|
|
|
24
27
|
has_many :jobs, class_name: 'AngryBatch::Job', dependent: :delete_all
|
|
25
28
|
|
|
26
29
|
enum :state, {
|
|
27
|
-
scheduling: 'scheduling',
|
|
28
30
|
pending: 'pending',
|
|
29
31
|
completed: 'completed',
|
|
30
32
|
failed: 'failed',
|
|
@@ -32,20 +34,58 @@ class AngryBatch::Batch < ActiveRecord::Base
|
|
|
32
34
|
|
|
33
35
|
class << self
|
|
34
36
|
def expired
|
|
35
|
-
completed.where(updated_at: ...2.days.ago)
|
|
36
|
-
.or(failed.where(updated_at: ...4.weeks.ago))
|
|
37
|
-
.or(pending.where(updated_at: ...4.weeks.ago))
|
|
37
|
+
completed.where(updated_at: ...2.days.ago).or(failed.where(updated_at: ...4.weeks.ago)).or(pending.where(updated_at: ...4.weeks.ago))
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def enqueue(job_class, *, **)
|
|
42
|
+
AngryBatch::Helper.assert_batchable(job_class)
|
|
43
|
+
|
|
44
|
+
job = job_class.new(*, **)
|
|
45
|
+
|
|
46
|
+
with_lock do
|
|
47
|
+
raise AngryBatch::BatchFinishedError, "Batch #{id} is #{state}" unless pending?
|
|
48
|
+
|
|
49
|
+
AngryBatch::Helper.add_job_to_batch(self, job)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
AngryBatch::Helper.after_transaction do
|
|
53
|
+
raise ActiveJob::EnqueueError, ["#{job_class} was not enqueued", job.enqueue_error&.message].compact.join(': ') unless job.enqueue
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
job
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def metadata
|
|
60
|
+
raw = read_attribute(:metadata)
|
|
61
|
+
|
|
62
|
+
unless defined?(@metadata) && @metadata_raw == raw
|
|
63
|
+
@metadata = ActiveJob::Arguments.deserialize([raw || {}]).first
|
|
64
|
+
@metadata_raw = raw
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@metadata
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def pending_jobs_count
|
|
71
|
+
jobs_count - completed_jobs_count - failed_jobs_count
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def progress
|
|
75
|
+
return 0 if jobs_count.zero?
|
|
76
|
+
|
|
77
|
+
((completed_jobs_count + failed_jobs_count) * 100 / jobs_count).clamp(0, 100)
|
|
78
|
+
end
|
|
79
|
+
|
|
41
80
|
def check_status_of_jobs
|
|
42
81
|
handlers_to_enqueue = with_lock do
|
|
43
82
|
return unless pending?
|
|
44
|
-
return
|
|
83
|
+
return if jobs_count.zero?
|
|
84
|
+
return unless pending_jobs_count <= 0
|
|
45
85
|
|
|
46
86
|
self.finished_at = Time.current
|
|
47
87
|
|
|
48
|
-
if
|
|
88
|
+
if failed_jobs_count.zero?
|
|
49
89
|
update! state: 'completed'
|
|
50
90
|
complete_handlers
|
|
51
91
|
else
|
|
@@ -60,8 +100,17 @@ class AngryBatch::Batch < ActiveRecord::Base
|
|
|
60
100
|
private
|
|
61
101
|
|
|
62
102
|
def enqueue_handlers(handlers)
|
|
103
|
+
error = nil
|
|
104
|
+
|
|
63
105
|
handlers.each do |(job_class, job_arguments)|
|
|
64
|
-
job_class.constantize.
|
|
106
|
+
job = job_class.constantize.new(*ActiveJob::Arguments.deserialize(job_arguments || []))
|
|
107
|
+
job.angry_batch_id = id if job.is_a?(AngryBatch::Batchable)
|
|
108
|
+
|
|
109
|
+
raise ActiveJob::EnqueueError, "#{job_class} was not enqueued" unless job.enqueue
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
error ||= e
|
|
65
112
|
end
|
|
113
|
+
|
|
114
|
+
raise error if error
|
|
66
115
|
end
|
|
67
116
|
end
|
|
@@ -10,4 +10,21 @@ module AngryBatch::Batchable
|
|
|
10
10
|
AngryBatch::Handle.job_failed(job, exception)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
attr_accessor :angry_batch_id
|
|
15
|
+
|
|
16
|
+
def serialize
|
|
17
|
+
super.merge('angry_batch_id' => angry_batch_id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def deserialize(job_data)
|
|
21
|
+
super
|
|
22
|
+
self.angry_batch_id = job_data['angry_batch_id']
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def batch
|
|
26
|
+
return @batch if defined?(@batch)
|
|
27
|
+
|
|
28
|
+
@batch = angry_batch_id && AngryBatch::Batch.find_by(id: angry_batch_id)
|
|
29
|
+
end
|
|
13
30
|
end
|
data/lib/angry_batch/builder.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class AngryBatch::Builder
|
|
4
|
-
def initialize(label: nil)
|
|
4
|
+
def initialize(label: nil, metadata: {})
|
|
5
|
+
@metadata = ActiveJob::Arguments.serialize([metadata || {}]).first
|
|
5
6
|
@batch = AngryBatch::Batch.new(
|
|
6
7
|
label: label,
|
|
7
|
-
|
|
8
|
+
metadata: @metadata,
|
|
8
9
|
complete_handlers: [],
|
|
9
10
|
failure_handlers: [],
|
|
10
11
|
)
|
|
@@ -20,22 +21,24 @@ class AngryBatch::Builder
|
|
|
20
21
|
|
|
21
22
|
def on_complete(job_class, *, **)
|
|
22
23
|
raise AngryBatch::BatchArgumentError, 'Batch is already running' if performed?
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
AngryBatch::Helper.assert_job_class(job_class)
|
|
24
26
|
|
|
25
27
|
@batch.complete_handlers << [job_class, job_class.new(*, **).serialize['arguments']]
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def on_failure(job_class, *, **)
|
|
29
31
|
raise AngryBatch::BatchArgumentError, 'Batch is already running' if performed?
|
|
30
|
-
|
|
32
|
+
|
|
33
|
+
AngryBatch::Helper.assert_job_class(job_class)
|
|
31
34
|
|
|
32
35
|
@batch.failure_handlers << [job_class, job_class.new(*, **).serialize['arguments']]
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def enqueue(job_class, *, **)
|
|
36
39
|
raise AngryBatch::BatchArgumentError, 'Batch is already running' if performed?
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
|
|
41
|
+
AngryBatch::Helper.assert_batchable(job_class)
|
|
39
42
|
|
|
40
43
|
@jobs << job_class.new(*, **)
|
|
41
44
|
end
|
|
@@ -47,25 +50,23 @@ class AngryBatch::Builder
|
|
|
47
50
|
ActiveRecord::Base.transaction(requires_new: true) do
|
|
48
51
|
@batch.save!
|
|
49
52
|
|
|
53
|
+
@jobs.each { |job| AngryBatch::Helper.add_job_to_batch(@batch, job) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@performed = true
|
|
57
|
+
|
|
58
|
+
AngryBatch::Helper.after_transaction do
|
|
50
59
|
@jobs.each do |job|
|
|
51
|
-
|
|
52
|
-
active_job_idx: job.job_id,
|
|
53
|
-
active_job_class: job.class.name,
|
|
54
|
-
active_job_arguments: job.serialize['arguments'],
|
|
55
|
-
)
|
|
60
|
+
raise ActiveJob::EnqueueError, ["#{job.class.name} was not enqueued", job.enqueue_error&.message].compact.join(': ') unless job.enqueue
|
|
56
61
|
end
|
|
57
|
-
|
|
58
|
-
@batch.update!(state: 'pending')
|
|
59
62
|
end
|
|
60
63
|
|
|
61
|
-
@
|
|
62
|
-
|
|
63
|
-
@batch.check_status_of_jobs
|
|
64
|
-
rescue
|
|
64
|
+
@batch
|
|
65
|
+
rescue StandardError
|
|
65
66
|
unless @performed
|
|
66
67
|
@batch = AngryBatch::Batch.new(
|
|
67
68
|
label: @batch.label,
|
|
68
|
-
|
|
69
|
+
metadata: @metadata,
|
|
69
70
|
complete_handlers: @batch.complete_handlers,
|
|
70
71
|
failure_handlers: @batch.failure_handlers,
|
|
71
72
|
)
|
data/lib/angry_batch/handle.rb
CHANGED
|
@@ -4,30 +4,25 @@ module AngryBatch::Handle
|
|
|
4
4
|
extend self
|
|
5
5
|
|
|
6
6
|
def job_completed(job)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return if record.blank?
|
|
10
|
-
|
|
11
|
-
record.with_lock do
|
|
12
|
-
return if record.failed?
|
|
13
|
-
|
|
14
|
-
record.update!(state: 'completed')
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
record.batch&.check_status_of_jobs
|
|
18
|
-
rescue ActiveRecord::RecordNotFound
|
|
19
|
-
nil
|
|
7
|
+
finish(job, state: 'completed')
|
|
20
8
|
end
|
|
21
9
|
|
|
22
10
|
def job_failed(job, exception = nil)
|
|
11
|
+
finish(job, state: 'failed', error_message: exception&.message)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def finish(job, state:, **attributes)
|
|
23
17
|
record = AngryBatch::Job.find_by(active_job_idx: job.job_id)
|
|
24
18
|
|
|
25
19
|
return if record.blank?
|
|
26
20
|
|
|
27
21
|
record.with_lock do
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
if record.pending?
|
|
23
|
+
record.update!(state: state, **attributes)
|
|
24
|
+
AngryBatch::Batch.increment_counter(:"#{state}_jobs_count", record.batch_id) # rubocop:disable Rails/SkipsModelValidations
|
|
25
|
+
end
|
|
31
26
|
end
|
|
32
27
|
|
|
33
28
|
record.batch&.check_status_of_jobs
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AngryBatch::Helper
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def assert_job_class(job_class)
|
|
7
|
+
return if job_class.is_a?(Class) && job_class < ActiveJob::Base
|
|
8
|
+
|
|
9
|
+
raise AngryBatch::BatchArgumentError, "#{job_class} must be a subclass of ActiveJob::Base"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def assert_batchable(job_class)
|
|
13
|
+
assert_job_class(job_class)
|
|
14
|
+
|
|
15
|
+
return if job_class.included_modules.include?(AngryBatch::Batchable)
|
|
16
|
+
|
|
17
|
+
raise AngryBatch::BatchArgumentError, "#{job_class} must include AngryBatch::Batchable"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_job_to_batch(batch, job)
|
|
21
|
+
job.angry_batch_id = batch.id
|
|
22
|
+
|
|
23
|
+
batch.jobs.create!(
|
|
24
|
+
active_job_idx: job.job_id,
|
|
25
|
+
active_job_class: job.class.name,
|
|
26
|
+
active_job_arguments: job.serialize['arguments'],
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# NOTE(rstankov): pushing after the commit keeps workers from seeing a job
|
|
31
|
+
# before its record. Rails < 7.2 has no hook for it and pushes immediately.
|
|
32
|
+
if ActiveRecord.respond_to?(:after_all_transactions_commit)
|
|
33
|
+
def after_transaction(&)
|
|
34
|
+
ActiveRecord.after_all_transactions_commit(&)
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
def after_transaction
|
|
38
|
+
yield
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/angry_batch/version.rb
CHANGED
data/lib/angry_batch.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'angry_batch/version'
|
|
4
|
+
require_relative 'angry_batch/helper'
|
|
4
5
|
require_relative 'angry_batch/job'
|
|
5
6
|
require_relative 'angry_batch/batch'
|
|
6
7
|
require_relative 'angry_batch/handle'
|
|
@@ -17,6 +18,9 @@ module AngryBatch
|
|
|
17
18
|
|
|
18
19
|
class BatchArgumentError < ArgumentError
|
|
19
20
|
end
|
|
21
|
+
|
|
22
|
+
class BatchFinishedError < StandardError
|
|
23
|
+
end
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
if defined?(Rails)
|
|
@@ -12,7 +12,16 @@ module AngryBatch
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def copy_migrations
|
|
15
|
-
|
|
15
|
+
existing = self.class.migration_exists?(File.join(destination_root, 'db/migrate'), 'create_angry_batch_tables')
|
|
16
|
+
|
|
17
|
+
if existing.nil?
|
|
18
|
+
migration_template 'create_angry_batch_tables.rb', 'db/migrate/create_angry_batch_tables.rb'
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
return if File.read(existing).include?('completed_jobs_count')
|
|
23
|
+
|
|
24
|
+
migration_template 'add_metadata_and_counters_to_angry_batch_tables.rb', 'db/migrate/add_metadata_and_counters_to_angry_batch_tables.rb', skip: true
|
|
16
25
|
end
|
|
17
26
|
end
|
|
18
27
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddMetadataAndCountersToAngryBatchTables < ActiveRecord::Migration[7.0]
|
|
4
|
+
def up
|
|
5
|
+
change_table :angry_batch_batches do |t|
|
|
6
|
+
begin
|
|
7
|
+
t.jsonb :metadata, null: false, default: {}
|
|
8
|
+
rescue NoMethodError
|
|
9
|
+
t.json :metadata, null: false, default: {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
t.integer :completed_jobs_count, null: false, default: 0
|
|
13
|
+
t.integer :failed_jobs_count, null: false, default: 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
change_column_default :angry_batch_batches, :state, from: 'scheduling', to: 'pending'
|
|
17
|
+
|
|
18
|
+
execute "UPDATE angry_batch_batches SET state = 'pending' WHERE state = 'scheduling'"
|
|
19
|
+
|
|
20
|
+
execute <<~SQL.squish
|
|
21
|
+
UPDATE angry_batch_batches SET
|
|
22
|
+
completed_jobs_count = (SELECT COUNT(*) FROM angry_batch_jobs WHERE angry_batch_jobs.batch_id = angry_batch_batches.id AND angry_batch_jobs.state = 'completed'),
|
|
23
|
+
failed_jobs_count = (SELECT COUNT(*) FROM angry_batch_jobs WHERE angry_batch_jobs.batch_id = angry_batch_batches.id AND angry_batch_jobs.state = 'failed')
|
|
24
|
+
SQL
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def down
|
|
28
|
+
change_column_default :angry_batch_batches, :state, from: 'pending', to: 'scheduling'
|
|
29
|
+
|
|
30
|
+
remove_column :angry_batch_batches, :metadata
|
|
31
|
+
remove_column :angry_batch_batches, :completed_jobs_count
|
|
32
|
+
remove_column :angry_batch_batches, :failed_jobs_count
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -6,15 +6,19 @@ class CreateAngryBatchTables < ActiveRecord::Migration[7.0]
|
|
|
6
6
|
begin
|
|
7
7
|
t.jsonb :complete_handlers, null: false, default: []
|
|
8
8
|
t.jsonb :failure_handlers, null: false, default: []
|
|
9
|
+
t.jsonb :metadata, null: false, default: {}
|
|
9
10
|
rescue NoMethodError
|
|
10
11
|
t.json :complete_handlers, null: false, default: []
|
|
11
12
|
t.json :failure_handlers, null: false, default: []
|
|
13
|
+
t.json :metadata, null: false, default: {}
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
t.datetime :finished_at
|
|
15
17
|
t.integer :jobs_count, null: false, default: 0
|
|
18
|
+
t.integer :completed_jobs_count, null: false, default: 0
|
|
19
|
+
t.integer :failed_jobs_count, null: false, default: 0
|
|
16
20
|
t.string :label
|
|
17
|
-
t.string :state, null: false, default: '
|
|
21
|
+
t.string :state, null: false, default: 'pending'
|
|
18
22
|
|
|
19
23
|
t.timestamps
|
|
20
24
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: angry_batch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Radoslav Stankov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activejob
|
|
@@ -255,9 +255,11 @@ files:
|
|
|
255
255
|
- lib/angry_batch/builder.rb
|
|
256
256
|
- lib/angry_batch/cleanup_cron_job.rb
|
|
257
257
|
- lib/angry_batch/handle.rb
|
|
258
|
+
- lib/angry_batch/helper.rb
|
|
258
259
|
- lib/angry_batch/job.rb
|
|
259
260
|
- lib/angry_batch/version.rb
|
|
260
261
|
- lib/generators/angry_batch/install_generator.rb
|
|
262
|
+
- lib/generators/angry_batch/templates/add_metadata_and_counters_to_angry_batch_tables.rb
|
|
261
263
|
- lib/generators/angry_batch/templates/create_angry_batch_tables.rb
|
|
262
264
|
homepage: https://github.com/RStankov/AngryBatch
|
|
263
265
|
licenses:
|