queue_classic_plus 0.0.2 → 1.0.0.alpha2

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
  SHA1:
3
- metadata.gz: 8acdaf6cd41362f50d7ddb9c50fa87555c00adc9
4
- data.tar.gz: 58c26d31a05c2390504975b6a7513179f0397ede
3
+ metadata.gz: f5b1934a03303e576a999e71f978a68da13f0298
4
+ data.tar.gz: d1de5250aa7b2ddfe4114043d87e5a95986145f8
5
5
  SHA512:
6
- metadata.gz: 453a051c64c4c539f488f9ea7f0c01da9ac326de11891746b6b0abea14bc4103bb347afd4020bedb0b37c0262d1c1a983659f39c0b35909521db6af2951dcacb
7
- data.tar.gz: 37263c41d6929a351e74920a8abe5d2d4d3bbc3c5c9bdb941f4cd2e6758ef2443ddf580dd16298124809ca0232ce7df02b3533fac75fef8fa911f21c45ecb597
6
+ metadata.gz: ebadc9325c0307789f6a6216047a2c2cfe240d4364b613ee9eefc9a479224a9196813146b90ed251124cd5ad7a4966556c8fca8e1b48a490cfd806bf95124ff3
7
+ data.tar.gz: 619c736ed7b74f94841328acfa27926e2799fecc6589974d9b79de236c1f396ef126d1e80205df223e626536b69c1c0a834014e09e190b000635e571c2a8e677
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ before_script:
3
+ - psql -c 'create database queue_classic_plus_test;' -U postgres
4
+
5
+ rvm:
6
+ - 2.0.0
7
+ - 2.1.5
8
+ - 2.2.0
data/Gemfile CHANGED
@@ -3,9 +3,15 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in queue_classic_plus.gemspec
4
4
  gemspec
5
5
 
6
+ gem "queue_classic_matchers", github: 'rainforestapp/queue_classic_matchers'
7
+ gem 'pry'
6
8
 
7
- gem 'rspec'
8
- gem 'pg'
9
- gem 'timecop'
10
- gem 'queue_classic-later', github: 'jipiboily/queue_classic-later', branch: "add-qc-3-to-custom-columns" # This is until the 3.0 work is merged into original repo
11
- gem 'queue_classic_matchers'
9
+ group :development do
10
+ gem "guard-rspec", require: false
11
+ gem "terminal-notifier-guard"
12
+ end
13
+
14
+ group :test do
15
+ gem 'rspec'
16
+ gem 'timecop'
17
+ end
@@ -0,0 +1,77 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec feature)
6
+
7
+ ## Uncomment to clear the screen before every task
8
+ # clearing :on
9
+
10
+ ## Guard internally checks for changes in the Guardfile and exits.
11
+ ## If you want Guard to automatically start up again, run guard in a
12
+ ## shell loop, e.g.:
13
+ ##
14
+ ## $ while bundle exec guard; do echo "Restarting Guard..."; done
15
+ ##
16
+ ## Note: if you are using the `directories` clause above and you are not
17
+ ## watching the project directory ('.'), the you will want to move the Guardfile
18
+ ## to a watched dir and symlink it back, e.g.
19
+ #
20
+ # $ mkdir config
21
+ # $ mv Guardfile config/
22
+ # $ ln -s config/Guardfile .
23
+ #
24
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
25
+
26
+ # Note: The cmd option is now required due to the increasing number of ways
27
+ # rspec may be run, below are examples of the most common uses.
28
+ # * bundler: 'bundle exec rspec'
29
+ # * bundler binstubs: 'bin/rspec'
30
+ # * spring: 'bin/rspec' (This will use spring if running and you have
31
+ # installed the spring binstubs per the docs)
32
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
33
+ # * 'just' rspec: 'rspec'
34
+
35
+ guard :rspec, cmd: "bundle exec rspec" do
36
+ require "guard/rspec/dsl"
37
+ dsl = Guard::RSpec::Dsl.new(self)
38
+
39
+ # Feel free to open issues for suggestions and improvements
40
+
41
+ # RSpec files
42
+ rspec = dsl.rspec
43
+ watch(rspec.spec_helper) { rspec.spec_dir }
44
+ watch(rspec.spec_support) { rspec.spec_dir }
45
+ watch(rspec.spec_files)
46
+
47
+ # Ruby files
48
+ ruby = dsl.ruby
49
+ dsl.watch_spec_files_for(ruby.lib_files)
50
+
51
+ # Rails files
52
+ rails = dsl.rails(view_extensions: %w(erb haml slim))
53
+ dsl.watch_spec_files_for(rails.app_files)
54
+ dsl.watch_spec_files_for(rails.views)
55
+
56
+ watch(rails.controllers) do |m|
57
+ [
58
+ rspec.spec.("routing/#{m[1]}_routing"),
59
+ rspec.spec.("controllers/#{m[1]}_controller"),
60
+ rspec.spec.("acceptance/#{m[1]}")
61
+ ]
62
+ end
63
+
64
+ # Rails config changes
65
+ watch(rails.spec_helper) { rspec.spec_dir }
66
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
67
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
68
+
69
+ # Capybara features specs
70
+ watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") }
71
+
72
+ # Turnip features and steps
73
+ watch(%r{^spec/acceptance/(.+)\.feature$})
74
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
75
+ Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
76
+ end
77
+ end
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # QueueClassicPlus
2
2
 
