belated 0.6.7 → 0.8.2

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: e0c28df2d6dffdff23e48d5fba469ca7a67378bebdf6538067f748ebc28d8a67
4
- data.tar.gz: 1805135d97859dd9d399b3e46d636141b014e102630eefa1e7d0a9dc4a2a19bc
3
+ metadata.gz: e61b9cff8c5b664a99f10ce08629e22f5e5439d1f77a50c13568b0c3e90efc61
4
+ data.tar.gz: 84816d001fe4769645181d4baf6868cd89cf06b4812bd710e39bd5a735d04b2b
5
5
  SHA512:
6
- metadata.gz: '086a0dbc146fe3386a24bef59523aeca27dcb72506e83189f39553f63e20f867f6a094faab2ce07fdaadeb0f159bd740fde9bb8bc7f6bf8b2e4f04ea551f5e2e'
7
- data.tar.gz: dea44b6d05c0eda6e5cb5d58c3b317c150d1d5b4399fcac55d80c8e044faecbfd1a21d6f96394950ab271e9db2eb346057c99de4cc02df4d25e8929f25487ff3
6
+ metadata.gz: dc977e1a7806efc5efd79844881162f235ced10abb4f50018f6a2d29b92bf3f18b3bc4b9409e72179a336b091fa0638de7b8057aff61884c9818fff66cccf416
7
+ data.tar.gz: c9852a96a7c50ffc163bb0dd6891133f77f883a87cbd30bab5f3c51b512674f26dded61e50e3dfb5694b464afd5ef6b3410e12fccd95d3ba319651045b8ce3e1
@@ -7,7 +7,7 @@ jobs:
7
7
  strategy:
8
8
  fail-fast: false
9
9
  matrix:
10
- os: [ubuntu-latest, macos-latest]
10
+ os: [ubuntu-latest]
11
11
  # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
12
12
  ruby: [2.6, 2.7, '3.0']
13
13
  runs-on: ${{ matrix.os }}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.8.2] - 2021-09-09
4
+ - Fixed a bug where the adapter was not defined when loading jobs from the PStore file.
5
+
6
+ ## [0.8.1] - 2021-09-09
7
+ - Now you can delete jobs from the future jobs queue. This is useful if you want to delete a job that is scheduled for a future date.
8
+
9
+ ## [0.8] - 2021-09-08
10
+ - Using PStore for future jobs backup and job history. Job history is rotated daily, future jobs cannot at the moment.
11
+ - PStore has ultrasafe mode, should test whether that makes things very slow. If not, might be worth using it for the peace of mind. Or at least have it as an option.
12
+ - ActiveJob support is a bit more natural now code-wise.
13
+
14
+ ## [0.7] - 2021-09-04
15
+ - ActiveJob support! Retries, exception rescuing should work as expected.
16
+ - Second Moderna jab took me out for a while... sorry for the long wait.
3
17
  ## [0.6.7] - 2021-08-25
4
18
 
5
19
  - A bug fix for bad jobs bringing down client side.
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- belated (0.6.7)
4
+ belated (0.8.2)
5
5
  drb
6
6
  dry-configurable
7
+ pstore
7
8
  ruby2_keywords
8
9
  sorted_set
9
10
 
@@ -111,6 +112,7 @@ GEM
111
112
  parallel (1.20.1)
112
113
  parser (3.0.2.0)
113
114
  ast (~> 2.4.1)
115
+ pstore (0.1.1)
114
116
  racc (1.5.2)
115
117
  rack (2.2.3)
116
118
  rack-test (1.1.0)
