angry_batch 1.0.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b40c1e274e731164e81dc6066fab25c89cb62a6bc9aac8bc2332a3387c061282
4
- data.tar.gz: 22999450f01bb673c9729c48a1e52190a2f67771d9a1fe9ef2079f0b622f2780
3
+ metadata.gz: c1f0b7ce33e9b643c38801a188cb1a5734f30d652a8e3cd7cdd0b3b65f88340a
4
+ data.tar.gz: fbace43e4e2a6740786caa4e04117bbdf62b40191e6834e9d5eb1e5db3e37692
5
5
  SHA512:
6
- metadata.gz: fe4deb92e43765b17ab14f2ae0edc013549dc0451c29c79a59661f8e0872fbb129d274d5c9f546e17ce40fdf9fb4c4a9b9aee0bf42a0bc7389efef3649ea75a7
7
- data.tar.gz: 12b57e37c3fc89e7b912b2d6907ef89bd97777b3b4656eeb28529b4a8466b416e949b1ab40f5159ba15c59f0728e963dc55d1246bd3af5f28ca0b78b222fbc81
6
+ metadata.gz: 72de753a921085e998d060d0cffa1018b3914def85adff93c9b7adbb61009f9d0f7bb295060519b2bbcd0c72db041b03e334f6d6ff170d19a9daf4d062ffb691
7
+ data.tar.gz: adfa172d9219a8f86ba12838c15426a5a12b725dec24a815bccf93b5423afe550e3a2b062cb0f8ad1e3230c82199d62c08ee0ec713011536e98bb323daf3cbe1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
+
15
+ ## Version 1.0.1 - 2026-05-11
16
+
17
+ - Handle job failures via `after_discard` hook — marks job as `failed` and stores error message
18
+ - Bug fixes
19
+
3
20
  ## Version 1.0.0 - 2025-07-20
4
21
 
5
22
  - Initial release
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # AngryBatch
2
2
 