3
- QueueClassic is a simple Postgresql back DB queue. However, it's a little too simple to use it as the main queueing system of a medium to large app.
3
+ [![Build Status](https://travis-ci.org/rainforestapp/queue_classic_plus.svg?branch=master)](https://travis-ci.org/rainforestapp/queue_classic_plus)
4
+
5
+ [QueueClassic](https://github.com/QueueClassic/queue_classic) is a simple Postgresql back DB queue. However, it's a little too simple to use it as the main queueing system of a medium to large app.
4
6
 
5
7
  QueueClassicPlus adds many lacking features to QueueClassic.
6
8
 
@@ -10,13 +12,17 @@ QueueClassicPlus adds many lacking features to QueueClassic.
10
12
  - Metrics
11
13
  - Error logging / handling
12
14
  - Transactions
15
+ - Rails generator to create new jobs
16
+
17
+ ## Compatibility
18
+
19
+ This version of the matchers are compatible with queue_classic 3.1+ which includes built-in scheduling. See other branches for other compatible versions.
13
20
 
14
21
  ## Installation
15
22
 
16
23
  Add these line to your application's Gemfile:
17
24
 
18
25
  gem 'queue_classic_plus'
19
- gem "queue_classic-later", github: "dpiddy/queue_classic-later"
20
26
 
21
27
  And then execute:
22
28
 
@@ -33,7 +39,7 @@ Run the migration
33
39
  ### Create a new job
34
40
 
35
41
  ```bash
36
- rails g my_job test_job
42
+ rails g qc_plus_job test_job
37
43
  ```
38
44
 
39
45
  ```ruby
@@ -54,17 +60,67 @@ end
54
60
  Jobs::MyJob.do(1, "foo")
55
61
 
56
62
  # You can also schedule a job in the future by doing
57
-
58
63
  Jobs::MyJob.enqueue_perform_in(1.hour, 1, "foo")
59
64
  ```
60
65
 
61
66
  ### Run the QueueClassicPlus worker
62
67
 
68
+ QueueClassicPlus ships with its own worker and a rake task to run it. You need to use this worker to take advance of many features of QueueClassicPlus.
69
+
63
70
  ```
64
71
  QUEUE=low bundle exec qc_plus:work
65
72
  ```
66
73
 
67
- ## Advance configuration
74
+ ### Other jobs options
75
+
76
+ #### Singleton Job
77
+
78
+ It's common for background jobs to never need to be enqueed multiple time. QueueClassicPlus support these type of single jobs. Here's an example one:
79
+
80
+ ```ruby
81
+ class Jobs::UpdateMetrics < QueueClassicPlus::Base
82
+ @queue = :low
83
+
84
+ # Use the lock! keyword to prevent the job from being enqueud once.
85
+ lock!
86
+
87
+ def self.perform(metric_type)
88
+ # ...
89
+ end
90
+ end
91
+
92
+ ```
93
+
94
+ Note that `lock!` only prevents the same job from beeing enqued multiple times if the argument match.
95
+
96
+ So in our example:
97
+
98
+ ```ruby
99
+ Jobs::UpdateMetrics.do 'type_a' # enqueues job
100
+ Jobs::UpdateMetrics.do 'type_a' # does not enqueues job since it's already queued
101
+ Jobs::UpdateMetrics.do 'type_b' # enqueues job as the arguments are different.
102
+ ```
103
+
104
+ ```ruby
105
+ class Jobs::NoTransaction < QueueClassicPlus::Base
106
+ # Don't run the perform method in a transaction
107
+ skip_transaction!
108
+
109
+ @queue = :low
110
+
111
+ def self.perform(user_id)
112
+ # ...
113
+ end
114
+ end
115
+ ```
116
+
117
+ #### Transaction
118
+
119
+ By default, all QueueClassicPlus jobs are executed in a PostgreSQL transaction. This decision was made because most jobs are usually pretty small and it's preferable to have all the benefits of the transaction.
120
+
121
+ You can disable this feature on a per job basis in the follwing way:
122
+
123
+ ## Advanced configuration
68
124
 
69
125
  If you want to log exceptions in your favorite exception tracker. You can configured it like sso:
70
126
 
@@ -84,14 +140,22 @@ QueueClassicPlus.update_metrics
84
140
 
85
141
  Call this is a cron job or something similar.
86
142
 
87
- ## TODO
143
+ If you are using NewRelic and want to push performance data to it, you can add this to an initializer:
88
144
 
89
- - Remove dep on ActiveRecord
145
+ ```ruby
146
+ require "queue_classic_plus/new_relic"
147
+ ```
90
148
 
91
149
  ## Contributing
92
150
 
93
151
  1. Fork it ( https://github.com/[my-github-username]/queue_classic_plus/fork )
94
- 2. Create your feature branch (`git checkout -b my-new-feature`)
95
- 3. Commit your changes (`git commit -am 'Add some feature'`)
96
- 4. Push to the branch (`git push origin my-new-feature`)
97
- 5. Create a new Pull Request
152
+ - Create your feature branch (`git checkout -b my-new-feature`)
153
+ - Commit your changes (`git commit -am 'Add some feature'`)
154
+ - Push to the branch (`git push origin my-new-feature`)
155
+ - Create a new Pull Request
156
+
157
+ ### Setting up the test database
158
+
159
+ ```
160
+ createdb queue_classic_plus_test
161
+ ```
data/Rakefile CHANGED
@@ -1,2 +1,11 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
9
+ rescue LoadError
10
+ # no rspec available
11
+ end
@@ -1,14 +1,14 @@
1
+ require 'logger'
1
2
  require "queue_classic"
2
- require "queue_classic/later"
3
- require "active_record"
4
- require "with_advisory_lock"
5
- require "active_support/core_ext/class/attribute"
6
3
 
7
4
  require "queue_classic_plus/version"
5
+ require "queue_classic_plus/inheritable_attr"
6
+ require "queue_classic_plus/inflector"
8
7
  require "queue_classic_plus/metrics"
9
8
  require "queue_classic_plus/update_metrics"
10
9
  require "queue_classic_plus/base"
11
10
  require "queue_classic_plus/worker"
11
+ require "queue_classic_plus/queue_classic/queue"
12
12
 
13
13
  module QueueClassicPlus
14
14
  require 'queue_classic_plus/railtie' if defined?(Rails)
@@ -17,14 +17,12 @@ module QueueClassicPlus
17
17
  conn = QC::ConnAdapter.new(c)
18
18
  conn.execute("ALTER TABLE queue_classic_jobs ADD COLUMN last_error TEXT")
19
19
  conn.execute("ALTER TABLE queue_classic_jobs ADD COLUMN remaining_retries INTEGER")
20
- conn.execute("ALTER TABLE queue_classic_later_jobs ADD COLUMN remaining_retries INTEGER")
21
20
  end
22
21
 
23
22
  def self.demigrate(c = QC::default_conn_adapter.connection)
24
23
  conn = QC::ConnAdapter.new(c)
25
24
  conn.execute("ALTER TABLE queue_classic_jobs DROP COLUMN last_error")
26
25
  conn.execute("ALTER TABLE queue_classic_jobs DROP COLUMN remaining_retries")
27
- conn.execute("ALTER TABLE queue_classic_later_jobs DROP COLUMN remaining_retries")
28
26
  end
29
27
 
30
28
  def self.exception_handler
@@ -1,13 +1,15 @@
1
1
  module QueueClassicPlus
2
2
  class Base
3
+ extend QueueClassicPlus::InheritableAttribute
4
+
3
5
  def self.queue
4
6
  QC::Queue.new(@queue)
5
7
  end
6
8
 
7
- class_attribute :locked
8
- class_attribute :skip_transaction
9
- class_attribute :retries_on
10
- class_attribute :max_retries
9
+ inheritable_attr :locked
10
+ inheritable_attr :skip_transaction
11
+ inheritable_attr :retries_on
12
+ inheritable_attr :max_retries
11
13
 
12
14
  self.max_retries = 0
13
15
  self.retries_on = {}
@@ -39,35 +41,21 @@ module QueueClassicPlus
39
41
 
40
42
  def self.can_enqueue?(method, *args)
41
43
  if locked?
42
- lock_key = [@queue, method, args].to_json
43
44
  max_lock_time = ENV.fetch("QUEUE_CLASSIC_MAX_LOCK_TIME", 10 * 60).to_i
44
45
 
45
- ActiveRecord::Base.with_advisory_lock(lock_key, 0.1) do
46
- q = "SELECT COUNT(1) AS count
47
- FROM
46
+ q = "SELECT COUNT(1) AS count
47
+ FROM
48
48
  (
49
- (
50
49
  SELECT 1
51
50
  FROM queue_classic_jobs
52
51
  WHERE q_name = $1 AND method = $2 AND args::text = $3::text
53
52
  AND (locked_at IS NULL OR locked_at > current_timestamp - interval '#{max_lock_time} seconds')
54
53
  LIMIT 1
55
- )
56
-
57
- UNION
58
-
59
- (
60
- SELECT 1
61
- FROM queue_classic_later_jobs
62
- WHERE q_name = $4 AND method = $5 AND args = $6::text
63
- LIMIT 1
64
- )
65
54
  )
66
- AS x"
55
+ AS x"
67
56
 
68
- result = QC.default_conn_adapter.execute(q, @queue, method, args.to_json, @queue, method, args.to_json)
69
- result['count'].to_i == 0
70
- end
57
+ result = QC.default_conn_adapter.execute(q, @queue, method, args.to_json)
58
+ result['count'].to_i == 0
71
59
  else
72
60
  true
73
61
  end
@@ -89,7 +77,7 @@ module QueueClassicPlus
89
77
  end
90
78
 
91
79
  def self.restart_in(time, remaining_retries, *args)
92
- queue.enqueue_in_with_custom(time, "#{self.to_s}._perform", {'remaining_retries' => remaining_retries}, *args)
80
+ queue.enqueue_retry_in(time, "#{self.to_s}._perform", remaining_retries, *args)
93
81
  end
94
82
 
95
83
  def self.do(*args)
@@ -111,17 +99,36 @@ module QueueClassicPlus
111
99
  end
112
100
 
113
101
  def self.librato_key
114
- (self.name || "").underscore.gsub(/\//, ".")
102
+ Inflector.underscore(self.name || "").gsub(/\//, ".")
115
103
  end
116
104
 
117
105
  def self.transaction(options = {}, &block)
118
- ActiveRecord::Base.transaction(options, &block)
106
+ if defined?(ActiveRecord)
107
+ # If ActiveRecord is loaded, we use it's own transaction mechanisn since
108
+ # it has slightly different semanctics for rollback.
109
+ ActiveRecord::Base.transaction(options, &block)
110
+ else
111
+ begin
112
+ execute "BEGIN"
113
+ block.call
114
+ rescue
115
+ execute "ROLLBACK"
116
+ raise
117
+ end
118
+
119
+ execute "COMMIT"
120
+ end
119
121
  end
120
122
 
121
123
  # Debugging
122
124
  def self.list
123
125
  q = "SELECT * FROM queue_classic_jobs WHERE q_name = '#{@queue}'"
124
- ActiveRecord::Base.connection.execute(q).to_a
126
+ execute q
127
+ end
128
+
129
+ private
130
+ def self.execute(sql, *args)
131
+ QC.default_conn_adapter.execute(sql, *args)
125
132
  end
126
133
  end
127
134
  end
@@ -0,0 +1,15 @@
1
+ module QueueClassicPlus
2
+ module Inflector
3
+ # Storngly inspired by
4
+ # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L91
5
+ def self.underscore(camel_cased_word)
6
+ return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
7
+ word = camel_cased_word.to_s.gsub(/::/, '/')
8
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
9
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
10
+ word.tr!("-", "_")
11
+ word.downcase!
12
+ word
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module QueueClassicPlus
2
+ # From https://github.com/apotonick/uber/blob/master/lib/uber/inheritable_attr.rb which is MIT license
3
+ module InheritableAttribute
4
+ def inheritable_attr(name)
5
+ instance_eval %Q{
6
+ def #{name}=(v)
7
+ @#{name} = v
8
+ end
9
+ def #{name}
10
+ return @#{name} if instance_variable_defined?(:@#{name})
11
+ @#{name} = InheritableAttribute.inherit_for(self, :#{name})
12
+ end
13
+ }
14
+ end
15
+
16
+ def self.inherit_for(klass, name)
17
+ return unless klass.superclass.respond_to?(name)
18
+
19
+ value = klass.superclass.send(name) # could be nil.
20
+ Clone.(value) # this could be dynamic, allowing other inheritance strategies.
21
+ end
22
+
23
+ class Clone
24
+ # The second argument allows injecting more types.
25
+ def self.call(value, uncloneable=uncloneable())
26
+ uncloneable.each { |klass| return value if value.kind_of?(klass) }
27
+ value.clone
28
+ end
29
+
30
+ def self.uncloneable
31
+ [Symbol, TrueClass, FalseClass, NilClass]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ require 'new_relic/agent/method_tracer'
2
+
3
+ QueueClassicPlus::Base.class_eval do
4
+ class << self
5
+ include NewRelic::Agent::Instrumentation::ControllerInstrumentation
6
+
7
+ def new_relic_key
8
+ "Custom/QueueClassicPlus/#{librato_key}"
9
+ end
10
+
11
+ def _perform_with_new_relic(*args)
12
+ opts = {
13
+ name: 'perform',
14
+ class_name: self.name,
15
+ category: 'OtherTransaction/QueueClassicPlus',
16
+ }
17
+
18
+ perform_action_with_newrelic_trace(opts) do
19
+ if NewRelic::Agent.config[:'queue_classic_plus.capture_params']
20
+ NewRelic::Agent.add_custom_parameters(job_arguments: args)
21
+ end
22
+ _perform_without_new_relic *args
23
+ end
24
+ end
25
+
26
+ alias_method_chain :_perform, :new_relic
27
+ end
28
+ end
29
+
30
+ QueueClassicPlus::CustomWorker.class_eval do
31
+ def initialize(*)
32
+ ::NewRelic::Agent.manual_start
33
+ super
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ module QC
2
+ class Queue
3
+ def enqueue_retry_in(seconds, method, remaining_retries, *args)
4
+ QC.log_yield(:measure => 'queue.enqueue') do
5
+ s = "INSERT INTO #{TABLE_NAME} (q_name, method, args, scheduled_at, remaining_retries)
6
+ VALUES ($1, $2, $3, now() + interval '#{seconds.to_i} seconds', $4)"
7
+ res = conn_adapter.execute(s, name, method, JSON.dump(args), remaining_retries)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,62 +1,83 @@
1
1
  module QueueClassicPlus
2
2
  module UpdateMetrics
3
3
  def self.update
4
- {
5
- "queued" => "queue_classic_jobs",
6
- "scheduled" => "queue_classic_later_jobs",
7
- }.each do |type, table|
8
- next unless ActiveRecord::Base.connection.table_exists?(table)
9
- q = "SELECT q_name, COUNT(1) FROM #{table} GROUP BY q_name"
10
- results = execute(q)
11
-
12
- # Log individual queue sizes
13
- results.each do |h|
14
- Metrics.measure("qc.jobs_#{type}", h.fetch('count').to_i, source: h.fetch('q_name'))
4
+ metrics.each do |name, values|
5
+ if values.respond_to?(:each)
6
+ values.each do |hash|
7
+ hash.to_a.each do |(source, count)|
8
+ Metrics.measure("qc.#{name}", count, source: source)
9
+ end
10
+ end
11
+ else
12
+ Metrics.measure("qc.#{name}", values)
15
13
  end
16
14
  end
15
+ end
17
16
 
18
- # Log oldest locked_at and created_at
19
- ['locked_at', 'created_at'].each do |column|
20
- age = max_age(column)
21
- Metrics.measure("qc.max_#{column}", age)
22
- end
17
+ def self.metrics
18
+ {
19
+ jobs_queued: jobs_queued,
20
+ jobs_scheduled: jobs_scheduled,
21
+ max_created_at: max_age("created_at", "created_at = scheduled_at"),
22
+ max_locked_at: max_age("locked_at", "locked_at IS NOT NULL"),
23
+ "max_created_at.unlocked" => max_age("locked_at", "locked_at IS NULL"),
24
+ "jobs_delayed.lag" => max_age("scheduled_at"),
25
+ "jobs_delayed.late_count" => late_count,
26
+ }
27
+ end
23
28
 
24
- # Log oldes unlocked jobs
25
- age = max_age("created_at", "locked_at IS NULL")
26
- Metrics.measure("qc.max_created_at.unlocked", age)
29
+ def self.jobs_queued
30
+ sql_group_count "SELECT q_name AS group, COUNT(1)
31
+ FROM queue_classic_jobs
32
+ WHERE scheduled_at <= NOW() GROUP BY q_name"
33
+ end
27
34
 
28
- if ActiveRecord::Base.connection.table_exists?('queue_classic_later_jobs')
29
- lag = execute("SELECT MAX(EXTRACT(EPOCH FROM now() - not_before)) AS lag
30
- FROM queue_classic_later_jobs").first
31
- lag = lag ? lag['lag'] : 0
35
+ def self.jobs_scheduled
36
+ sql_group_count "SELECT q_name AS group, COUNT(1)
37
+ FROM queue_classic_jobs
38
+ WHERE scheduled_at > NOW() GROUP BY q_name"
39
+ end
32
40
 
33
- Metrics.measure("qc.jobs_delayed.lag", lag.to_f)
41
+ def self.late_count
42
+ nb_late = execute("SELECT COUNT(1)
43
+ FROM queue_classic_jobs
44
+ WHERE scheduled_at < NOW() AND #{not_failed}")
45
+ nb_late ? Integer(nb_late['count']) : 0
46
+ end
34
47
 
35
- nb_late = execute("SELECT COUNT(1)
36
- FROM queue_classic_later_jobs
37
- WHERE not_before < NOW()").first
38
- nb_late = nb_late ? nb_late['count'] : 0
48
+ private
39
49
 
40
- Metrics.measure("qc.jobs_delayed.late_count", nb_late.to_i)
50
+ def self.sql_group_count(sql)
51
+ results = execute(sql)
52
+ results = [results] if Hash === results
53
+ Array(results).map do |h|
54
+ {
55
+ h.fetch("group") => Integer(h.fetch('count'))
56
+ }
41
57
  end
42
58
  end
43
59
 
44
- private
45
60
  def self.max_age(column, *conditions)
46
- conditions.unshift "q_name != '#{::QueueClassicPlus::CustomWorker::FailedQueue.name}'"
61
+ conditions.unshift not_failed
62
+ conditions.unshift "scheduled_at <= NOW()"
47
63
 
48
- q = "SELECT EXTRACT(EPOCH FROM now() - #{column}) AS age_in_seconds
64
+ q = "SELECT EXTRACT(EPOCH FROM now() - #{column}) AS age_in_seconds
49
65
  FROM queue_classic_jobs
50
66
  WHERE #{conditions.join(" AND ")}
51
67
  ORDER BY age_in_seconds DESC
68
+ LIMIT 1
52
69
  "
53
- age_info = execute(q).first
70
+ age_info = execute(q)
54
71
 
55
72
  age_info ? age_info['age_in_seconds'].to_i : 0
56
73
  end
57
74
 
58
75
  def self.execute(q)
59
- ActiveRecord::Base.connection.execute(q)
76
+ QC.default_conn_adapter.execute(q)
77
+ end
78
+
79
+ def self.not_failed
80
+ "q_name != '#{::QueueClassicPlus::CustomWorker::FailedQueue.name}'"
60
81
  end
61
82
  end
62
83
  end
@@ -1,3 +1,3 @@
1
1
  module QueueClassicPlus
2
- VERSION = "0.0.2"
2
+ VERSION = "1.0.0.alpha2"
3
3
  end
@@ -1,18 +1,12 @@
1
1
  module QueueClassicPlus
2
2
  class CustomWorker < QC::Worker
3
- class QueueClassicJob < ActiveRecord::Base
4
- end
5
-
6
3
  BACKOFF_WIDTH = 10
7
4
  FailedQueue = QC::Queue.new("failed_jobs")
8
5
 
9
6
  def enqueue_failed(job, e)
10
- ActiveRecord::Base.transaction do
11
- FailedQueue.enqueue(job[:method], *job[:args])
12
- new_job = QueueClassicJob.order(:id).where(q_name: 'failed_jobs').last
13
- new_job.last_error = if e.backtrace then ([e.message] + e.backtrace ).join("\n") else e.message end
14
- new_job.save!
15
- end
7
+ sql = "INSERT INTO #{QC::TABLE_NAME} (q_name, method, args, last_error) VALUES ('failed_jobs', $1, $2, $3)"
8
+ last_error = e.backtrace ? ([e.message] + e.backtrace ).join("\n") : e.message
9
+ QC.default_conn_adapter.execute sql, job[:method], JSON.dump(job[:args]), last_error
16
10
 
17
11
  QueueClassicPlus.exception_handler.call(e, job)
18
12
  Metrics.increment("qc.errors", source: @q_name)
@@ -22,11 +16,9 @@ module QueueClassicPlus
22
16
  QueueClassicPlus.logger.info "Handling exception #{e.message} for job #{job[:id]}"
23
17
  klass = job_klass(job)
24
18
 
25
- model = QueueClassicJob.find(job[:id])
26
-
27
19
  # The mailers doesn't have a retries_on?
28
20
  if klass && klass.respond_to?(:retries_on?) && klass.retries_on?(e)
29
- remaining_retries = model.remaining_retries || klass.max_retries
21
+ remaining_retries = job[:remaining_retries] || klass.max_retries
30
22
  remaining_retries -= 1
31
23
 
32
24
  if remaining_retries > 0
@@ -39,7 +31,8 @@ module QueueClassicPlus
39
31
  else
40
32
  enqueue_failed(job, e)
41
33
  end
42
- model.destroy
34
+
35
+ FailedQueue.delete(job[:id])
43
36
  end
44
37
 
45
38
  private
@@ -18,11 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "queue_classic"
22
- spec.add_dependency "queue_classic-later"
23
- spec.add_dependency "activerecord", "> 3.0"
24
- spec.add_dependency "activesupport", "> 3.0"
25
- spec.add_dependency "with_advisory_lock"
21
+ spec.add_dependency "queue_classic", ">= 3.1.0"
26
22
  spec.add_development_dependency "bundler", "~> 1.6"
27
23
  spec.add_development_dependency "rake"
28
24
  end
@@ -17,9 +17,8 @@ describe QueueClassicPlus::Base do
17
17
 
18
18
  it "does allow multiple enqueues if something got locked for too long" do
19
19
  subject.do
20
- ActiveRecord::Base.connection.execute "
21
- UPDATE queue_classic_jobs SET locked_at = '#{1.day.ago.to_s}' WHERE q_name = 'test'
22
- "
20
+ one_day_ago = Time.now - 60*60*24
21
+ execute "UPDATE queue_classic_jobs SET locked_at = '#{one_day_ago}' WHERE q_name = 'test'"
23
22
  subject.do
24
23
  subject.should have_queue_size_of(2)
25
24
  end
@@ -40,8 +39,8 @@ describe QueueClassicPlus::Base do
40
39
  end
41
40
 
42
41
  it "calls perform in a transaction" do
43
- ActiveRecord::Base.should_receive(:transaction).and_call_original
44
- subject._perform
42
+ QueueClassicPlus::Base.should_receive(:transaction).and_call_original
43
+ subject._perform
45
44
  end
46
45
 
47
46
  it "measures the time" do
@@ -62,8 +61,8 @@ describe QueueClassicPlus::Base do
62
61
  end
63
62
 
64
63
  it "calls perform outside of a transaction" do
65
- ActiveRecord::Base.should_not_receive(:transaction)
66
- subject._perform
64
+ QueueClassicPlus::Base.should_not_receive(:transaction)
65
+ subject._perform
67
66
  end
68
67
  end
69
68
 
@@ -0,0 +1,9 @@
1
+ module QcHelpers
2
+ def execute(sql, *args)
3
+ QC.default_conn_adapter.execute(sql, *args)
4
+ end
5
+
6
+ def find_job(id)
7
+ execute("SELECT * FROM #{QC::TABLE_NAME} WHERE id = $1", id)
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ module QueueClassicPlus
4
+ describe Inflector do
5
+ describe ".underscore" do
6
+ {
7
+ "foo" => "foo",
8
+ "Foo" => "foo",
9
+ "FooBar" => "foo_bar",
10
+ "Foo::Bar" => "foo/bar"
11
+ }.each do |word, expected|
12
+ it "converst #{word} to #{expected}" do
13
+ expect(described_class.underscore(word)).to eq(expected)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -3,29 +3,22 @@ require 'pg'
3
3
  require 'timecop'
4
4
  require 'queue_classic_matchers'
5
5
  require_relative './sample_jobs'
6
+ require_relative './helpers'
7
+ require 'pry'
8
+
9
+ ENV["DATABASE_URL"] ||= "postgres:///queue_classic_plus_test"
6
10
 
7
11
  RSpec.configure do |config|
8
- config.before(:suite) do
9
- ActiveRecord::Base.establish_connection(
10
- :adapter => "postgresql",
11
- :username => "postgres",
12
- :database => "queue_classic_plus_test",
13
- :host => 'localhost',
14
- )
12
+ config.include QcHelpers
15
13
 
16
- ActiveRecord::Base.connection.execute "drop schema public cascade; create schema public;"
14
+ config.before(:suite) do
15
+ QC.default_conn_adapter.execute "drop schema public cascade; create schema public;"
17
16
 
18
- QC.default_conn_adapter = QC::ConnAdapter.new(ActiveRecord::Base.connection.raw_connection)
19
17
  QC::Setup.create
20
- QC::Later::Setup.create
21
18
  QueueClassicPlus.migrate
22
19
  end
23
20
 
24
21
  config.before(:each) do
25
- tables = ActiveRecord::Base.connection.tables.select do |table|
26
- table != "schema_migrations"
27
- end
28
- ActiveRecord::Base.connection.execute("TRUNCATE #{tables.join(', ')} CASCADE") unless tables.empty?
29
-
22
+ QC.default_conn_adapter.execute "TRUNCATE queue_classic_jobs;"
30
23
  end
31
24
  end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ describe QueueClassicPlus::UpdateMetrics do
4
+ describe ".update" do
5
+ it "works" do
6
+ QC.enqueue "puts"
7
+
8
+ expect(QueueClassicPlus::Metrics).to(receive(:measure)).at_least(1).times do |metric, value, options|
9
+ expect(metric).to_not be_nil
10
+ expect(value).to_not be_nil
11
+ end
12
+ described_class.update
13
+ end
14
+ end
15
+
16
+ describe ".metrics" do
17
+ subject { described_class.metrics }
18
+
19
+ before do
20
+ QC.enqueue "puts"
21
+ QC.enqueue "puts", 2
22
+ QC.enqueue_in 60, "puts", 2
23
+ end
24
+
25
+ context "jobs_queued" do
26
+ it "returns the number of jobs group per queue" do
27
+ expect(subject[:jobs_queued]).to eq([{"default" => 2}])
28
+ end
29
+ end
30
+
31
+ context "jobs_scheduled" do
32
+ it "returns the number of jobs group per queue" do
33
+ expect(subject[:jobs_scheduled]).to eq([{"default" => 1}])
34
+ end
35
+ end
36
+
37
+ context "max_locked_at" do
38
+ it "zero if nothing is locked" do
39
+ max = subject[:max_locked_at]
40
+ expect(max).to eq(0)
41
+ end
42
+
43
+ it "returns the age of the oldest lock" do
44
+ execute "UPDATE queue_classic_jobs SET locked_at = '#{Time.now - 60}'"
45
+
46
+ max = subject[:max_locked_at]
47
+ expect(59..61).to include(max)
48
+ end
49
+
50
+ context 'scheduled jobs' do
51
+ it 'reports the correct max_locked_at' do
52
+ execute "UPDATE queue_classic_jobs SET locked_at = '#{Time.now - 30}', scheduled_at = '#{Time.now - 60}', created_at = '#{Time.now - 5*60}'"
53
+
54
+ expect(subject[:max_locked_at]).to be_within(1).of(30)
55
+ end
56
+ end
57
+ end
58
+
59
+ context "max_created_at" do
60
+ it "returns a small positive value" do
61
+ max = subject[:max_created_at]
62
+ expect(0..0.2).to include(max)
63
+ end
64
+
65
+ context 'scheduled jobs' do
66
+ it "ignores jobs schedule in the future" do
67
+ execute "UPDATE queue_classic_jobs SET created_at = '#{Time.now - 60}', scheduled_at = '#{Time.now + 60}'"
68
+
69
+ max = subject[:max_created_at]
70
+ expect(0..0.2).to include(max)
71
+ end
72
+
73
+ it 'reports time only for jobs that were never scheduled for future' do
74
+ execute "DELETE FROM queue_classic_jobs"
75
+ QC.enqueue 'puts'
76
+ QC.enqueue_in 1, 'puts'
77
+ one_min_ago = Time.now - 60
78
+ execute "UPDATE queue_classic_jobs SET created_at = '#{one_min_ago}', scheduled_at = '#{one_min_ago}'"
79
+ expect(subject[:max_created_at]).to be_within(1).of(60)
80
+ end
81
+ end
82
+ end
83
+
84
+ context "max_created_at.unlocked" do
85
+ it "ignores lock jobs" do
86
+ execute "UPDATE queue_classic_jobs SET locked_at = '#{Time.now - 60}', created_at = '#{Time.now - 2*60}'"
87
+
88
+ max = subject["max_created_at.unlocked"]
89
+ expect(max).to eq(0)
90
+ end
91
+ end
92
+
93
+ context "jobs_delayed.lag" do
94
+ it "returns the maximum different between the scheduled time and now" do
95
+ execute "UPDATE queue_classic_jobs SET scheduled_at = '#{Time.now - 60}'"
96
+
97
+ lag = subject["jobs_delayed.lag"]
98
+ expect(lag).to be_within(1.0).of(60)
99
+ end
100
+
101
+ it "ignores jobs scheduled in the future" do
102
+ execute "UPDATE queue_classic_jobs SET scheduled_at = '#{Time.now + 60}'"
103
+
104
+ lag = subject["jobs_delayed.lag"]
105
+ expect(lag).to eq(0)
106
+ end
107
+
108
+ it "ignores the failed queue" do
109
+ execute "UPDATE queue_classic_jobs SET scheduled_at = '#{Time.now - 60}', q_name = 'failed_jobs'"
110
+
111
+ lag = subject["jobs_delayed.lag"]
112
+ expect(lag).to eq(0)
113
+ end
114
+ end
115
+
116
+ context "jobs_delayed.late_count" do
117
+ it "returns the jobs that a created in the future" do
118
+ count = subject["jobs_delayed.late_count"]
119
+ # All jobs are always late because enqueue sets the value of
120
+ # scheduled_at to now() for normal jobs
121
+ expect(count).to eq(2)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -1,12 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe QueueClassicPlus::CustomWorker do
4
- class QueueClassicLaterJob < ActiveRecord::Base
5
- end
6
-
7
- class QueueClassicJob < ActiveRecord::Base
8
- end
9
-
10
4
  let(:failed_queue) { described_class::FailedQueue }
11
5
 
12
6
  context "failure" do
@@ -21,7 +15,9 @@ describe QueueClassicPlus::CustomWorker do
21
15
  job = failed_queue.lock
22
16
  job[:method].should == "Kerklfadsjflaksj"
23
17
  job[:args].should == [1, 2, 3]
24
- QueueClassicJob.last.last_error.should be_present
18
+ full_job = find_job(job[:id])
19
+
20
+ full_job['last_error'].should_not be_nil
25
21
  end
26
22
 
27
23
  it "records normal errors" do
@@ -35,7 +31,7 @@ describe QueueClassicPlus::CustomWorker do
35
31
  context "retry" do
36
32
  let(:job_type) { Jobs::Tests::LockedTestJob }
37
33
  let(:worker) { described_class.new q_name: job_type.queue.name }
38
- let(:enqueue_expected_ts) { described_class::BACKOFF_WIDTH.seconds.from_now }
34
+ let(:enqueue_expected_ts) { Time.now + described_class::BACKOFF_WIDTH }
39
35
 
40
36
  before do
41
37
  job_type.skip_transaction!
@@ -51,16 +47,14 @@ describe QueueClassicPlus::CustomWorker do
51
47
  QueueClassicMatchers::QueueClassicRspec.find_by_args('low', 'Jobs::Tests::LockedTestJob._perform', [true]).first['remaining_retries'].should be_nil
52
48
 
53
49
  Timecop.freeze do
54
- expect do
55
- worker.work
56
- end.to change_queue_size_of(job_type).by(-1)
50
+ worker.work
57
51
 
58
52
  failed_queue.count.should == 0 # not enqueued on Failed
59
- Jobs::Tests::LockedTestJob.should have_scheduled(true).at(Time.now + described_class::BACKOFF_WIDTH.seconds.to_i) # should have scheduled a retry for later
53
+ QueueClassicMatchers::QueueClassicRspec.find_by_args('low', 'Jobs::Tests::LockedTestJob._perform', [true]).first['remaining_retries'].should eq "4"
54
+ Jobs::Tests::LockedTestJob.should have_scheduled(true).at(Time.now + described_class::BACKOFF_WIDTH) # should have scheduled a retry for later
60
55
  end
61
56
 
62
- Timecop.freeze(Time.now + (described_class::BACKOFF_WIDTH.seconds.to_i * 2)) do
63
- QC::Later.tick(true)
57
+ Timecop.freeze(Time.now + (described_class::BACKOFF_WIDTH * 2)) do
64
58
  # the job should be re-enqueued with a decremented retry count
65
59
  jobs = QueueClassicMatchers::QueueClassicRspec.find_by_args('low', 'Jobs::Tests::LockedTestJob._perform', [true])
66
60
  jobs.size.should == 1
@@ -82,46 +76,11 @@ describe QueueClassicPlus::CustomWorker do
82
76
  QueueClassicMatchers::QueueClassicRspec.find_by_args('low', 'Jobs::Tests::LockedTestJob._perform', [true]).first['remaining_retries'].should be_nil
83
77
 
84
78
  Timecop.freeze do
85
- expect do
86
- worker.work
87
- end.to change_queue_size_of(job_type).by(-1)
79
+ worker.work
88
80
 
81
+ QueueClassicMatchers::QueueClassicRspec.find_by_args('failed_jobs', 'Jobs::Tests::LockedTestJob._perform', [true]).first['remaining_retries'].should be_nil
89
82
  failed_queue.count.should == 1 # not enqueued on Failed
90
- Jobs::Tests::LockedTestJob.should_not have_scheduled(true).at(Time.now + described_class::BACKOFF_WIDTH.seconds.to_i) # should have scheduled a retry for later
91
- end
92
- end
93
- end
94
-
95
- context "enqueuing during a retry" do
96
- let(:job_type) { Jobs::Tests::LockedTestJob }
97
- let(:worker) { described_class.new q_name: job_type.queue.name }
98
- let(:enqueue_expected_ts) { described_class::BACKOFF_WIDTH.seconds.from_now }
99
-
100
- before do
101
- job_type.max_retries = 5
102
- job_type.skip_transaction!
103
- end
104
-
105
- it "does not enqueue in main queue while retrying" do
106
- expect do
107
- job_type.enqueue_perform(true)
108
- end.to change_queue_size_of(job_type).by(1)
109
-
110
- Jobs::Tests::LockedTestJob.should have_queue_size_of(1)
111
- failed_queue.count.should == 0
112
- QueueClassicMatchers::QueueClassicRspec.find_by_args('low', 'Jobs::Tests::LockedTestJob._perform', [true]).first['remaining_retries'].should be_nil
113
-
114
- Timecop.freeze do
115
- expect do
116
- worker.work
117
- end.to change_queue_size_of(job_type).by(-1)
118
-
119
- failed_queue.count.should == 0 # not enqueued on Failed
120
- Jobs::Tests::LockedTestJob.should have_scheduled(true).at(Time.now + described_class::BACKOFF_WIDTH.seconds.to_i) # should have scheduled a retry for later
121
-
122
- expect do
123
- job_type.enqueue_perform(true)
124
- end.to change_queue_size_of(job_type).by(0)
83
+ Jobs::Tests::LockedTestJob.should_not have_scheduled(true).at(Time.now + described_class::BACKOFF_WIDTH) # should have scheduled a retry for later
125
84
  end
126
85
  end
127
86
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: queue_classic_plus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 1.0.0.alpha2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Mathieu
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2014-10-29 00:00:00.000000000 Z
13
+ date: 2015-02-21 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: queue_classic
@@ -18,70 +18,14 @@ dependencies:
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: '0'
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- version: '0'
29
- - !ruby/object:Gem::Dependency
30
- name: queue_classic-later
31
- requirement: !ruby/object:Gem::Requirement
32
- requirements:
33
- - - ">="
34
- - !ruby/object:Gem::Version
35
- version: '0'
36
- type: :runtime
37
- prerelease: false
38
- version_requirements: !ruby/object:Gem::Requirement
39
- requirements:
40
- - - ">="
41
- - !ruby/object:Gem::Version
42
- version: '0'
43
- - !ruby/object:Gem::Dependency
44
- name: activerecord
45
- requirement: !ruby/object:Gem::Requirement
46
- requirements:
47
- - - ">"
48
- - !ruby/object:Gem::Version
49
- version: '3.0'
50
- type: :runtime
51
- prerelease: false
52
- version_requirements: !ruby/object:Gem::Requirement
53
- requirements:
54
- - - ">"
55
- - !ruby/object:Gem::Version
56
- version: '3.0'
57
- - !ruby/object:Gem::Dependency
58
- name: activesupport
59
- requirement: !ruby/object:Gem::Requirement
60
- requirements:
61
- - - ">"
62
- - !ruby/object:Gem::Version
63
- version: '3.0'
64
- type: :runtime
65
- prerelease: false
66
- version_requirements: !ruby/object:Gem::Requirement
67
- requirements:
68
- - - ">"
69
- - !ruby/object:Gem::Version
70
- version: '3.0'
71
- - !ruby/object:Gem::Dependency
72
- name: with_advisory_lock
73
- requirement: !ruby/object:Gem::Requirement
74
- requirements:
75
- - - ">="
76
- - !ruby/object:Gem::Version
77
- version: '0'
21
+ version: 3.1.0
78
22
  type: :runtime
79
23
  prerelease: false
80
24
  version_requirements: !ruby/object:Gem::Requirement
81
25
  requirements:
82
26
  - - ">="
83
27
  - !ruby/object:Gem::Version
84
- version: '0'
28
+ version: 3.1.0
85
29
  - !ruby/object:Gem::Dependency
86
30
  name: bundler
87
31
  requirement: !ruby/object:Gem::Requirement
@@ -122,13 +66,19 @@ files:
122
66
  - ".gitignore"
123
67
  - ".rspec"
124
68
  - ".rvmrc"
69
+ - ".travis.yml"
125
70
  - Gemfile
71
+ - Guardfile
126
72
  - LICENSE.txt
127
73
  - README.md
128
74
  - Rakefile
129
75
  - lib/queue_classic_plus.rb
130
76
  - lib/queue_classic_plus/base.rb
77
+ - lib/queue_classic_plus/inflector.rb
78
+ - lib/queue_classic_plus/inheritable_attr.rb
131
79
  - lib/queue_classic_plus/metrics.rb
80
+ - lib/queue_classic_plus/new_relic.rb
81
+ - lib/queue_classic_plus/queue_classic/queue.rb
132
82
  - lib/queue_classic_plus/railtie.rb
133
83
  - lib/queue_classic_plus/tasks/work.rake
134
84
  - lib/queue_classic_plus/update_metrics.rb
@@ -139,8 +89,11 @@ files:
139
89
  - lib/rails/generators/qc_plus_job/templates/job_spec.rb.erb
140
90
  - queue_classic_plus.gemspec
141
91
  - spec/base_spec.rb
92
+ - spec/helpers.rb
93
+ - spec/inflector_spec.rb
142
94
  - spec/sample_jobs.rb
143
95
  - spec/spec_helper.rb
96
+ - spec/update_metrics_spec.rb
144
97
  - spec/worker_spec.rb
145
98
  homepage: ''
146
99
  licenses:
@@ -157,9 +110,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
157
110
  version: '0'
158
111
  required_rubygems_version: !ruby/object:Gem::Requirement
159
112
  requirements:
160
- - - ">="
113
+ - - ">"
161
114
  - !ruby/object:Gem::Version
162
- version: '0'
115
+ version: 1.3.1
163
116
  requirements: []
164
117
  rubyforge_project:
165
118
  rubygems_version: 2.2.2
@@ -168,6 +121,9 @@ specification_version: 4
168
121
  summary: Useful extras for Queue Classic
169
122
  test_files:
170
123
  - spec/base_spec.rb
124
+ - spec/helpers.rb
125
+ - spec/inflector_spec.rb
171
126
  - spec/sample_jobs.rb
172
127
  - spec/spec_helper.rb
128
+ - spec/update_metrics_spec.rb
173
129
  - spec/worker_spec.rb