angry_batch 1.0.0 → 1.0.1

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: 8363c90cc908a4ae3fa64957ebfc422580dd91041dfd32d0d2eb7607e9130a62
4
+ data.tar.gz: b83a95d6938a4ad2e2cc20d3e7fc9edbd0f998e7c1256749d7c2ba6c90f22a04
5
5
  SHA512:
6
- metadata.gz: fe4deb92e43765b17ab14f2ae0edc013549dc0451c29c79a59661f8e0872fbb129d274d5c9f546e17ce40fdf9fb4c4a9b9aee0bf42a0bc7389efef3649ea75a7
7
- data.tar.gz: 12b57e37c3fc89e7b912b2d6907ef89bd97777b3b4656eeb28529b4a8466b416e949b1ab40f5159ba15c59f0728e963dc55d1246bd3af5f28ca0b78b222fbc81
6
+ metadata.gz: 1ab00df9aef51980652bb56151d43ce16af63aa58a2e41f31badcb90f50b2687c9e74e119f6d77fa9f3113f6fb88aa441e2e0988275ef732942ab20f8f73f2e0
7
+ data.tar.gz: bba22c7ff6de71cdd1bfe285aeaead5163140536a19e2142e1e163b57dcec50ec0262519b2148a7ebed2db30cc4274d16200c6d0e576372684bf3633568e150a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 1.0.1 - 2026-05-11
4
+
5
+ - Handle job failures via `after_discard` hook — marks job as `failed` and stores error message
6
+ - Bug fixes
7
+
3
8
  ## Version 1.0.0 - 2025-07-20
4
9
 
5
10
  - 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
 
@@ -61,6 +61,14 @@ queue.enqueue SomeJob, argument3
61
61
  in the queue.perform_later
