delayed 0.1.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -3
- data/{lib → app/models}/delayed/job.rb +7 -1
- data/lib/delayed/{railtie.rb → engine.rb} +1 -1
- data/lib/delayed/message_sending.rb +4 -14
- data/lib/delayed/performable_mailer.rb +1 -1
- data/lib/delayed/performable_method.rb +10 -3
- data/lib/delayed/priority.rb +1 -1
- data/lib/delayed/psych_ext.rb +1 -0
- data/lib/delayed/runnable.rb +1 -1
- data/lib/delayed.rb +5 -4
- data/spec/delayed/active_job_adapter_spec.rb +19 -0
- data/spec/delayed/job_spec.rb +10 -4
- data/spec/helper.rb +9 -2
- data/spec/message_sending_spec.rb +33 -23
- data/spec/performable_mailer_spec.rb +48 -38
- data/spec/performable_method_spec.rb +36 -11
- data/spec/psych_ext_spec.rb +24 -0
- data/spec/yaml_ext_spec.rb +1 -1
- metadata +26 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec0bc05440d0ea5392480d83c4a3295aecfcad71a47fc48b16de19d356387e8d
|
4
|
+
data.tar.gz: 2a9b09c4a77f058e8fe322f9b80da8aed4b909b20e3ebbcc33da1277435407d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 183b8ad1b96cbe2ce987a6a2faa449b90963a898ae2ffccb7f99eacbeb94be5cedae452bc2ed884414e6cfe2a8169481192d9269e2111b80b3be2d805a5cac83
|
7
|
+
data.tar.gz: 75ad14cf19505ee4b0152797cbee705dbe712939a5e5a0f3bd960a9ddad2c25756e0aedbc8745e70e40d5b634bdf96ef5d175c81f88a86308b524a1f97b2356c
|
data/README.md
CHANGED
@@ -13,9 +13,11 @@ It supports **postgres**, **mysql**, and **sqlite**, and is designed to be:
|
|
13
13
|
- **Resilient**, with built-in retry mechanisms, exponential back-off, and failed job preservation
|
14
14
|
- **Maintainable**, with robust instrumentation, continuous monitoring, and priority-based alerting
|
15
15
|
|
16
|
-
For an overview of how Betterment uses `delayed` to build resilience into distributed systems,
|
17
|
-
|
18
|
-
|
16
|
+
For an overview of how Betterment uses `delayed` to build resilience into distributed systems, read
|
17
|
+
the
|
18
|
+
[announcement blog post](https://www.betterment.com/resources/delayed-resilient-background-jobs-on-rails/),
|
19
|
+
and/or check out the talk ✨[Can I break this?](https://www.youtube.com/watch?v=TuhS13rBoVY)✨
|
20
|
+
given at RailsConf 2021!
|
19
21
|
|
20
22
|
|
21
23
|
### Why `Delayed`?
|
@@ -163,13 +163,19 @@ module Delayed
|
|
163
163
|
def self.db_time_now
|
164
164
|
if Time.zone
|
165
165
|
Time.zone.now
|
166
|
-
elsif
|
166
|
+
elsif default_timezone == :utc
|
167
167
|
Time.now.utc
|
168
168
|
else
|
169
169
|
Time.current
|
170
170
|
end
|
171
171
|
end
|
172
172
|
|
173
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
174
|
+
def self.default_timezone
|
175
|
+
ActiveRecord.default_timezone
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
173
179
|
def self.worker_tags(worker)
|
174
180
|
{
|
175
181
|
min_priority: worker.min_priority,
|
@@ -8,8 +8,8 @@ module Delayed
|
|
8
8
|
@options = options
|
9
9
|
end
|
10
10
|
|
11
|
-
def method_missing(method, *args)
|
12
|
-
Job.enqueue({ payload_object: @payload_class.new(@target, method.to_sym, args) }.merge(@options))
|
11
|
+
def method_missing(method, *args, **kwargs)
|
12
|
+
Job.enqueue({ payload_object: @payload_class.new(@target, method.to_sym, args, kwargs) }.merge(@options))
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
@@ -18,16 +18,6 @@ module Delayed
|
|
18
18
|
DelayProxy.new(PerformableMethod, self, options)
|
19
19
|
end
|
20
20
|
alias __delay__ delay
|
21
|
-
|
22
|
-
def send_later(method, *args)
|
23
|
-
warn '[DEPRECATION] `object.send_later(:method)` is deprecated. Use `object.delay.method'
|
24
|
-
__delay__.__send__(method, *args)
|
25
|
-
end
|
26
|
-
|
27
|
-
def send_at(time, method, *args)
|
28
|
-
warn '[DEPRECATION] `object.send_at(time, :method)` is deprecated. Use `object.delay(:run_at => time).method'
|
29
|
-
__delay__(run_at: time).__send__(method, *args)
|
30
|
-
end
|
31
21
|
end
|
32
22
|
|
33
23
|
module MessageSendingClassMethods
|
@@ -36,7 +26,7 @@ module Delayed
|
|
36
26
|
punctuation = $1 # rubocop:disable Style/PerlBackrefs
|
37
27
|
with_method = "#{aliased_method}_with_delay#{punctuation}"
|
38
28
|
without_method = "#{aliased_method}_without_delay#{punctuation}"
|
39
|
-
define_method(with_method) do |*args|
|
29
|
+
define_method(with_method) do |*args, **kwargs|
|
40
30
|
curr_opts = opts.clone
|
41
31
|
curr_opts.each_key do |key|
|
42
32
|
next unless (val = curr_opts[key]).is_a?(Proc)
|
@@ -47,7 +37,7 @@ module Delayed
|
|
47
37
|
val.call
|
48
38
|
end
|
49
39
|
end
|
50
|
-
delay(curr_opts).__send__(without_method, *args)
|
40
|
+
delay(curr_opts).__send__(without_method, *args, **kwargs)
|
51
41
|
end
|
52
42
|
|
53
43
|
alias_method without_method, method
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module Delayed
|
2
2
|
class PerformableMethod
|
3
|
-
attr_accessor :object, :method_name, :args
|
3
|
+
attr_accessor :object, :method_name, :args, :kwargs
|
4
4
|
|
5
|
-
def initialize(object, method_name, args)
|
5
|
+
def initialize(object, method_name, args, kwargs)
|
6
6
|
raise NoMethodError, "undefined method `#{method_name}' for #{object.inspect}" unless object.respond_to?(method_name, true)
|
7
7
|
|
8
8
|
if !her_model?(object) && object.respond_to?(:persisted?) && !object.persisted?
|
@@ -11,6 +11,7 @@ module Delayed
|
|
11
11
|
|
12
12
|
self.object = object
|
13
13
|
self.args = args
|
14
|
+
self.kwargs = kwargs
|
14
15
|
self.method_name = method_name.to_sym
|
15
16
|
end
|
16
17
|
|
@@ -23,7 +24,13 @@ module Delayed
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def perform
|
26
|
-
|
27
|
+
return unless object
|
28
|
+
|
29
|
+
if kwargs.nil? || (RUBY_VERSION < '2.7' && kwargs.empty?)
|
30
|
+
object.send(method_name, *args)
|
31
|
+
else
|
32
|
+
object.send(method_name, *args, **kwargs)
|
33
|
+
end
|
27
34
|
end
|
28
35
|
|
29
36
|
def method(sym)
|
data/lib/delayed/priority.rb
CHANGED
@@ -150,7 +150,7 @@ module Delayed
|
|
150
150
|
private
|
151
151
|
|
152
152
|
def respond_to_missing?(method_name, include_private = false)
|
153
|
-
method_name.to_s.end_with?('?') && self.class.names.key?(method_name.to_s[0..-2].to_sym) || super
|
153
|
+
(method_name.to_s.end_with?('?') && self.class.names.key?(method_name.to_s[0..-2].to_sym)) || super
|
154
154
|
end
|
155
155
|
|
156
156
|
def method_missing(method_name, *args)
|
data/lib/delayed/psych_ext.rb
CHANGED
data/lib/delayed/runnable.rb
CHANGED
data/lib/delayed.rb
CHANGED
@@ -14,11 +14,12 @@ require 'delayed/plugins/instrumentation'
|
|
14
14
|
require 'delayed/backend/base'
|
15
15
|
require 'delayed/backend/job_preparer'
|
16
16
|
require 'delayed/worker'
|
17
|
-
require 'delayed/railtie' if defined?(Rails::Railtie)
|
18
17
|
|
19
|
-
|
20
|
-
require 'delayed/
|
21
|
-
|
18
|
+
if defined?(Rails::Engine)
|
19
|
+
require 'delayed/engine'
|
20
|
+
else
|
21
|
+
require 'active_record'
|
22
|
+
require_relative '../app/models/delayed/job'
|
22
23
|
end
|
23
24
|
|
24
25
|
ActiveSupport.on_load(:active_job) do
|
@@ -223,6 +223,25 @@ RSpec.describe Delayed::ActiveJobAdapter do
|
|
223
223
|
end
|
224
224
|
end
|
225
225
|
|
226
|
+
context 'when ActiveJob has both positional and keyword arguments' do
|
227
|
+
let(:job_class) do
|
228
|
+
Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
|
229
|
+
cattr_accessor(:result)
|
230
|
+
|
231
|
+
def perform(arg, kwarg:)
|
232
|
+
self.class.result = [arg, kwarg]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'passes arguments through to the perform method' do
|
238
|
+
JobClass.perform_later('foo', kwarg: 'bar')
|
239
|
+
|
240
|
+
Delayed::Worker.new.work_off
|
241
|
+
expect(JobClass.result).to eq %w(foo bar)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
226
245
|
context 'when using the ActiveJob test adapter' do
|
227
246
|
let(:queue_adapter) { :test }
|
228
247
|
|
data/spec/delayed/job_spec.rb
CHANGED
@@ -352,7 +352,7 @@ describe Delayed::Job do
|
|
352
352
|
context 'large handler' do
|
353
353
|
before do
|
354
354
|
text = 'Lorem ipsum dolor sit amet. ' * 1000
|
355
|
-
@job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, {})
|
355
|
+
@job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, [], {})
|
356
356
|
end
|
357
357
|
|
358
358
|
it 'has an id' do
|
@@ -739,10 +739,16 @@ describe Delayed::Job do
|
|
739
739
|
end
|
740
740
|
end
|
741
741
|
|
742
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
743
|
+
delegate :default_timezone=, to: ActiveRecord
|
744
|
+
else
|
745
|
+
delegate :default_timezone=, to: ActiveRecord::Base
|
746
|
+
end
|
747
|
+
|
742
748
|
context "db_time_now" do
|
743
749
|
after do
|
744
750
|
Time.zone = nil
|
745
|
-
|
751
|
+
self.default_timezone = :local
|
746
752
|
end
|
747
753
|
|
748
754
|
it "returns time in current time zone if set" do
|
@@ -752,13 +758,13 @@ describe Delayed::Job do
|
|
752
758
|
|
753
759
|
it "returns UTC time if that is the AR default" do
|
754
760
|
Time.zone = nil
|
755
|
-
|
761
|
+
self.default_timezone = :utc
|
756
762
|
expect(described_class.db_time_now.zone).to eq "UTC"
|
757
763
|
end
|
758
764
|
|
759
765
|
it "returns local time if that is the AR default" do
|
760
766
|
Time.zone = "Arizona"
|
761
|
-
|
767
|
+
self.default_timezone = :local
|
762
768
|
expect(described_class.db_time_now.zone).to eq("MST")
|
763
769
|
end
|
764
770
|
end
|
data/spec/helper.rb
CHANGED
@@ -139,8 +139,15 @@ RSpec.configure do |config|
|
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
142
|
-
|
143
|
-
|
142
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
143
|
+
require "zeitwerk"
|
144
|
+
loader = Zeitwerk::Loader.new
|
145
|
+
loader.push_dir File.dirname(__FILE__)
|
146
|
+
loader.setup
|
147
|
+
else
|
148
|
+
# Add this directory so the ActiveSupport autoloading works
|
149
|
+
ActiveSupport::Dependencies.autoload_paths << File.dirname(__FILE__)
|
150
|
+
end
|
144
151
|
|
145
152
|
RSpec::Matchers.define :emit_notification do |expected_event_name|
|
146
153
|
attr_reader :actual, :expected
|
@@ -7,24 +7,27 @@ describe Delayed::MessageSending do
|
|
7
7
|
end
|
8
8
|
|
9
9
|
describe 'handle_asynchronously' do
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
let(:test_class) do
|
11
|
+
Class.new do
|
12
|
+
def tell!(_arg, _kwarg:); end
|
13
|
+
handle_asynchronously :tell!
|
14
|
+
end
|
13
15
|
end
|
14
16
|
|
15
17
|
it 'aliases original method' do
|
16
|
-
expect(
|
17
|
-
expect(
|
18
|
+
expect(test_class.new).to respond_to(:tell_without_delay!)
|
19
|
+
expect(test_class.new).to respond_to(:tell_with_delay!)
|
18
20
|
end
|
19
21
|
|
20
22
|
it 'creates a PerformableMethod' do
|
21
|
-
|
23
|
+
obj = test_class.new
|
22
24
|
expect {
|
23
|
-
job =
|
25
|
+
job = obj.tell!('a', kwarg: 'b')
|
24
26
|
expect(job.payload_object.class).to eq(Delayed::PerformableMethod)
|
25
27
|
expect(job.payload_object.method_name).to eq(:tell_without_delay!)
|
26
|
-
expect(job.payload_object.args).to eq([
|
27
|
-
|
28
|
+
expect(job.payload_object.args).to eq(['a'])
|
29
|
+
expect(job.payload_object.kwargs).to eq(kwarg: 'b')
|
30
|
+
}.to change { Delayed::Job.count }.by(1)
|
28
31
|
end
|
29
32
|
|
30
33
|
describe 'with options' do
|
@@ -64,26 +67,33 @@ describe Delayed::MessageSending do
|
|
64
67
|
end
|
65
68
|
|
66
69
|
context 'delay' do
|
67
|
-
|
68
|
-
|
70
|
+
let(:fairy_tail_class) do
|
71
|
+
Class.new do
|
72
|
+
attr_accessor :happy_ending
|
69
73
|
|
70
|
-
|
74
|
+
def self.princesses; end
|
71
75
|
|
72
|
-
|
73
|
-
|
76
|
+
def tell(arg, kwarg:)
|
77
|
+
@happy_ending = [arg, kwarg]
|
78
|
+
end
|
74
79
|
end
|
75
80
|
end
|
76
81
|
|
82
|
+
before do
|
83
|
+
stub_const('FairyTail', fairy_tail_class)
|
84
|
+
end
|
85
|
+
|
77
86
|
after do
|
78
87
|
Delayed::Worker.default_queue_name = nil
|
79
88
|
end
|
80
89
|
|
81
90
|
it 'creates a new PerformableMethod job' do
|
82
91
|
expect {
|
83
|
-
job =
|
92
|
+
job = FairyTail.new.delay.tell('arg', kwarg: 'kwarg')
|
84
93
|
expect(job.payload_object.class).to eq(Delayed::PerformableMethod)
|
85
|
-
expect(job.payload_object.method_name).to eq(:
|
86
|
-
expect(job.payload_object.args).to eq(['
|
94
|
+
expect(job.payload_object.method_name).to eq(:tell)
|
95
|
+
expect(job.payload_object.args).to eq(['arg'])
|
96
|
+
expect(job.payload_object.kwargs).to eq(kwarg: 'kwarg')
|
87
97
|
}.to change { Delayed::Job.count }.by(1)
|
88
98
|
end
|
89
99
|
|
@@ -111,8 +121,8 @@ describe Delayed::MessageSending do
|
|
111
121
|
fairy_tail = FairyTail.new
|
112
122
|
expect {
|
113
123
|
expect {
|
114
|
-
fairy_tail.delay.tell
|
115
|
-
}.to change { fairy_tail.happy_ending }.from(nil).to(
|
124
|
+
fairy_tail.delay.tell('a', kwarg: 'b')
|
125
|
+
}.to change { fairy_tail.happy_ending }.from(nil).to %w(a b)
|
116
126
|
}.not_to(change { Delayed::Job.count })
|
117
127
|
end
|
118
128
|
|
@@ -121,7 +131,7 @@ describe Delayed::MessageSending do
|
|
121
131
|
fairy_tail = FairyTail.new
|
122
132
|
expect {
|
123
133
|
expect {
|
124
|
-
fairy_tail.delay.tell
|
134
|
+
fairy_tail.delay.tell('a', kwarg: 'b')
|
125
135
|
}.not_to change { fairy_tail.happy_ending }
|
126
136
|
}.to change { Delayed::Job.count }.by(1)
|
127
137
|
end
|
@@ -131,7 +141,7 @@ describe Delayed::MessageSending do
|
|
131
141
|
fairy_tail = FairyTail.new
|
132
142
|
expect {
|
133
143
|
expect {
|
134
|
-
fairy_tail.delay.tell
|
144
|
+
fairy_tail.delay.tell('a', kwarg: 'b')
|
135
145
|
}.not_to change { fairy_tail.happy_ending }
|
136
146
|
}.to change { Delayed::Job.count }.by(1)
|
137
147
|
end
|
@@ -141,8 +151,8 @@ describe Delayed::MessageSending do
|
|
141
151
|
fairy_tail = FairyTail.new
|
142
152
|
expect {
|
143
153
|
expect {
|
144
|
-
fairy_tail.delay.tell
|
145
|
-
}.to change { fairy_tail.happy_ending }.from(nil).to(
|
154
|
+
fairy_tail.delay.tell('a', kwarg: 'b')
|
155
|
+
}.to change { fairy_tail.happy_ending }.from(nil).to %w(a b)
|
146
156
|
}.not_to(change { Delayed::Job.count })
|
147
157
|
end
|
148
158
|
end
|
@@ -1,58 +1,41 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
3
|
+
describe Delayed::PerformableMailer do
|
4
|
+
let(:mailer_class) do
|
5
|
+
Class.new(ActionMailer::Base) do
|
6
|
+
cattr_accessor(:emails) { [] }
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
expect {
|
13
|
-
job = MyMailer.delay.signup('john@example.com')
|
14
|
-
expect(job.payload_object.class).to eq(Delayed::PerformableMailer)
|
15
|
-
expect(job.payload_object.method_name).to eq(:signup)
|
16
|
-
expect(job.payload_object.args).to eq(['john@example.com'])
|
17
|
-
}.to change { Delayed::Job.count }.by(1)
|
8
|
+
def signup(email, beta_tester: false)
|
9
|
+
mail to: email, subject: "Delaying Emails (beta: #{beta_tester})", from: 'delayedjob@example.com', body: 'Delaying Emails Body'
|
10
|
+
end
|
18
11
|
end
|
19
12
|
end
|
20
13
|
|
21
|
-
|
22
|
-
|
23
|
-
expect {
|
24
|
-
MyMailer.signup('john@example.com').delay
|
25
|
-
}.to raise_error(RuntimeError)
|
26
|
-
end
|
14
|
+
before do
|
15
|
+
stub_const('MyMailer', mailer_class)
|
27
16
|
end
|
28
17
|
|
29
|
-
describe
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
18
|
+
describe 'perform' do
|
19
|
+
it 'calls the method and #deliver on the mailer' do
|
20
|
+
mailer = MyMailer.new
|
21
|
+
email = double('email', deliver: true)
|
22
|
+
allow(mailer).to receive(:mail).and_return(email)
|
23
|
+
mailer_job = described_class.new(mailer, :signup, ['john@example.com'], {})
|
35
24
|
|
36
|
-
|
37
|
-
|
38
|
-
mailer.perform
|
39
|
-
end
|
25
|
+
expect(email).to receive(:deliver)
|
26
|
+
mailer_job.perform
|
40
27
|
end
|
41
28
|
end
|
42
|
-
end
|
43
29
|
|
44
|
-
|
45
|
-
describe ActionMailer::Parameterized::Mailer do
|
30
|
+
describe ActionMailer::Base do
|
46
31
|
describe 'delay' do
|
47
32
|
it 'enqueues a PerformableEmail job' do
|
48
33
|
expect {
|
49
|
-
job = MyMailer.
|
34
|
+
job = MyMailer.delay.signup('john@example.com', beta_tester: true)
|
50
35
|
expect(job.payload_object.class).to eq(Delayed::PerformableMailer)
|
51
|
-
expect(job.payload_object.object.class).to eq(described_class)
|
52
|
-
expect(job.payload_object.object.instance_variable_get('@mailer')).to eq(MyMailer)
|
53
|
-
expect(job.payload_object.object.instance_variable_get('@params')).to eq(foo: 1, bar: 2)
|
54
36
|
expect(job.payload_object.method_name).to eq(:signup)
|
55
37
|
expect(job.payload_object.args).to eq(['john@example.com'])
|
38
|
+
expect(job.payload_object.kwargs).to eq(beta_tester: true)
|
56
39
|
}.to change { Delayed::Job.count }.by(1)
|
57
40
|
end
|
58
41
|
end
|
@@ -60,9 +43,36 @@ if defined?(ActionMailer::Parameterized::Mailer)
|
|
60
43
|
describe 'delay on a mail object' do
|
61
44
|
it 'raises an exception' do
|
62
45
|
expect {
|
63
|
-
MyMailer.
|
46
|
+
MyMailer.signup('john@example.com').delay
|
64
47
|
}.to raise_error(RuntimeError)
|
65
48
|
end
|
66
49
|
end
|
67
50
|
end
|
51
|
+
|
52
|
+
if defined?(ActionMailer::Parameterized::Mailer)
|
53
|
+
describe ActionMailer::Parameterized::Mailer do
|
54
|
+
describe 'delay' do
|
55
|
+
it 'enqueues a PerformableEmail job' do
|
56
|
+
expect {
|
57
|
+
job = MyMailer.with(foo: 1, bar: 2).delay.signup('john@example.com', beta_tester: false)
|
58
|
+
expect(job.payload_object.class).to eq(Delayed::PerformableMailer)
|
59
|
+
expect(job.payload_object.object.class).to eq(described_class)
|
60
|
+
expect(job.payload_object.object.instance_variable_get('@mailer')).to eq(MyMailer)
|
61
|
+
expect(job.payload_object.object.instance_variable_get('@params')).to eq(foo: 1, bar: 2)
|
62
|
+
expect(job.payload_object.method_name).to eq(:signup)
|
63
|
+
expect(job.payload_object.args).to eq(['john@example.com'])
|
64
|
+
expect(job.payload_object.kwargs).to eq(beta_tester: false)
|
65
|
+
}.to change { Delayed::Job.count }.by(1)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'delay on a mail object' do
|
70
|
+
it 'raises an exception' do
|
71
|
+
expect {
|
72
|
+
MyMailer.with(foo: 1, bar: 2).signup('john@example.com').delay
|
73
|
+
}.to raise_error(RuntimeError)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
68
78
|
end
|
@@ -2,8 +2,18 @@ require 'helper'
|
|
2
2
|
|
3
3
|
describe Delayed::PerformableMethod do
|
4
4
|
describe 'perform' do
|
5
|
+
let(:test_class) do
|
6
|
+
Class.new do
|
7
|
+
cattr_accessor :result
|
8
|
+
|
9
|
+
def foo(arg, kwarg:)
|
10
|
+
self.class.result = [arg, kwarg]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
5
15
|
before do
|
6
|
-
@method = described_class.new(
|
16
|
+
@method = described_class.new(test_class.new, :foo, ['a'], { kwarg: 'b' })
|
7
17
|
end
|
8
18
|
|
9
19
|
context 'with the persisted record cannot be found' do
|
@@ -17,14 +27,29 @@ describe Delayed::PerformableMethod do
|
|
17
27
|
end
|
18
28
|
|
19
29
|
it 'calls the method on the object' do
|
20
|
-
expect
|
21
|
-
|
30
|
+
expect { @method.perform }
|
31
|
+
.to change { test_class.result }
|
32
|
+
.from(nil).to %w(a b)
|
33
|
+
end
|
34
|
+
|
35
|
+
if RUBY_VERSION < '3.0'
|
36
|
+
context 'when kwargs are nil (job was delayed via prior gem version)' do
|
37
|
+
before do
|
38
|
+
@method = described_class.new(test_class.new, :foo, ['a', { kwarg: 'b' }], nil)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'calls the method on the object' do
|
42
|
+
expect { @method.perform }
|
43
|
+
.to change { test_class.result }
|
44
|
+
.from(nil).to %w(a b)
|
45
|
+
end
|
46
|
+
end
|
22
47
|
end
|
23
48
|
end
|
24
49
|
|
25
50
|
it "raises a NoMethodError if target method doesn't exist" do
|
26
51
|
expect {
|
27
|
-
described_class.new(Object, :method_that_does_not_exist, [])
|
52
|
+
described_class.new(Object, :method_that_does_not_exist, [], {})
|
28
53
|
}.to raise_error(NoMethodError)
|
29
54
|
end
|
30
55
|
|
@@ -33,29 +58,29 @@ describe Delayed::PerformableMethod do
|
|
33
58
|
def private_method; end
|
34
59
|
private :private_method
|
35
60
|
end
|
36
|
-
expect { described_class.new(clazz.new, :private_method, []) }.not_to raise_error
|
61
|
+
expect { described_class.new(clazz.new, :private_method, [], {}) }.not_to raise_error
|
37
62
|
end
|
38
63
|
|
39
64
|
context 'when it receives an object that is not persisted' do
|
40
65
|
let(:object) { double(persisted?: false, expensive_operation: true) }
|
41
66
|
|
42
67
|
it 'raises an ArgumentError' do
|
43
|
-
expect { described_class.new(object, :expensive_operation, []) }.to raise_error ArgumentError
|
68
|
+
expect { described_class.new(object, :expensive_operation, [], {}) }.to raise_error ArgumentError
|
44
69
|
end
|
45
70
|
|
46
71
|
it 'does not raise ArgumentError if the object acts like a Her model' do
|
47
72
|
allow(object.class).to receive(:save_existing).and_return(true)
|
48
|
-
expect { described_class.new(object, :expensive_operation, []) }.not_to raise_error
|
73
|
+
expect { described_class.new(object, :expensive_operation, [], {}) }.not_to raise_error
|
49
74
|
end
|
50
75
|
end
|
51
76
|
|
52
77
|
describe 'display_name' do
|
53
78
|
it 'returns class_name#method_name for instance methods' do
|
54
|
-
expect(described_class.new('foo', :count, ['o']).display_name).to eq('String#count')
|
79
|
+
expect(described_class.new('foo', :count, ['o'], {}).display_name).to eq('String#count')
|
55
80
|
end
|
56
81
|
|
57
82
|
it 'returns class_name.method_name for class methods' do
|
58
|
-
expect(described_class.new(Class, :inspect, []).display_name).to eq('Class.inspect')
|
83
|
+
expect(described_class.new(Class, :inspect, [], {}).display_name).to eq('Class.inspect')
|
59
84
|
end
|
60
85
|
end
|
61
86
|
|
@@ -84,7 +109,7 @@ describe Delayed::PerformableMethod do
|
|
84
109
|
end
|
85
110
|
|
86
111
|
it 'delegates failure hook to object' do
|
87
|
-
method = described_class.new('object', :size, [])
|
112
|
+
method = described_class.new('object', :size, [], {})
|
88
113
|
expect(method.object).to receive(:failure)
|
89
114
|
method.failure
|
90
115
|
end
|
@@ -114,7 +139,7 @@ describe Delayed::PerformableMethod do
|
|
114
139
|
end
|
115
140
|
|
116
141
|
it 'delegates failure hook to object' do
|
117
|
-
method = described_class.new('object', :size, [])
|
142
|
+
method = described_class.new('object', :size, [], {})
|
118
143
|
expect(method.object).to receive(:failure)
|
119
144
|
method.failure
|
120
145
|
end
|
data/spec/psych_ext_spec.rb
CHANGED
@@ -11,6 +11,30 @@ describe 'Psych::Visitors::ToRuby', if: defined?(Psych::Visitors::ToRuby) do
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
context Delayed::PerformableMethod do
|
15
|
+
it 'serializes with object, method_name, args, and kwargs' do
|
16
|
+
Delayed::PerformableMethod.new(String, :new, ['hello'], capacity: 20).tap do |pm|
|
17
|
+
serialized = YAML.dump_dj(pm)
|
18
|
+
expect(serialized).to eq <<~YAML
|
19
|
+
--- !ruby/object:Delayed::PerformableMethod
|
20
|
+
object: !ruby/class 'String'
|
21
|
+
method_name: :new
|
22
|
+
args:
|
23
|
+
- hello
|
24
|
+
kwargs:
|
25
|
+
:capacity: 20
|
26
|
+
YAML
|
27
|
+
|
28
|
+
deserialized = YAML.load_dj(serialized)
|
29
|
+
expect(deserialized).to be_an_instance_of(Delayed::PerformableMethod)
|
30
|
+
expect(deserialized.object).to eq String
|
31
|
+
expect(deserialized.method_name).to eq :new
|
32
|
+
expect(deserialized.args).to eq ['hello']
|
33
|
+
expect(deserialized.kwargs).to eq(capacity: 20)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
14
38
|
context ActiveRecord::Base do
|
15
39
|
it 'serializes and deserializes in a version-independent way' do
|
16
40
|
Story.create.tap do |story|
|
data/spec/yaml_ext_spec.rb
CHANGED
@@ -25,7 +25,7 @@ describe 'YAML' do
|
|
25
25
|
it 'autoloads the class of an anonymous struct' do
|
26
26
|
expect {
|
27
27
|
yaml = "--- !ruby/struct\nn: 1\n"
|
28
|
-
object =
|
28
|
+
object = load_with_delayed_visitor(yaml)
|
29
29
|
expect(object).to be_kind_of(Struct)
|
30
30
|
expect(object.n).to eq(1)
|
31
31
|
}.not_to raise_error
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delayed
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Griffith
|
@@ -19,7 +19,7 @@ authors:
|
|
19
19
|
autorequire:
|
20
20
|
bindir: bin
|
21
21
|
cert_chain: []
|
22
|
-
date: 2021-
|
22
|
+
date: 2021-11-30 00:00:00.000000000 Z
|
23
23
|
dependencies:
|
24
24
|
- !ruby/object:Gem::Dependency
|
25
25
|
name: activerecord
|
@@ -203,19 +203,27 @@ dependencies:
|
|
203
203
|
- - ">="
|
204
204
|
- !ruby/object:Gem::Version
|
205
205
|
version: '0'
|
206
|
+
- !ruby/object:Gem::Dependency
|
207
|
+
name: zeitwerk
|
208
|
+
requirement: !ruby/object:Gem::Requirement
|
209
|
+
requirements:
|
210
|
+
- - ">="
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: '0'
|
213
|
+
type: :development
|
214
|
+
prerelease: false
|
215
|
+
version_requirements: !ruby/object:Gem::Requirement
|
216
|
+
requirements:
|
217
|
+
- - ">="
|
218
|
+
- !ruby/object:Gem::Version
|
219
|
+
version: '0'
|
206
220
|
description: |
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
- *Resilient*, with built-in retry mechanisms, exponential backoff, and failed job preservation
|
214
|
-
- *Maintainable*, with robust instrumentation, continuous monitoring, and priority-based alerting
|
215
|
-
|
216
|
-
For an overview of how Betterment uses +delayed+ to build resilience into distributed systems, check
|
217
|
-
out the talk ✨{Can I break this?}[https://www.youtube.com/watch?v=TuhS13rBoVY]✨ given at RailsConf
|
218
|
-
2021!
|
221
|
+
Delayed is a multi-threaded, SQL-driven ActiveJob backend used at Betterment to process millions
|
222
|
+
of background jobs per day. It supports postgres, mysql, and sqlite, and is designed to be
|
223
|
+
Reliable (with co-transactional job enqueues and guaranteed, at-least-once execution), Scalable
|
224
|
+
(with an optimized pickup query and concurrent job execution), Resilient (with built-in retry
|
225
|
+
mechanisms, exponential backoff, and failed job preservation), and Maintainable (with robust
|
226
|
+
instrumentation, continuous monitoring, and priority-based alerting).
|
219
227
|
email:
|
220
228
|
- nathan@betterment.com
|
221
229
|
executables: []
|
@@ -225,12 +233,13 @@ files:
|
|
225
233
|
- LICENSE
|
226
234
|
- README.md
|
227
235
|
- Rakefile
|
236
|
+
- app/models/delayed/job.rb
|
228
237
|
- lib/delayed.rb
|
229
238
|
- lib/delayed/active_job_adapter.rb
|
230
239
|
- lib/delayed/backend/base.rb
|
231
240
|
- lib/delayed/backend/job_preparer.rb
|
241
|
+
- lib/delayed/engine.rb
|
232
242
|
- lib/delayed/exceptions.rb
|
233
|
-
- lib/delayed/job.rb
|
234
243
|
- lib/delayed/lifecycle.rb
|
235
244
|
- lib/delayed/message_sending.rb
|
236
245
|
- lib/delayed/monitor.rb
|
@@ -241,7 +250,6 @@ files:
|
|
241
250
|
- lib/delayed/plugins/instrumentation.rb
|
242
251
|
- lib/delayed/priority.rb
|
243
252
|
- lib/delayed/psych_ext.rb
|
244
|
-
- lib/delayed/railtie.rb
|
245
253
|
- lib/delayed/runnable.rb
|
246
254
|
- lib/delayed/serialization/active_record.rb
|
247
255
|
- lib/delayed/syck_ext.rb
|
@@ -282,6 +290,7 @@ metadata:
|
|
282
290
|
changelog_uri: https://github.com/betterment/delayed/blob/main/CHANGELOG.md
|
283
291
|
bug_tracker_uri: https://github.com/betterment/delayed/issues
|
284
292
|
source_code_uri: https://github.com/betterment/delayed
|
293
|
+
rubygems_mfa_required: 'true'
|
285
294
|
post_install_message:
|
286
295
|
rdoc_options: []
|
287
296
|
require_paths:
|
@@ -297,7 +306,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
297
306
|
- !ruby/object:Gem::Version
|
298
307
|
version: '0'
|
299
308
|
requirements: []
|
300
|
-
rubygems_version: 3.
|
309
|
+
rubygems_version: 3.1.6
|
301
310
|
signing_key:
|
302
311
|
specification_version: 4
|
303
312
|
summary: a multi-threaded, SQL-driven ActiveJob backend used at Betterment to process
|