Skip to content

Commit 24b75f5

Browse files
authored
Allow configuring default priority value at midpoint of range (#39)
### Summary This PR Adds a configuration value (`.assign_at_midpoint`) to `Delayed::Priority` The intent is to allow users to change the default priority of a job (i.e. when using a named priority) to be the middle of the configured priority ranges -- derived from the default or custom priority names -- as opposed to the starting value of the range. By allowing a default value that is in the middle of the range, users will be able to bump the priority of some jobs such that they remain in their assigned queue but take priority over jobs of the same named priority. **Note**: since the last priority uses a range that is infinite, it's not possible to derive the midpoint. So, for now, I've defaulted to adding 5, since that matches our defaulted priority values.
1 parent 9a328c6 commit 24b75f5

File tree

5 files changed

+139
-7
lines changed

5 files changed

+139
-7
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
delayed (0.5.3)
4+
delayed (0.5.4)
55
activerecord (>= 5.2)
66
concurrent-ruby
77

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,14 @@ Delayed::Worker.min_priority = nil
491491
Delayed::Worker.max_priority = nil
492492
```
493493

494+
Job priorities can specified by using the name of the desired range (i.e. :user_visible).
495+
By default, the value for a named priority will be the first value in that range.
496+
To set each priority's default value to the middle of its range (i.e. 15 for :user_visible), Delayed::Priority can be configured with:
497+
498+
```ruby
499+
Delayed::Priority.assign_at_midpoint = true
500+
```
501+
494502
Logging verbosity is also configurable. The gem will attempt to default to `Rails.logger` with an
495503
"info" log level.
496504

delayed.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
1818
spec.require_paths = ['lib']
1919
spec.summary = 'a multi-threaded, SQL-driven ActiveJob backend used at Betterment to process millions of background jobs per day'
2020

21-
spec.version = '0.5.3'
21+
spec.version = '0.5.4'
2222
spec.metadata = {
2323
'changelog_uri' => 'https://github.com/betterment/delayed/blob/main/CHANGELOG.md',
2424
'bug_tracker_uri' => 'https://github.com/betterment/delayed/issues',

lib/delayed/priority.rb

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class Priority < Numeric
5757
}.freeze
5858

5959
class << self
60+
attr_writer :assign_at_midpoint
61+
6062
def names
6163
@names || default_names
6264
end
@@ -70,6 +72,7 @@ def names=(names)
7072

7173
@ranges = nil
7274
@alerts = nil
75+
@names_to_priority = nil
7376
@names = names&.sort_by(&:last)&.to_h&.transform_values { |v| new(v) }
7477
end
7578

@@ -82,12 +85,25 @@ def alerts=(alerts)
8285
@alerts = alerts&.sort_by { |k, _| names.keys.index(k) }&.to_h
8386
end
8487

88+
def assign_at_midpoint?
89+
@assign_at_midpoint || false
90+
end
91+
8592
def ranges
8693
@ranges ||= names.zip(names.except(names.keys.first)).each_with_object({}) do |((name, lower), (_, upper)), obj|
8794
obj[name] = (lower...(upper || Float::INFINITY))
8895
end
8996
end
9097

98+
def names_to_priority
99+
@names_to_priority ||=
100+
if assign_at_midpoint?
101+
names_to_midpoint_priority
102+
else
103+
names
104+
end
105+
end
106+
91107
private
92108

93109
def default_names
@@ -98,13 +114,23 @@ def default_alerts
98114
@names ? {} : DEFAULT_ALERTS
99115
end
100116

117+
def names_to_midpoint_priority
118+
names.each_cons(2).to_h { |(name, priority_value), (_, next_priority_value)|
119+
[name, new(midpoint(priority_value, next_priority_value))]
120+
}.merge(names.keys.last => new(names.values.last + 5))
121+
end
122+
123+
def midpoint(low, high)
124+
low + ((high - low).to_d / 2).ceil
125+
end
126+
101127
def respond_to_missing?(method_name, include_private = false)
102-
names.key?(method_name) || super
128+
names_to_priority.key?(method_name) || super
103129
end
104130

105131
def method_missing(method_name, *args)
106-
if names.key?(method_name) && args.none?
107-
names[method_name]
132+
if names_to_priority.key?(method_name) && args.none?
133+
names_to_priority[method_name]
108134
else
109135
super
110136
end
@@ -118,7 +144,7 @@ def method_missing(method_name, *args)
118144

119145
def initialize(value)
120146
super()
121-
value = self.class.names[value] if value.is_a?(Symbol)
147+
value = self.class.names_to_priority[value] if value.is_a?(Symbol)
122148
@value = value.to_i
123149
end
124150

@@ -147,6 +173,20 @@ def <=>(other)
147173
to_i <=> other
148174
end
149175

176+
def -(other)
177+
other = other.to_i if other.is_a?(self.class)
178+
self.class.new(to_i - other)
179+
end
180+
181+
def +(other)
182+
other = other.to_i if other.is_a?(self.class)
183+
self.class.new(to_i + other)
184+
end
185+
186+
def to_d
187+
to_i.to_d
188+
end
189+
150190
private
151191

152192
def respond_to_missing?(method_name, include_private = false)

spec/delayed/priority_spec.rb

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
RSpec.describe Delayed::Priority do
44
let(:custom_names) { nil }
55
let(:custom_alerts) { nil }
6+
let(:assign_at_midpoint) { nil }
67

78
around do |example|
9+
described_class.assign_at_midpoint = assign_at_midpoint
810
described_class.names = custom_names
911
described_class.alerts = custom_alerts
1012
example.run
@@ -13,7 +15,7 @@
1315
described_class.names = nil
1416
end
1517

16-
describe '.names, .ranges, .alerts, method_missing' do
18+
describe '.names, .ranges, .alerts, .names_to_priority, method_missing' do
1719
it 'defaults to interactive, user_visible, eventual, reporting' do
1820
expect(described_class.names).to eq(
1921
interactive: 0,
@@ -33,6 +35,12 @@
3335
eventual: { age: 1.5.hours, run_time: 5.minutes, attempts: 8 },
3436
reporting: { age: 4.hours, run_time: 10.minutes, attempts: 8 },
3537
)
38+
expect(described_class.names_to_priority).to eq(
39+
interactive: 0,
40+
user_visible: 10,
41+
eventual: 20,
42+
reporting: 30,
43+
)
3644
expect(described_class).to respond_to(:interactive)
3745
expect(described_class).to respond_to(:user_visible)
3846
expect(described_class).to respond_to(:eventual)
@@ -43,6 +51,23 @@
4351
expect(described_class.reporting).to eq 30
4452
end
4553

54+
context 'when assign_at_midpoint is set to true' do
55+
let(:assign_at_midpoint) { true }
56+
57+
it 'returns the midpoint value' do
58+
expect(described_class.names_to_priority).to eq(
59+
interactive: 5,
60+
user_visible: 15,
61+
eventual: 25,
62+
reporting: 35,
63+
)
64+
expect(described_class.interactive).to eq 5
65+
expect(described_class.user_visible).to eq 15
66+
expect(described_class.eventual).to eq 25
67+
expect(described_class.reporting).to eq 35
68+
end
69+
end
70+
4671
context 'when customized to high, medium, low' do
4772
let(:custom_names) { { high: 0, medium: 100, low: 500 } }
4873

@@ -57,6 +82,11 @@
5782
medium: (100...500),
5883
low: (500...Float::INFINITY),
5984
)
85+
expect(described_class.names_to_priority).to eq(
86+
high: 0,
87+
medium: 100,
88+
low: 500,
89+
)
6090
expect(described_class.alerts).to eq({})
6191
expect(described_class).not_to respond_to(:interactive)
6292
expect(described_class).not_to respond_to(:user_visible)
@@ -81,6 +111,21 @@
81111
)
82112
end
83113
end
114+
115+
context 'when assign_at_midpoint is set to true' do
116+
let(:assign_at_midpoint) { true }
117+
118+
it 'returns the midpoint value' do
119+
expect(described_class.names_to_priority).to eq(
120+
high: 50,
121+
medium: 300,
122+
low: 505,
123+
)
124+
expect(described_class.high).to eq 50
125+
expect(described_class.medium).to eq 300
126+
expect(described_class.low).to eq 505
127+
end
128+
end
84129
end
85130
end
86131

@@ -110,6 +155,36 @@
110155
expect(described_class.new(-123).interactive?).to eq false
111156
end
112157

158+
context 'when assign_at_midpoint is set to true' do
159+
let(:assign_at_midpoint) { true }
160+
161+
it 'provides the name of the priority range' do
162+
expect(described_class.new(0).name).to eq :interactive
163+
expect(described_class.new(3).name).to eq :interactive
164+
expect(described_class.new(10).name).to eq :user_visible
165+
expect(described_class.new(29).name).to eq :eventual
166+
expect(described_class.new(999).name).to eq :reporting
167+
expect(described_class.new(-123).name).to eq nil
168+
end
169+
170+
it 'supports initialization by symbol value' do
171+
expect(described_class.new(:interactive)).to eq(5)
172+
expect(described_class.new(:user_visible)).to eq(15)
173+
expect(described_class.new(:eventual)).to eq(25)
174+
expect(described_class.new(:reporting)).to eq(35)
175+
end
176+
177+
it "supports predicate ('?') methods" do
178+
expect(described_class.new(0).interactive?).to eq true
179+
expect(described_class.new(3)).to be_interactive
180+
expect(described_class.new(3).user_visible?).to eq false
181+
expect(described_class.new(10)).to be_user_visible
182+
expect(described_class.new(29)).to be_eventual
183+
expect(described_class.new(999)).to be_reporting
184+
expect(described_class.new(-123).interactive?).to eq false
185+
end
186+
end
187+
113188
it 'supports alert threshold methods' do
114189
described_class.alerts = {
115190
interactive: { age: 77.seconds },
@@ -151,4 +226,13 @@
151226
].sort,
152227
).to eq [-13, 3, 5, 40]
153228
end
229+
230+
it 'supports addition and subtraction' do
231+
expect(described_class.new(0) + 10).to eq(10)
232+
expect(10 + described_class.new(5)).to eq(15)
233+
expect(described_class.new(0) + described_class.new(33)).to eq(33)
234+
expect(described_class.new(10) - 5).to eq(5)
235+
expect(15 - described_class.new(10)).to eq(5)
236+
expect(described_class.new(5) - described_class.new(15)).to eq(-10)
237+
end
154238
end

0 commit comments

Comments
 (0)