62
62
  ```
63
63
 
64
+ ### Cleaning completed jobs
65
+
66
+ `AngryBatch` stores jobs in the database. You have to run `AngryBatch::CleanupCronJob` in a cron to clean the records.
67
+
68
+ ```
69
+ AngryBatch::CleanupCronJob.perform
70
+ ```
71
+
64
72
  ### Example
65
73
 
66
74
  **Example 1**
@@ -32,40 +32,36 @@ class AngryBatch::Batch < ActiveRecord::Base
32
32
 
33
33
  class << self
34
34
  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)
38
-
39
- completed.or(failed).or(pending)
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))
40
38
  end
41
39
  end
42
40
 
43
41
  def check_status_of_jobs
44
- return unless pending?
45
- return unless jobs_count == jobs.finished.count
46
-
47
- self.finished_at = Time.current
42
+ handlers_to_enqueue = with_lock do
43
+ return unless pending?
44
+ return unless jobs_count == jobs.finished.count
48
45
 
49
- if jobs.failed.none?
50
- update! state: 'completed'
46
+ self.finished_at = Time.current
51
47
 
52
- enqueue_handlers(complete_handlers)
53
- else
54
- update! state: 'failed'
55
-
56
- enqueue_handlers(failure_handlers)
48
+ if jobs.failed.none?
49
+ update! state: 'completed'
50
+ complete_handlers
51
+ else
52
+ update! state: 'failed'
53
+ failure_handlers
54
+ end
57
55
  end
56
+
57
+ enqueue_handlers(handlers_to_enqueue)
58
58
  end
59
59
 
60
60
  private
61
61
 
62
62
  def enqueue_handlers(handlers)
63
63
  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
64
+ job_class.constantize.perform_later(*ActiveJob::Arguments.deserialize(job_arguments || []))
69
65
  end
70
66
  end
71
67
  end
@@ -5,5 +5,9 @@ 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
8
12
  end
9
13
  end
@@ -9,31 +9,32 @@ class AngryBatch::Builder
9
9
  failure_handlers: [],
10
10
  )
11
11
  @jobs = []
12
+ @performed = false
12
13
  end
13
14
 
14
15
  def performed?
15
- @batch.persisted?
16
+ @performed
16
17
  end
17
18
 
18
19
  delegate :empty?, to: :@jobs
19
20
 
20
21
  def on_complete(job_class, *, **)
21
22
  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
23
+ raise AngryBatch::BatchArgumentError, "#{job_class} must be a subclass of ActiveJob::Base" unless job_class.is_a?(Class) && job_class < ActiveJob::Base
23
24
 
24
25
  @batch.complete_handlers << [job_class, job_class.new(*, **).serialize['arguments']]
25
26
  end
26
27
 
27
28
  def on_failure(job_class, *, **)
28
29
  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
30
+ raise AngryBatch::BatchArgumentError, "#{job_class} must be a subclass of ActiveJob::Base" unless job_class.is_a?(Class) && job_class < ActiveJob::Base
30
31
 
31
32
  @batch.failure_handlers << [job_class, job_class.new(*, **).serialize['arguments']]
32
33
  end
33
34
 
34
35
  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
36
+ raise AngryBatch::BatchArgumentError, 'Batch is already running' if performed?
37
+ raise AngryBatch::BatchArgumentError, "#{job_class} must be a subclass of ActiveJob::Base" unless job_class.is_a?(Class) && job_class < ActiveJob::Base
37
38
  raise AngryBatch::BatchArgumentError, "#{job_class} must include AngryBatch::Batchable" unless job_class.included_modules.include?(AngryBatch::Batchable)
38
39
 
39
40
  @jobs << job_class.new(*, **)
@@ -43,20 +44,32 @@ class AngryBatch::Builder
43
44
  raise AngryBatch::BatchArgumentError, 'Batch is empty' if empty?
44
45
  raise AngryBatch::BatchArgumentError, 'Batch is already running' if performed?
45
46
 
46
- @batch.save!
47
+ ActiveRecord::Base.transaction(requires_new: true) do
48
+ @batch.save!
47
49
 
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
- )
50
+ @jobs.each do |job|
51
+ @batch.jobs.create!(
52
+ active_job_idx: job.job_id,
53
+ active_job_class: job.class.name,
54
+ active_job_arguments: job.serialize['arguments'],
55
+ )
56
+ end
54
57
 
55
- job.enqueue
58
+ @batch.update!(state: 'pending')
56
59
  end
57
60
 
58
- @batch.update!(state: 'pending')
59
- @batch.reload
61
+ @performed = true
62
+ @jobs.each(&:enqueue)
60
63
  @batch.check_status_of_jobs
64
+ rescue
65
+ unless @performed
66
+ @batch = AngryBatch::Batch.new(
67
+ label: @batch.label,
68
+ state: 'scheduling',
69
+ complete_handlers: @batch.complete_handlers,
70
+ failure_handlers: @batch.failure_handlers,
71
+ )
72
+ end
73
+ raise
61
74
  end
62
75
  end
@@ -9,9 +9,29 @@ module AngryBatch::Handle
9
9
  return if record.blank?
10
10
 
11
11
  record.with_lock do
12
+ return if record.failed?
13
+
12
14
  record.update!(state: 'completed')
13
15
  end
14
16
 
15
- record.batch.check_status_of_jobs
17
+ record.batch&.check_status_of_jobs
18
+ rescue ActiveRecord::RecordNotFound
19
+ nil
20
+ end
21
+
22
+ def job_failed(job, exception = nil)
23
+ record = AngryBatch::Job.find_by(active_job_idx: job.job_id)
24
+
25
+ return if record.blank?
26
+
27
+ record.with_lock do
28
+ return if record.completed?
29
+
30
+ record.update!(state: 'failed', error_message: exception&.message)
31
+ end
32
+
33
+ record.batch&.check_status_of_jobs
34
+ rescue ActiveRecord::RecordNotFound
35
+ nil
16
36
  end
17
37
  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.0.1'
5
5
  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.0.1
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-05-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -259,7 +259,6 @@ files:
259
259
  - lib/angry_batch/version.rb
260
260
  - lib/generators/angry_batch/install_generator.rb
261
261
  - lib/generators/angry_batch/templates/create_angry_batch_tables.rb
262
- - sig/angry_batch.rbs
263
262
  homepage: https://github.com/RStankov/AngryBatch
264
263
  licenses:
265
264
  - 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