3
3
  ![Build Status](https://github.com/RStankov/AngryBatch/actions/workflows/main.yml/badge.svg)
4
+ [![Gem Version](https://badge.fury.io/rb/angry_batch.svg)](http://badge.fury.io/rb/angry_batch)
4
5
 
5
-
6
- **AngryBatch** is a lightweight batching utility for [ActiveJob](https://guides.rubyonrails.org/active_job_basics.html) that lets you group multiple jobs into a batch and trigger follow-up jobs when all jobs in the batch are done.
6
+ **AngryBatch** is a batching utility for [ActiveJob](https://guides.rubyonrails.org/active_job_basics.html) that lets you group multiple jobs into a batch and trigger follow-up jobs when all jobs in the batch are done.
7
7
 
8
8
  ## Installation
9
9
 
@@ -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,7 +69,100 @@ queue.enqueue SomeJob, argument2
58
69
  queue.enqueue SomeJob, argument3
59
70
 
60
71
  # Step 5: Trigger all jobs in the queue
61
- in the queue.perform_later
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
102
+ ```
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
+
160
+ ### Cleaning completed jobs
161
+
162
+ `AngryBatch` stores jobs in the database. You have to run `AngryBatch::CleanupCronJob` in a cron to clean the records.
163
+
164
+ ```
165
+ AngryBatch::CleanupCronJob.perform
62
166
  ```
63
167
 
64
168
  ### Example
@@ -4,15 +4,18 @@
4
4
  #
5
5
  # Table name: angry_batch_batches
6
6
  #
7
- # id :bigint(8) not null, primary key
8
- # complete_handlers :jsonb not null
9
- # failure_handlers :jsonb not null
10
- # finished_at :datetime
11
- # jobs_count :integer default(0), not null
12
- # label :string
13
- # state :string default("scheduling"), not null
14
- # created_at :datetime not null
15
- # updated_at :datetime not null
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,40 +34,83 @@ class AngryBatch::Batch < ActiveRecord::Base
32
34
 
33
35
  class << self
34
36
  def expired
35
- completed = where('state = ? AND updated_at < ?', :completed, 2.days.ago)
36
- failed = where('state = ? AND updated_at < ?', :failed, 4.weeks.ago)
37
- pending = where('state = ? AND updated_at < ?', :pending, 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
+ end
39
+ end
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
38
51
 
39
- completed.or(failed).or(pending)
52
+ AngryBatch::Helper.after_transaction do
53
+ raise ActiveJob::EnqueueError, ["#{job_class} was not enqueued", job.enqueue_error&.message].compact.join(': ') unless job.enqueue
40
54
  end
55
+
56
+ job
41
57
  end
42
58
 
43
- def check_status_of_jobs
44
- return unless pending?
45
- return unless jobs_count == jobs.finished.count
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
46
69
 
47
- self.finished_at = Time.current
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?
48
76
 
49
- if jobs.failed.none?
50
- update! state: 'completed'
77
+ ((completed_jobs_count + failed_jobs_count) * 100 / jobs_count).clamp(0, 100)
78
+ end
51
79
 
52
- enqueue_handlers(complete_handlers)
53
- else
54
- update! state: 'failed'
80
+ def check_status_of_jobs
81
+ handlers_to_enqueue = with_lock do
82
+ return unless pending?
83
+ return if jobs_count.zero?
84
+ return unless pending_jobs_count <= 0
55
85
 
56
- enqueue_handlers(failure_handlers)
86
+ self.finished_at = Time.current
87
+
88
+ if failed_jobs_count.zero?
89
+ update! state: 'completed'
90
+ complete_handlers
91
+ else
92
+ update! state: 'failed'
93
+ failure_handlers
94
+ end
57
95
  end
96
+
97
+ enqueue_handlers(handlers_to_enqueue)
58
98
  end
59
99
 
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
- if job_arguments.nil?
65
- job_class.constantize.perform_later
66
- else
67
- job_class.constantize.perform_later(*ActiveJob::Arguments.deserialize(job_arguments))
68
- end
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
69
112
  end
113
+
114
+ raise error if error
70
115
  end
71
116
  end
@@ -5,5 +5,26 @@ module AngryBatch::Batchable
5
5
  base.after_perform do |job|
6
6
  AngryBatch::Handle.job_completed(job)
7
7
  end
8
+
9
+ base.after_discard do |job, exception|
10
+ AngryBatch::Handle.job_failed(job, exception)
11
+ end
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)
8
29
  end
9
30
  end
@@ -1,40 +1,44 @@
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
- state: 'scheduling',
8
+ metadata: @metadata,
8
9
  complete_handlers: [],
9
10
  failure_handlers: [],
10
11
  )
11
12
  @jobs = []
13
+ @performed = false
12
14
  end
13
15
 
14
16
  def performed?
15
- @batch.persisted?
17
+ @performed
16
18
  end
17
19
 
18
20
  delegate :empty?, to: :@jobs
19
21
 
20
22
  def on_complete(job_class, *, **)
21
23
  raise AngryBatch::BatchArgumentError, 'Batch is already running' if performed?
22
- raise AngryBatch::BatchArgumentError, "#{job_class} be a subclass of ActiveJob::Base" unless job_class.is_a?(Class) && job_class < ActiveJob::Base
24
+
25
+ AngryBatch::Helper.assert_job_class(job_class)
23
26
 
24
27
  @batch.complete_handlers << [job_class, job_class.new(*, **).serialize['arguments']]
25
28
  end
26
29
 
27
30
  def on_failure(job_class, *, **)
28
31
  raise AngryBatch::BatchArgumentError, 'Batch is already running' if performed?
29
- raise AngryBatch::BatchArgumentError, "#{job_class} be a subclass of ActiveJob::Base" unless job_class.is_a?(Class) && job_class < ActiveJob::Base
32
+
33
+ AngryBatch::Helper.assert_job_class(job_class)
30
34
 
31
35
  @batch.failure_handlers << [job_class, job_class.new(*, **).serialize['arguments']]
32
36
  end
33
37
 
34
38
  def enqueue(job_class, *, **)
35
- raise AngryBatch::BatchArgumentError, 'Batch is already running' unless @batch.new_record?
36
- raise AngryBatch::BatchArgumentError, "#{job_class} be a subclass of ActiveJob::Base" unless job_class.is_a?(Class) && job_class < ActiveJob::Base
37
- raise AngryBatch::BatchArgumentError, "#{job_class} must include AngryBatch::Batchable" unless job_class.included_modules.include?(AngryBatch::Batchable)
39
+ raise AngryBatch::BatchArgumentError, 'Batch is already running' if performed?
40
+
41
+ AngryBatch::Helper.assert_batchable(job_class)
38
42
 
39
43
  @jobs << job_class.new(*, **)
40
44
  end
@@ -43,20 +47,30 @@ class AngryBatch::Builder
43
47
  raise AngryBatch::BatchArgumentError, 'Batch is empty' if empty?
44
48
  raise AngryBatch::BatchArgumentError, 'Batch is already running' if performed?
45
49
 
46
- @batch.save!
50
+ ActiveRecord::Base.transaction(requires_new: true) do
51
+ @batch.save!
47
52
 
48
- @jobs.each do |job|
49
- @batch.jobs.create!(
50
- active_job_idx: job.job_id,
51
- active_job_class: job.class.name,
52
- active_job_arguments: job.serialize['arguments'],
53
- )
53
+ @jobs.each { |job| AngryBatch::Helper.add_job_to_batch(@batch, job) }
54
+ end
55
+
56
+ @performed = true
54
57
 
55
- job.enqueue
58
+ AngryBatch::Helper.after_transaction do
59
+ @jobs.each do |job|
60
+ raise ActiveJob::EnqueueError, ["#{job.class.name} was not enqueued", job.enqueue_error&.message].compact.join(': ') unless job.enqueue
61
+ end
56
62
  end
57
63
 
58
- @batch.update!(state: 'pending')
59
- @batch.reload
60
- @batch.check_status_of_jobs
64
+ @batch
65
+ rescue StandardError
66
+ unless @performed
67
+ @batch = AngryBatch::Batch.new(
68
+ label: @batch.label,
69
+ metadata: @metadata,
70
+ complete_handlers: @batch.complete_handlers,
71
+ failure_handlers: @batch.failure_handlers,
72
+ )
73
+ end
74
+ raise
61
75
  end
62
76
  end
@@ -4,14 +4,29 @@ module AngryBatch::Handle
4
4
  extend self
5
5
 
6
6
  def job_completed(job)
7
+ finish(job, state: 'completed')
8
+ end
9
+
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)
7
17
  record = AngryBatch::Job.find_by(active_job_idx: job.job_id)
8
18
 
9
19
  return if record.blank?
10
20
 
11
21
  record.with_lock do
12
- record.update!(state: 'completed')
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
13
26
  end
14
27
 
15
- record.batch.check_status_of_jobs
28
+ record.batch&.check_status_of_jobs
29
+ rescue ActiveRecord::RecordNotFound
30
+ nil
16
31
  end
17
32
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AngryBatch
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
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
- migration_template 'create_angry_batch_tables.rb', 'db/migrate/create_angry_batch_tables.rb'
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: 'scheduling'
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.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: 2025-07-20 00:00:00.000000000 Z
11
+ date: 2026-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -255,11 +255,12 @@ 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
- - sig/angry_batch.rbs
263
264
  homepage: https://github.com/RStankov/AngryBatch
264
265
  licenses:
265
266
  - MIT
data/sig/angry_batch.rbs DELETED
@@ -1,4 +0,0 @@
1
- module AngryBatch
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end