metrician 0.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 +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rubocop.yml +65 -0
- data/.rubocop_todo.yml +24 -0
- data/.ruby-version +1 -0
- data/.travis.yml +36 -0
- data/Gemfile +1 -0
- data/METRICS.MD +48 -0
- data/README.md +77 -0
- data/Rakefile +12 -0
- data/config/metrician.yaml +136 -0
- data/gemfiles/Gemfile.4.2.sidekiq-4 +24 -0
- data/gemfiles/Gemfile.4.2.sidekiq-4.lock +173 -0
- data/gemfiles/Gemfile.5.0.pg +24 -0
- data/gemfiles/Gemfile.5.0.pg.lock +182 -0
- data/lib/metrician.rb +80 -0
- data/lib/metrician/configuration.rb +33 -0
- data/lib/metrician/jobs.rb +32 -0
- data/lib/metrician/jobs/delayed_job_callbacks.rb +33 -0
- data/lib/metrician/jobs/resque_plugin.rb +36 -0
- data/lib/metrician/jobs/sidekiq_middleware.rb +28 -0
- data/lib/metrician/middleware.rb +64 -0
- data/lib/metrician/middleware/application_timing.rb +29 -0
- data/lib/metrician/middleware/request_timing.rb +152 -0
- data/lib/metrician/reporter.rb +41 -0
- data/lib/metrician/reporters/active_record.rb +63 -0
- data/lib/metrician/reporters/delayed_job.rb +17 -0
- data/lib/metrician/reporters/honeybadger.rb +26 -0
- data/lib/metrician/reporters/memcache.rb +49 -0
- data/lib/metrician/reporters/method_tracer.rb +70 -0
- data/lib/metrician/reporters/middleware.rb +22 -0
- data/lib/metrician/reporters/net_http.rb +28 -0
- data/lib/metrician/reporters/redis.rb +31 -0
- data/lib/metrician/reporters/resque.rb +17 -0
- data/lib/metrician/reporters/sidekiq.rb +19 -0
- data/lib/metrician/version.rb +5 -0
- data/metrician.gemspec +25 -0
- data/script/setup +72 -0
- data/script/test +36 -0
- data/spec/metrician_spec.rb +372 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/database.rb +33 -0
- data/spec/support/database.sample.yml +10 -0
- data/spec/support/database.travis.yml +9 -0
- data/spec/support/models.rb +2 -0
- data/spec/support/test_delayed_job.rb +12 -0
- data/spec/support/test_resque_job.rb +8 -0
- data/spec/support/test_sidekiq_worker.rb +8 -0
- metadata +188 -0
data/script/test
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -e
|
3
|
+
cd "$(dirname "$0")/.."
|
4
|
+
|
5
|
+
eval "$(rbenv init - --no-rehash)"
|
6
|
+
|
7
|
+
rspec_file_line="$1"
|
8
|
+
if [[ "$rspec_file_line" != "" ]]; then
|
9
|
+
rspec_file_line="[${rspec_file_line}]"
|
10
|
+
fi
|
11
|
+
|
12
|
+
success="true"
|
13
|
+
|
14
|
+
for ruby_version in `ruby -ryaml -e 'puts YAML.load(File.read(".travis.yml"))["rvm"].join(" ")'`; do
|
15
|
+
{
|
16
|
+
echo "testing rub version $ruby_version" &&
|
17
|
+
rbenv shell $ruby_version &&
|
18
|
+
bundle exec rake matrix:spec${rspec_file_line}
|
19
|
+
} || success="false"
|
20
|
+
done
|
21
|
+
|
22
|
+
if [ $success == "true" ]; then
|
23
|
+
tput bold # bold text
|
24
|
+
tput setaf 2 # green text
|
25
|
+
echo "======================================"
|
26
|
+
echo "= Passed ="
|
27
|
+
echo "======================================"
|
28
|
+
tput sgr0 # reset to default text
|
29
|
+
else
|
30
|
+
tput bold # bold text
|
31
|
+
tput setaf 1 # red text
|
32
|
+
echo "======================================"
|
33
|
+
echo "= FAILED ="
|
34
|
+
echo "======================================"
|
35
|
+
tput sgr0 # reset to default text
|
36
|
+
fi
|
@@ -0,0 +1,372 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "instrumental_agent"
|
3
|
+
|
4
|
+
module Metrician
|
5
|
+
def self.null_agent(agent_class: Instrumental::Agent)
|
6
|
+
self.agent = agent_class.new(nil, enabled: false)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.describe Metrician do
|
11
|
+
before do
|
12
|
+
Metrician.instance_variable_set("@agent", nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "has a version number" do
|
16
|
+
Metrician::VERSION.should_not be nil
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "Metrician.activate" do
|
20
|
+
it "takes an agent" do
|
21
|
+
# if this excepts, the world changed
|
22
|
+
Metrician.activate(Metrician.null_agent)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "excepts if the agent is nil" do
|
26
|
+
lambda { Metrician.activate(nil) }.should raise_error(Metrician::MissingAgent)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "excepts if the agent doesn't define one of the required agent methods" do
|
30
|
+
class BadAgent
|
31
|
+
# this list is from Metrician::REQUIRED_AGENT_METHODS
|
32
|
+
def cleanup; end
|
33
|
+
def gauge; end
|
34
|
+
def increment; end
|
35
|
+
def logger; end
|
36
|
+
# def logger=(value); end
|
37
|
+
end
|
38
|
+
agent = BadAgent.new
|
39
|
+
lambda { Metrician.activate(agent) }.should raise_error(Metrician::IncompatibleAgent)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "ActiveRecord" do
|
44
|
+
before do
|
45
|
+
@agent = Metrician.null_agent
|
46
|
+
Metrician.activate(@agent)
|
47
|
+
end
|
48
|
+
|
49
|
+
specify "top level queries are instrumented" do
|
50
|
+
@agent.stub(:gauge)
|
51
|
+
@agent.should_receive(:gauge).with("database.query", anything)
|
52
|
+
|
53
|
+
User.where(name: "foobar").to_a
|
54
|
+
end
|
55
|
+
|
56
|
+
specify "per command instrumentation" do
|
57
|
+
Metrician.configuration[:database][:command][:enabled] = true
|
58
|
+
@agent.stub(:gauge)
|
59
|
+
@agent.should_receive(:gauge).with("database.select", anything)
|
60
|
+
|
61
|
+
User.where(name: "foobar").to_a
|
62
|
+
end
|
63
|
+
|
64
|
+
specify "per table instrumentation" do
|
65
|
+
Metrician.configuration[:database][:table] = true
|
66
|
+
@agent.stub(:gauge)
|
67
|
+
@agent.should_receive(:gauge).with("database.users", anything)
|
68
|
+
|
69
|
+
User.where(name: "foobar").to_a
|
70
|
+
end
|
71
|
+
|
72
|
+
specify "per command and table instrumentation" do
|
73
|
+
Metrician.configuration[:database][:command] = true
|
74
|
+
Metrician.configuration[:database][:table] = true
|
75
|
+
@agent.stub(:gauge)
|
76
|
+
@agent.should_receive(:gauge).with("database.select.users", anything)
|
77
|
+
|
78
|
+
User.where(name: "foobar").to_a
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "job systems" do
|
83
|
+
describe "delayed_job" do
|
84
|
+
before do
|
85
|
+
@agent = Metrician.null_agent
|
86
|
+
Metrician.activate(@agent)
|
87
|
+
end
|
88
|
+
|
89
|
+
specify "DelayedJob is instrumented" do
|
90
|
+
@agent.stub(:gauge)
|
91
|
+
|
92
|
+
@agent.should_receive(:gauge).with("jobs.run", anything)
|
93
|
+
Delayed::Job.enqueue(TestDelayedJob.new(success: true))
|
94
|
+
Delayed::Worker.new(exit_on_complete: true).start
|
95
|
+
end
|
96
|
+
|
97
|
+
specify "job errors are instrumented" do
|
98
|
+
@agent.stub(:increment)
|
99
|
+
|
100
|
+
@agent.should_receive(:increment).with("jobs.error", 1)
|
101
|
+
Delayed::Job.enqueue(TestDelayedJob.new(error: true))
|
102
|
+
Delayed::Worker.new(exit_on_complete: true).start
|
103
|
+
end
|
104
|
+
|
105
|
+
specify "per job instrumentation" do
|
106
|
+
Metrician.configuration[:jobs][:job_specific][:enabled] = true
|
107
|
+
@agent.stub(:gauge)
|
108
|
+
|
109
|
+
@agent.should_receive(:gauge).with("jobs.run.job.TestDelayedJob", anything)
|
110
|
+
Delayed::Job.enqueue(TestDelayedJob.new(success: true))
|
111
|
+
Delayed::Worker.new(exit_on_complete: true).start
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "resque" do
|
116
|
+
before do
|
117
|
+
Resque.inline = true
|
118
|
+
@agent = Metrician.null_agent
|
119
|
+
Metrician.activate(@agent)
|
120
|
+
end
|
121
|
+
|
122
|
+
after do
|
123
|
+
Resque.inline = false
|
124
|
+
end
|
125
|
+
|
126
|
+
specify "Resque is instrumented" do
|
127
|
+
@agent.stub(:gauge)
|
128
|
+
@agent.should_receive(:gauge).with("jobs.run", anything)
|
129
|
+
|
130
|
+
# typically Metrician would be loaded in an initalizer
|
131
|
+
# and this _extend_ could be done inside the job itself, but here
|
132
|
+
# we are in a weird situation.
|
133
|
+
TestResqueJob.send(:extend, Metrician::Jobs::ResquePlugin)
|
134
|
+
Resque.enqueue(TestResqueJob, { "success" => true })
|
135
|
+
end
|
136
|
+
|
137
|
+
specify "job errors are instrumented" do
|
138
|
+
@agent.stub(:increment)
|
139
|
+
@agent.should_receive(:increment).with("jobs.error", 1)
|
140
|
+
|
141
|
+
# typically Metrician would be loaded in an initalizer
|
142
|
+
# and this _extend_ could be done inside the job itself, but here
|
143
|
+
# we are in a weird situation.
|
144
|
+
TestResqueJob.send(:extend, Metrician::Jobs::ResquePlugin)
|
145
|
+
lambda { Resque.enqueue(TestResqueJob, { "error" => true }) }.should raise_error(StandardError)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "sidekiq" do
|
150
|
+
before do
|
151
|
+
Sidekiq::Testing.inline!
|
152
|
+
@agent = Metrician.null_agent
|
153
|
+
Metrician.activate(@agent)
|
154
|
+
# sidekiq doesn't use middleware by design in their testing
|
155
|
+
# harness, so we add it just as metrician does
|
156
|
+
# https://github.com/mperham/sidekiq/wiki/Testing#testing-server-middleware
|
157
|
+
Sidekiq::Testing.server_middleware do |chain|
|
158
|
+
chain.add Metrician::Jobs::SidekiqMiddleware
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
after do
|
163
|
+
Sidekiq::Testing.disable!
|
164
|
+
end
|
165
|
+
|
166
|
+
specify "Sidekiq is instrumented" do
|
167
|
+
@agent.stub(:gauge)
|
168
|
+
@agent.should_receive(:gauge).with("jobs.run", anything)
|
169
|
+
|
170
|
+
# avoid load order error of sidekiq here by just including the
|
171
|
+
# worker bits at latest possible time
|
172
|
+
TestSidekiqWorker.perform_async({ "success" => true})
|
173
|
+
end
|
174
|
+
|
175
|
+
specify "job errors are instrumented" do
|
176
|
+
@agent.stub(:increment)
|
177
|
+
@agent.should_receive(:increment).with("jobs.error", 1)
|
178
|
+
|
179
|
+
# avoid load order error of sidekiq here by just including the
|
180
|
+
# worker bits at latest possible time
|
181
|
+
lambda { TestSidekiqWorker.perform_async({ "error" => true}) }.should raise_error(StandardError)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "cache systems" do
|
187
|
+
specify "redis is instrumented" do
|
188
|
+
agent = Metrician.null_agent
|
189
|
+
Metrician.activate(agent)
|
190
|
+
client = Redis.new
|
191
|
+
agent.stub(:gauge)
|
192
|
+
agent.should_receive(:gauge).with("cache.command", anything)
|
193
|
+
client.get("foo-#{rand(100_000)}")
|
194
|
+
end
|
195
|
+
|
196
|
+
memcached_clients = [
|
197
|
+
defined?(::Memcached) && ::Memcached.new("localhost:11211"),
|
198
|
+
defined?(::Dalli::Client) && ::Dalli::Client.new("localhost:11211"),
|
199
|
+
].compact
|
200
|
+
raise "no memcached client" if memcached_clients.empty?
|
201
|
+
|
202
|
+
memcached_clients.each do |client|
|
203
|
+
specify "memcached is instrumented (#{client.class.name})" do
|
204
|
+
agent = Metrician.null_agent
|
205
|
+
Metrician.activate(agent)
|
206
|
+
agent.stub(:gauge)
|
207
|
+
|
208
|
+
agent.should_receive(:gauge).with("cache.command", anything)
|
209
|
+
begin
|
210
|
+
client.get("foo-#{rand(100_000)}")
|
211
|
+
rescue Memcached::NotFound
|
212
|
+
# memcached raises this when there is no value for "foo-N" set
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe "external service timing" do
|
219
|
+
specify "Net::HTTP is instrumented" do
|
220
|
+
agent = Metrician.null_agent
|
221
|
+
Metrician.activate(agent)
|
222
|
+
agent.stub(:gauge)
|
223
|
+
|
224
|
+
agent.should_receive(:gauge).with("service.request", anything)
|
225
|
+
Net::HTTP.get(URI.parse("http://example.com/"))
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
describe "request timing" do
|
230
|
+
include Rack::Test::Methods
|
231
|
+
|
232
|
+
let(:agent) { Metrician.null_agent }
|
233
|
+
|
234
|
+
describe "success case" do
|
235
|
+
def app
|
236
|
+
require "metrician/middleware/request_timing"
|
237
|
+
require "metrician/middleware/application_timing"
|
238
|
+
Rack::Builder.app do
|
239
|
+
use Metrician::Middleware::RequestTiming
|
240
|
+
use Metrician::Middleware::ApplicationTiming
|
241
|
+
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
specify "Rack timing is instrumented" do
|
246
|
+
agent.stub(:gauge)
|
247
|
+
|
248
|
+
agent.should_receive(:gauge).with("web.request", anything)
|
249
|
+
get "/"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
describe "error case" do
|
254
|
+
def app
|
255
|
+
require "metrician/middleware/request_timing"
|
256
|
+
require "metrician/middleware/application_timing"
|
257
|
+
Rack::Builder.app do
|
258
|
+
use Metrician::Middleware::RequestTiming
|
259
|
+
use Metrician::Middleware::ApplicationTiming
|
260
|
+
run lambda { |env| [500, {'Content-Type' => 'text/plain'}, ['BOOM']] }
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
specify "500s are instrumented for error tracking" do
|
265
|
+
agent.stub(:gauge)
|
266
|
+
agent.stub(:increment)
|
267
|
+
|
268
|
+
agent.should_receive(:gauge).with("web.request", anything)
|
269
|
+
agent.should_receive(:increment).with("web.error", 1)
|
270
|
+
get "/"
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
describe "queueing timing" do
|
275
|
+
def app
|
276
|
+
require "metrician/middleware/request_timing"
|
277
|
+
require "metrician/middleware/application_timing"
|
278
|
+
Rack::Builder.app do
|
279
|
+
use Metrician::Middleware::RequestTiming
|
280
|
+
use Metrician::Middleware::ApplicationTiming
|
281
|
+
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
specify "Queue timing is instrumented" do
|
286
|
+
Metrician.configuration[:request_timing][:queue_time][:enabled] = true
|
287
|
+
agent.stub(:gauge)
|
288
|
+
|
289
|
+
agent.should_receive(:gauge).with("web.queue_time", anything)
|
290
|
+
get "/", {}, { Metrician::Middleware::ENV_QUEUE_START_KEYS.first => 1.second.ago.to_f }
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
describe "apdex" do
|
295
|
+
|
296
|
+
let(:agent) { Metrician.null_agent }
|
297
|
+
|
298
|
+
describe "fast" do
|
299
|
+
def app
|
300
|
+
require "metrician/middleware/request_timing"
|
301
|
+
require "metrician/middleware/application_timing"
|
302
|
+
Rack::Builder.app do
|
303
|
+
use Metrician::Middleware::RequestTiming
|
304
|
+
use Metrician::Middleware::ApplicationTiming
|
305
|
+
# This SHOULD be fast enough to fit under our
|
306
|
+
# default threshold of 2.5s :)
|
307
|
+
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
specify "satisfied is recorded" do
|
312
|
+
agent.stub(:gauge)
|
313
|
+
|
314
|
+
agent.should_receive(:gauge).with("web.apdex.satisfied", anything)
|
315
|
+
agent.should_not_receive(:gauge).with("web.apdex.tolerated", anything)
|
316
|
+
agent.should_not_receive(:gauge).with("web.apdex.frustrated", anything)
|
317
|
+
get "/"
|
318
|
+
end
|
319
|
+
|
320
|
+
end
|
321
|
+
|
322
|
+
describe "medium" do
|
323
|
+
def app
|
324
|
+
require "metrician/middleware/request_timing"
|
325
|
+
require "metrician/middleware/application_timing"
|
326
|
+
Rack::Builder.app do
|
327
|
+
use Metrician::Middleware::RequestTiming
|
328
|
+
use Metrician::Middleware::ApplicationTiming
|
329
|
+
run ->(env) {
|
330
|
+
env[Metrician::Middleware::ENV_REQUEST_TOTAL_TIME] = 3.0 # LOAD-BEARING
|
331
|
+
[200, {'Content-Type' => 'text/plain'}, ['OK']]
|
332
|
+
}
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
specify "tolerated is recorded" do
|
337
|
+
agent.stub(:gauge)
|
338
|
+
|
339
|
+
agent.should_not_receive(:gauge).with("web.apdex.satisfied", anything)
|
340
|
+
agent.should_receive(:gauge).with("web.apdex.tolerated", anything)
|
341
|
+
agent.should_not_receive(:gauge).with("web.apdex.frustrated", anything)
|
342
|
+
get "/"
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
describe "slow" do
|
347
|
+
def app
|
348
|
+
require "metrician/middleware/request_timing"
|
349
|
+
require "metrician/middleware/application_timing"
|
350
|
+
Rack::Builder.app do
|
351
|
+
use Metrician::Middleware::RequestTiming
|
352
|
+
use Metrician::Middleware::ApplicationTiming
|
353
|
+
run ->(env) {
|
354
|
+
env[Metrician::Middleware::ENV_REQUEST_TOTAL_TIME] = 28.0 # LOAD-BEARING
|
355
|
+
[200, {'Content-Type' => 'text/plain'}, ['OK']]
|
356
|
+
}
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
specify "frustrated is recorded" do
|
361
|
+
agent.stub(:gauge)
|
362
|
+
|
363
|
+
agent.should_not_receive(:gauge).with("web.apdex.satisfied", anything)
|
364
|
+
agent.should_not_receive(:gauge).with("web.apdex.tolerated", anything)
|
365
|
+
agent.should_receive(:gauge).with("web.apdex.frustrated", anything)
|
366
|
+
get "/"
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
ENV["RAILS_ENV"] = "test"
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
Bundler.require(:default)
|
5
|
+
require "metrician"
|
6
|
+
require "gemika"
|
7
|
+
require "byebug"
|
8
|
+
|
9
|
+
|
10
|
+
Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each {|f| require f}
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
# Enable flags like --only-failures and --next-failure
|
14
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
15
|
+
|
16
|
+
config.expect_with :rspec do |c|
|
17
|
+
c.syntax = :should
|
18
|
+
end
|
19
|
+
|
20
|
+
config.mock_with :rspec do |mocks|
|
21
|
+
mocks.syntax = :should
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
database = Gemika::Database.new
|
2
|
+
database.connect
|
3
|
+
|
4
|
+
if Gemika::Env.gem?('activerecord', '< 5')
|
5
|
+
class ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
6
|
+
NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
database.rewrite_schema! do
|
11
|
+
|
12
|
+
create_table :users do |t|
|
13
|
+
t.string :name
|
14
|
+
t.string :email
|
15
|
+
t.string :city
|
16
|
+
end
|
17
|
+
|
18
|
+
create_table :delayed_jobs, force: true do |table|
|
19
|
+
table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue
|
20
|
+
table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually.
|
21
|
+
table.text :handler, null: false # YAML-encoded string of the object that will do work
|
22
|
+
table.text :last_error # reason for last failure (See Note below)
|
23
|
+
table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
|
24
|
+
table.datetime :locked_at # Set when a client is working on this object
|
25
|
+
table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
|
26
|
+
table.string :locked_by # Who is working on this object (if locked)
|
27
|
+
table.string :queue # The name of the queue this job is in
|
28
|
+
table.timestamps null: true
|
29
|
+
end
|
30
|
+
|
31
|
+
add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority"
|
32
|
+
|
33
|
+
end
|