data/README.md CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  [![CodeFactor](https://www.codefactor.io/repository/github/sampokuokkanen/belated/badge)](https://www.codefactor.io/repository/github/sampokuokkanen/belated) [![Gem Version](https://badge.fury.io/rb/belated.svg)](https://badge.fury.io/rb/belated)
4
4
 
5
- This is Belated, a new Ruby backend job library! It supports running procs, lambdas and classes in the background. To deal with restarts, it uses YAML to load the queue into a file, which it then calls at startup to find the previous jobs. There is no way in Ruby to save procs or lambdas to a file, so they are discarded when the process restarts.
5
+ This is Belated, a new Ruby backend job library! It supports running procs, lambdas and classes in the background. To deal with restarts, it uses YAML for the current jobs in the queue waiting to be processed and PStore for the future jobs to load the queues into a file, which it then calls at startup to find the previous jobs. There is no way in Ruby to save procs or lambdas to a file, so they are discarded when the process restarts.
6
6
 
7
- Belated uses the Ruby Queue class, so it's First In, First Out (FIFO).
8
-
9
- Note that Belated used to be called HardWorker. That name was already in use in Sidekiq documentation and a bit too generic anyway.
7
+ Belated uses the Ruby Queue class, so it's First In, First Out (FIFO), unless of course you want to run the job in the future. In that case the order is decided by the time the job is scheduled to be executed.
10
8
 
11
9
  It uses dRuby to do the communication! Which is absolute great. No need for Redis or PostgreSQL, just Ruby standard libraries.
12
10
 
@@ -16,15 +14,14 @@ Can be used if you're on a normal instance such as EC2 or Digital Ocean drop. No
16
14
 
17
15
  TODO LIST:
18
16
 
19
- - Use GDBM for queue storage? That way could maybe get rid of YAML dumping and make things a bit safer. Not ordered though, so maybe keep a list of the jobs as YAML and update it sometimes? Just as backup. Or RocksDB? Would need to be configurable if you don't have something installed.
20
- - Maybe support ActiveJob?
21
- - Have a web UI.
22
- - Have a job history
23
- - Do some performance testing.
17
+ - Have a web UI with job history.
24
18
  - Deploy a Rails app to production that is using Belated
25
19
  and mention it in the readme. (Capistrano support?)
26
- ([Wasurechatta](https://wasurechatta.com/))
27
- - Add a section telling people to use Sidekiq if they can
20
+ ([Wasurechatta](https://wasurechatta.com/) deployed, still need to setup Capistrano)
21
+
22
+ # Why not Sidekiq?
23
+
24
+ If you can, definitely use Sidekiq!!! Belated is supposed to be used if you can't get anything else to work. Like if you want to use SQLite in a Rails app and don't want to have Redis running. Or maybe you just want to run procs in the background?
28
25
 
29
26
  ## Installation
30
27
 
@@ -48,7 +45,15 @@ Start up Belated!
48
45
 
49
46
  $ belated
50
47
 
51
- Then, in another program, connect to Belated and give it a job to do.
48
+ If you're using Rails, just set Belated to be the ActiveJob adapter like below:
49
+
50
+ ```ruby
51
+ config.active_job.adapter = :belated
52
+ ```
53
+
54
+ And you're good to go!
55
+
56
+ If not, in your non-ActiveJob using program, connect to Belated and give it a job to do.
52
57
  Sample below:
53
58
 
54
59
  ```ruby
@@ -69,8 +74,21 @@ client.perform_belated(DumDum.new, at: Time.now + 5 * 60)
69
74
  client.perform_belated(DumDum.new, max_retries: 3) # default 5
70
75
  ```
71
76
 
72
- Belated runs on localhost, port 8788.
73
- Going to make that an option in the future.
77
+ You can also fetch jobs from the future jobs queue:
78
+
79
+ ```ruby
80
+ job = client.perform_belated(proc { 0 / 0 }, at: Time.now + 5 * 60)
81
+ job = Belated.find job.id # Find the job if it's in the future queue
82
+ # Oh no, that job looks a bit weird!
83
+ # Let's delete it:
84
+ client.perform_belated(
85
+ Belated.delete job.id
86
+ )
87
+ # Yeah... currently you have to send the command through the client like this as a job. :/
88
+ # Maybe the client should handle the deletion?
89
+ ```
90
+
91
+ Belated runs on localhost, port 8788 by default, but the port is configurable, see below.
74
92
 
75
93
  ## Rails
76
94
 
data/belated.gemspec CHANGED
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency 'drb'
35
35
  spec.add_dependency 'dry-configurable'
36
36
  spec.add_dependency 'ruby2_keywords'
37
+ spec.add_dependency 'pstore'
37
38
  spec.add_dependency 'sorted_set'
38
39
  spec.add_development_dependency 'byebug'
39
40
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'belated'
4
+
5
+ module ActiveJob # :nodoc:
6
+ module QueueAdapters # :nodoc:
7
+ # The adapter in charge of handling ActiveJob integration.
8
+ # WIP
9
+ class BelatedAdapter
10
+ def instance
11
+ @instance ||= Belated::Client.instance
12
+ rescue StandardError
13
+ @instance = Belated::Client.new
14
+ end
15
+
16
+ def enqueue(job) # :nodoc:
17
+ Rails.logger.info "Belated got job #{job}"
18
+ instance.perform(job, active_job: true)
19
+ end
20
+
21
+ def enqueue_at(job, timestamp) # :nodoc:
22
+ Rails.logger.info "Belated got job #{job} to be performed at #{Time.at(timestamp)}"
23
+ instance.perform_belated(job, at: timestamp, active_job: true)
24
+ end
25
+
26
+ # JobWrapper that overwrites perform for ActiveJob
27
+ class JobWrapper < Belated::JobWrapper
28
+ def perform
29
+ Base.execute job.serialize
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -74,11 +74,11 @@ class Belated
74
74
  # @param at [Date] - The time at which the job should be executed.
75
75
  # @param max_retries [Integer] - Times the job should be retried if it fails.
76
76
  # @return [JobWrapper] - The job wrapper for the queue.
77
- def perform(job, at: nil, max_retries: 5)
77
+ def perform(job, at: nil, max_retries: 5, active_job: false)
78
78
  start unless started?
79
79
  return unless proper_job?(job)
80
80
 
81
- job_wrapper = wrap_job(job, at: at, max_retries: max_retries)
81
+ job_wrapper = wrap_job(job, at: at.to_f, max_retries: max_retries, active_job: active_job)
82
82
  bank.push(job_wrapper)
83
83
  @mutex.synchronize do
84
84
  proc_table[job_wrapper.object_id] = job_wrapper if job_wrapper.proc_klass
@@ -98,10 +98,15 @@ class Belated
98
98
  false
99
99
  end
100
100
 
101
- def wrap_job(job, at:, max_retries:)
101
+ def wrap_job(job, at:, max_retries:, active_job:)
102
102
  return job if job.is_a?(JobWrapper)
103
103
 
104
- JobWrapper.new(job: job, at: at, max_retries: max_retries)
104
+ wrapper = if active_job
105
+ ActiveJob::QueueAdapters::BelatedAdapter::JobWrapper
106
+ else
107
+ JobWrapper
108
+ end
109
+ wrapper.new(job: job, at: at, max_retries: max_retries, active_job: active_job)
105
110
  end
106
111
 
107
112
  def drb_connected?
@@ -13,47 +13,60 @@ class Belated
13
13
  class JobWrapper
14
14
  include Comparable
15
15
  include Logging
16
- attr_accessor :retries, :max_retries, :id, :job, :at, :completed, :proc_klass
16
+ attr_accessor :retries, :max_retries, :id, :job, :at,
17
+ :completed, :proc_klass, :error, :active_job
18
+
19
+ def initialize(job:, max_retries: 5, at: nil, active_job: false)
20
+ raise 'JobError' unless job.respond_to?(:call) || job.respond_to?(:perform)
17
21
 
18
- def initialize(job:, max_retries: 5, at: nil)
19
22
  self.retries = 0
20
23
  self.max_retries = max_retries
21
- self.id = SecureRandom.uuid
24
+ self.id = job.respond_to?(:job_id) ? job.job_id : SecureRandom.uuid
22
25
  self.job = job
23
26
  self.at = at
24
- self.completed = false
25
27
  self.proc_klass = job.instance_of?(Proc)
28
+ self.active_job = active_job
26
29
  end
27
30
 
28
31
  def <=>(other)
29
- at <=> other.at
32
+ at <=> (other&.at || other&.scheduled_at)
30
33
  end
31
34
 
32
35
  # rubocop:disable Lint/RescueException
33
36
  def perform
34
- resp = if job.respond_to?(:call)
35
- job.call
36
- else
37
- job.perform
38
- end
37
+ resp = execute
39
38
  self.completed = true
40
39
  resp
41
40
  rescue Exception => e
42
41
  case e.class
43
- when Interrupt, SignalException
42
+ when Interrupt, SignalException, NoMethodError
44
43
  raise e
45
44
  else
46
- retry_job
47
- "Error while executing job, #{e.inspect}. Retry #{retries} of #{max_retries}"
45
+ retry_job(e)
46
+ "Error while executing job #{job.inspect}, #{e.inspect}. Retry #{retries} of #{max_retries}"
48
47
  end
49
48
  end
49
+
50
50
  # rubocop:enable Lint/RescueException
51
+ def execute
52
+ if job.respond_to?(:call)
53
+ job.call
54
+ elsif job.respond_to?(:arguments)
55
+ job.perform(*job.arguments)
56
+ else
57
+ job.perform
58
+ end
59
+ end
51
60
 
52
- def retry_job
61
+ def retry_job(error)
53
62
  self.retries += 1
54
- return if retries > max_retries
63
+ if retries > max_retries
64
+ self.error = error
65
+ return
66
+ end
55
67
 
56
- self.at = Time.now + (retries.next**4)
68
+ seconds_to_retry = $TESTING ? 0.05 : retries.next**4
69
+ self.at = (Time.now + seconds_to_retry).to_f
57
70
  log "Job #{id} failed, retrying at #{at}"
58
71
  Belated.job_list.push(self)
59
72
  end
data/lib/belated/queue.rb CHANGED
@@ -4,7 +4,7 @@ require 'belated/job'
4
4
  require 'belated/logging'
5
5
  require 'belated/job_wrapper'
6
6
  require 'sorted_set'
7
-
7
+ require 'pstore'
8
8
  class Belated
9
9
  # Job queues that Belated uses.
10
10
  # queue is the jobs that are currenly
@@ -13,7 +13,7 @@ class Belated
13
13
  # to be added to queue at some point in the future.
14
14
  class Queue
15
15
  include Logging
16
- attr_accessor :future_jobs
16
+ attr_accessor :future_jobs, :future_jobs_db
17
17
 
18
18
  FILE_NAME = 'belated_dump'
19
19
 
@@ -21,15 +21,34 @@ class Belated
21
21
  @queue = queue
22
22
  @mutex = Mutex.new
23
23
  self.future_jobs = future_jobs
24
+ self.future_jobs_db = PStore.new('future_jobs.pstore', true) # pass true for thread safety
25
+ end
26
+
27
+ def enqueue_future_jobs
28
+ loop do
29
+ job = future_jobs.min
30
+ if job.nil?
31
+ sleep Belated.heartbeat
32
+ next
33
+ end
34
+ if job.at <= Time.now.to_f
35
+ delete_job(job)
36
+ push(job)
37
+ end
38
+ rescue DRb::DRbConnError
39
+ error 'DRb connection error!!!!!!'
40
+ log stats
41
+ end
24
42
  end
25
43
 
26
44
  def push(job)
27
45
  if job.is_a?(Symbol) || job.at.nil? ||
28
- job.at <= Time.now.utc
46
+ job.at <= Time.now.to_f
29
47
  @queue.push(job)
30
48
  else
31
49
  @mutex.synchronize do
32
50
  @future_jobs << job
51
+ insert_into_future_jobs_db(job) unless job.proc_klass
33
52
  end
34
53
  end
35
54
  end
@@ -52,16 +71,16 @@ class Belated
52
71
  end
53
72
 
54
73
  def load_jobs
55
- log "reloading... if file exists #{File.exist?(FILE_NAME)}"
74
+ future_jobs_db.transaction(true) do
75
+ future_jobs_db.roots.each do |id|
76
+ future_jobs << future_jobs_db[id]
77
+ end
78
+ end
56
79
  return unless File.exist?(FILE_NAME)
57
80
 
58
81
  jobs = YAML.load(File.binread(FILE_NAME))
59
82
  jobs.each do |job|
60
- if job.at && job.at > Time.now.utc
61
- future_jobs.push(job)
62
- else
63
- @queue.push(job)
64
- end
83
+ @queue.push(job)
65
84
  end
66
85
  File.delete(FILE_NAME)
67
86
  end
@@ -73,12 +92,6 @@ class Belated
73
92
  class_array << klass
74
93
  end
75
94
  end
76
- future_jobs.each do |_job|
77
- unless proc_or_shutdown?(klass = future_jobs.pop)
78
- class_array << klass
79
- end
80
- end
81
-
82
95
  pp File.open(FILE_NAME, 'wb') { |f| f.write(YAML.dump(class_array)) }
83
96
  end
84
97
 
@@ -86,10 +99,32 @@ class Belated
86
99
  true
87
100
  end
88
101
 
102
+ def find(job_id)
103
+ job = nil
104
+ future_jobs_db.transaction(true) do
105
+ job = future_jobs_db[job_id]
106
+ end
107
+ job = future_jobs.find { |j| j.id == job_id } if job.nil?
108
+ job
109
+ end
110
+
111
+ def delete_job(job)
112
+ log "Deleting #{future_jobs.delete(job)} from future jobs"
113
+ future_jobs_db.transaction do
114
+ future_jobs_db.delete(job.id)
115
+ end
116
+ end
117
+
89
118
  private
90
119
 
91
120
  def proc_or_shutdown?(job)
92
121
  job.is_a?(Symbol) || job.job.instance_of?(Proc)
93
122
  end
123
+
124
+ def insert_into_future_jobs_db(job)
125
+ future_jobs_db.transaction do
126
+ future_jobs_db[job.id] = job
127
+ end
128
+ end
94
129
  end
95
130
  end
@@ -22,7 +22,7 @@ class Belated
22
22
  # A client that can perform jobs inline
23
23
  class Client
24
24
  alias old_perform perform
25
- def perform(job, at: nil, max_retries: 5)
25
+ def perform(job, at: nil, max_retries: 5, active_job: false)
26
26
  if Belated::Testing.inline?
27
27
  if job.respond_to?(:call)
28
28
  job.call
@@ -30,7 +30,7 @@ class Belated
30
30
  job.perform
31
31
  end
32
32
  else
33
- old_perform(job, at: at, max_retries: max_retries)
33
+ old_perform(job, at: at, max_retries: max_retries, active_job: active_job)
34
34
  end
35
35
  end
36
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Belated
4
- VERSION = '0.6.7'
4
+ VERSION = '0.8.2'
5
5
  end
@@ -1,4 +1,6 @@
1
1
  require_relative 'logging'
2
+ require 'pstore'
3
+
2
4
  class Belated
3
5
  # The worker class that actually gets the jobs from the queue
4
6
  # and calls them. Expects the jobs to be procs or
@@ -20,9 +22,27 @@ class Belated
20
22
 
21
23
  log "Worker #{@number} got job: #{job.inspect}"
22
24
  log job.perform
25
+ history_insert(job) unless job.proc_klass || !job.completed
23
26
  rescue DRb::DRbConnError, Errno::ECONNREFUSED, RangeError => e
24
27
  log e
25
28
  end
26
29
  end
30
+
31
+ private
32
+
33
+ def history_insert(job)
34
+ store.transaction do
35
+ store[job.id] = job
36
+ end
37
+ rescue StandardError => e
38
+ error e
39
+ end
40
+
41
+ def store
42
+ today = Time.now.strftime('%F')
43
+ return @store if @store&.path&.include?(today)
44
+
45
+ @store = PStore.new("history-#{today}.pstore", true)
46
+ end
27
47
  end
28
48
  end
data/lib/belated.rb CHANGED
@@ -51,7 +51,7 @@ class Belated
51
51
  connect!
52
52
  banner_and_info
53
53
  trap_signals
54
- enqueue_future_jobs
54
+ @@queue.enqueue_future_jobs
55
55
  end
56
56
  alias initialize start
57
57
 
@@ -65,6 +65,7 @@ class Belated
65
65
  end
66
66
 
67
67
  def trap_signals
68
+ pp 'trap'
68
69
  %w[INT TERM].each do |signal|
69
70
  Signal.trap(signal) do
70
71
  @worker_list.length.times do
@@ -88,29 +89,13 @@ class Belated
88
89
  require File.expand_path("#{Belated.config.rails_path}/config/environment.rb")
89
90
  require 'rails/all'
90
91
  require 'belated/rails'
92
+ require 'active_job/queue_adapters/belated_adapter'
91
93
  end
92
94
 
93
95
  def rails?
94
96
  Belated.config.rails
95
97
  end
96
98
 
97
- def enqueue_future_jobs
98
- loop do
99
- job = @@queue.future_jobs.min
100
- if job.nil?
101
- sleep Belated.heartbeat
102
- next
103
- end
104
- if job.at <= Time.now
105
- log "Deleting #{@@queue.future_jobs.delete(job)} from future jobs"
106
- @@queue.push(job)
107
- end
108
- rescue DRb::DRbConnError
109
- error 'DRb connection error!!!!!!'
110
- log stats
111
- end
112
- end
113
-
114
99
  def reload
115
100
  log 'reloading...'
116
101
  @@queue.load_jobs
@@ -156,19 +141,30 @@ class Belated
156
141
  }
157
142
  end
158
143
 
159
- def self.kill_and_clear_queue!
160
- @worker_list&.each do |worker|
161
- Thread.kill(worker)
144
+ class << self
145
+ def find(job_id)
146
+ @@queue.find(job_id)
162
147
  end
163
- clear_queue!
164
- end
165
148
 
166
- def self.clear_queue!
167
- @@queue.clear
168
- end
149
+ def delete(job_id)
150
+ job = find(job_id)
151
+ @@queue.delete_job(job)
152
+ end
153
+
154
+ def kill_and_clear_queue!
155
+ @worker_list&.each do |worker|
156
+ Thread.kill(worker)
157
+ end
158
+ clear_queue!
159
+ end
169
160
 
170
- def self.fetch_job
171
- @@queue.pop
161
+ def clear_queue!
162
+ @@queue.clear
163
+ end
164
+
165
+ def fetch_job
166
+ @@queue.pop
167
+ end
172
168
  end
173
169
 
174
170
  def job_list
@@ -179,5 +175,4 @@ class Belated
179
175
  @@queue
180
176
  end
181
177
  end
182
-
183
- require 'belated/rails' if defined?(::Rails::Engine)
178
+ require 'active_job/queue_adapters/belated_adapter' if defined?(::Rails)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belated
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.7
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sampo Kuokkanen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-25 00:00:00.000000000 Z
11
+ date: 2021-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: drb
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pstore
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: sorted_set
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -106,6 +120,7 @@ files:
106
120
  - bin/bundle
107
121
  - bin/console
108
122
  - bin/setup
123
+ - lib/active_job/queue_adapters/belated_adapter.rb
109
124
  - lib/belated.rb
110
125
  - lib/belated/client.rb
111
126
  - lib/belated/exceptions.rb