pace 0.0.5
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.
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +22 -0
- data/Rakefile +63 -0
- data/bench/pace_http.rb +34 -0
- data/bench/resque_http.rb +41 -0
- data/examples/defer.rb +17 -0
- data/examples/echo.rb +5 -0
- data/examples/http.rb +20 -0
- data/examples/sleep.rb +13 -0
- data/lib/pace.rb +32 -0
- data/lib/pace/load_average.rb +34 -0
- data/lib/pace/mock.rb +63 -0
- data/lib/pace/version.rb +3 -0
- data/lib/pace/worker.rb +91 -0
- data/pace.gemspec +25 -0
- data/spec/pace/mock_spec.rb +72 -0
- data/spec/pace/worker_spec.rb +271 -0
- data/spec/pace_spec.rb +18 -0
- data/spec/spec_helper.rb +9 -0
- metadata +139 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(The MIT-License)
|
2
|
+
|
3
|
+
Copyright (c) 2011 GroupMe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Pace - A Resque Reactor #
|
2
|
+
|
3
|
+
More docs to come...
|
4
|
+
|
5
|
+
In short, the goals are:
|
6
|
+
|
7
|
+
* Performance
|
8
|
+
* Transparency via instrumentation (TODO)
|
9
|
+
|
10
|
+
## Examples ##
|
11
|
+
|
12
|
+
To have fun with the examples, fire one up and then start
|
13
|
+
enqueuing Resque jobs:
|
14
|
+
|
15
|
+
$ rake examples:http
|
16
|
+
|
17
|
+
$ irb
|
18
|
+
> require "rubygems"
|
19
|
+
> require "resque"
|
20
|
+
> class MyJob; def self.queue; "normal"; end; end
|
21
|
+
> Resque.enqueue(MyJob)
|
22
|
+
> 10.times { |n| Resque.enqueue(MyJob, :n => n) }
|
data/Rakefile
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require "resque/tasks"
|
3
|
+
Bundler::GemHelper.install_tasks
|
4
|
+
|
5
|
+
$: << File.join(File.dirname(__FILE__), "bench")
|
6
|
+
$: << File.join(File.dirname(__FILE__), "examples")
|
7
|
+
|
8
|
+
namespace :examples do
|
9
|
+
desc "Simply echo jobs to the console"
|
10
|
+
task :echo do
|
11
|
+
require "echo"
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Serial processing when the block doesn't defer"
|
15
|
+
task :sleep do
|
16
|
+
require "sleep"
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "Concurrent processing using EM.defer"
|
20
|
+
task :defer do
|
21
|
+
require "defer"
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "Concurrent processing by blocking on an HTTP connection"
|
25
|
+
task :http do
|
26
|
+
require "http"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
namespace :bench do
|
31
|
+
desc "Fire up Pace for benchmarking"
|
32
|
+
task :pace do
|
33
|
+
ENV["PACE_QUEUE"] = "normal"
|
34
|
+
require "pace_http"
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "Fire up Resque for benchmarking"
|
38
|
+
task :resque do
|
39
|
+
ENV["COUNT"] = "10"
|
40
|
+
ENV["QUEUE"] = "normal"
|
41
|
+
# ENV["VERBOSE"] = "1"
|
42
|
+
|
43
|
+
Rake::Task["resque:workers"].invoke
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "Inject jobs for benchmarking"
|
47
|
+
task :jobs do
|
48
|
+
require "resque"
|
49
|
+
require "resque_http"
|
50
|
+
|
51
|
+
count = (ENV["COUNT"] || 100).to_i
|
52
|
+
count.times do |n|
|
53
|
+
Resque.enqueue(ResqueHttp, :n => n)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# For benchmark purposes
|
59
|
+
namespace :resque do
|
60
|
+
task :setup do
|
61
|
+
require "resque_http"
|
62
|
+
end
|
63
|
+
end
|
data/bench/pace_http.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Benchmark Pace
|
2
|
+
#
|
3
|
+
# Performed by running:
|
4
|
+
#
|
5
|
+
# $ rake bench:pace
|
6
|
+
# $ COUNT=1000 rake bench:jobs
|
7
|
+
#
|
8
|
+
# This was setup to hit a dumb node.js server that simply responds with 200 OK.
|
9
|
+
#
|
10
|
+
# For 1000 jobs:
|
11
|
+
# 1.783s (avg. 5 runs, 1.78ms/job)
|
12
|
+
#
|
13
|
+
# For 50,000 jobs:
|
14
|
+
# 68.708s (avg. 5 runs, 1.37ms/job), memory topped out at a steady 21.3MB
|
15
|
+
|
16
|
+
require "pace"
|
17
|
+
|
18
|
+
Pace.logger = Logger.new(File.join(File.dirname(__FILE__), "pace.log"))
|
19
|
+
Pace.log("Starting #{'%0.6f' % Time.now}")
|
20
|
+
|
21
|
+
Pace.start do |job|
|
22
|
+
start_time = Time.now
|
23
|
+
args = job["args"][0].map { |k,v| "#{k}=#{v}" }
|
24
|
+
args = args.join("&")
|
25
|
+
|
26
|
+
http = EM::Protocols::HttpClient.request(
|
27
|
+
:host => "localhost",
|
28
|
+
:port => 9000,
|
29
|
+
:request => "/?#{args}"
|
30
|
+
)
|
31
|
+
http.callback do |r|
|
32
|
+
Pace.log("http://localhost:9000/?#{args}", start_time)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Benchmark Resque
|
2
|
+
#
|
3
|
+
# This was performed by running:
|
4
|
+
#
|
5
|
+
# $ rake bench:resque
|
6
|
+
# $ COUNT=1000 rake bench:jobs
|
7
|
+
#
|
8
|
+
# By default, it spins up ten workers locally, logging to bench/resque.log. A dummy
|
9
|
+
# node.js server was propped up to simply respond with 200 OK.
|
10
|
+
#
|
11
|
+
# For 1000 jobs:
|
12
|
+
# 18.33s (avg. 5 runs, 18.33ms/job)
|
13
|
+
#
|
14
|
+
# For 50,000 jobs:
|
15
|
+
# 870.81s (just 1 run, but I got stuff to do, 17.42ms/job),
|
16
|
+
# memory sitting at ~15MB per worker process (10 total)
|
17
|
+
|
18
|
+
require "net/http"
|
19
|
+
require "logger"
|
20
|
+
|
21
|
+
class ResqueHttp
|
22
|
+
def self.queue
|
23
|
+
"normal"
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.perform(args)
|
27
|
+
start_time = Time.now
|
28
|
+
args = args.map { |k,v| "#{k}=#{v}" }
|
29
|
+
args = args.join("&")
|
30
|
+
|
31
|
+
Net::HTTP.start("localhost", 9000) do |http|
|
32
|
+
http.get("/?#{args}")
|
33
|
+
end
|
34
|
+
|
35
|
+
logger.info "http://localhost:9000/?#{args} #{"(%0.6fs)" % (Time.now - start_time)}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.logger
|
39
|
+
@logger ||= Logger.new(File.join(File.dirname(__FILE__), "resque.log"))
|
40
|
+
end
|
41
|
+
end
|
data/examples/defer.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# To gain concurrent processing on jobs that don't block on
|
2
|
+
# sockets, we can use EM.defer to run in background threads.
|
3
|
+
|
4
|
+
require "pace"
|
5
|
+
|
6
|
+
Pace.start(:queue => (ENV["PACE_QUEUE"] || "normal")) do |job|
|
7
|
+
start_time = Time.now
|
8
|
+
|
9
|
+
operation = proc {
|
10
|
+
rand(10).times { sleep 0.1 }
|
11
|
+
}
|
12
|
+
callback = proc { |result|
|
13
|
+
Pace.log(job.inspect, start_time)
|
14
|
+
}
|
15
|
+
|
16
|
+
EM.defer operation, callback
|
17
|
+
end
|
data/examples/echo.rb
ADDED
data/examples/http.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Pace (and EventMachine) works best when your job can block
|
2
|
+
# on a socket and proceed to process jobs (almost) concurrently.
|
3
|
+
#
|
4
|
+
# A good explanation can be found here:
|
5
|
+
# http://www.igvita.com/2008/05/27/ruby-eventmachine-the-speed-demon/
|
6
|
+
|
7
|
+
require "pace"
|
8
|
+
|
9
|
+
Pace.start(:queue => (ENV["PACE_QUEUE"] || "normal")) do |job|
|
10
|
+
start_time = Time.now
|
11
|
+
|
12
|
+
http = EM::Protocols::HttpClient.request(
|
13
|
+
:host => "localhost",
|
14
|
+
:port => 9000,
|
15
|
+
:request => "/"
|
16
|
+
)
|
17
|
+
http.callback do |r|
|
18
|
+
Pace.log(job.inspect, start_time)
|
19
|
+
end
|
20
|
+
end
|
data/examples/sleep.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Our work in this example does not defer nor block on a socket,
|
2
|
+
# so jobs will be processed serially, finishing one before starting
|
3
|
+
# the next.
|
4
|
+
#
|
5
|
+
# This should be avoided.
|
6
|
+
|
7
|
+
require "pace"
|
8
|
+
|
9
|
+
Pace.start(:queue => (ENV["PACE_QUEUE"] || "normal")) do |job|
|
10
|
+
start_time = Time.now
|
11
|
+
rand(10).times { sleep 0.1 }
|
12
|
+
Pace.log(job.inspect, start_time)
|
13
|
+
end
|
data/lib/pace.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "em-redis"
|
3
|
+
require "json"
|
4
|
+
require "uri"
|
5
|
+
require "logger"
|
6
|
+
require "pace/load_average"
|
7
|
+
require "pace/worker"
|
8
|
+
|
9
|
+
module Pace
|
10
|
+
def self.start(options = {}, &block)
|
11
|
+
worker = Pace::Worker.new(options)
|
12
|
+
worker.start(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.log(message, start_time = nil)
|
16
|
+
if start_time
|
17
|
+
logger.info("%s (%0.6fs)" % [message, Time.now - start_time])
|
18
|
+
else
|
19
|
+
logger.info("%s" % message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.logger
|
26
|
+
@logger ||= Logger.new(STDOUT)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.logger=(new_logger)
|
30
|
+
@logger = new_logger
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Pace
|
2
|
+
module LoadAverage
|
3
|
+
INTERVAL = 5.0 # sec
|
4
|
+
FSHIFT = 11
|
5
|
+
FIXED_1 = 1 << FSHIFT
|
6
|
+
EXP_1 = 1884.0 # 1/exp(5sec/1min) as fixed-point
|
7
|
+
EXP_5 = 2014.0 # 1/exp(5sec/5min)
|
8
|
+
EXP_15 = 2037.0 # 1/exp(5sec/15min)
|
9
|
+
$ticks = 0
|
10
|
+
$load = [0.0, 0.0, 0.0, 0.0] # sec, min, 5 min, 15 min
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def compute
|
14
|
+
per_second = $ticks / INTERVAL
|
15
|
+
$load[0] = per_second
|
16
|
+
$load[1] = average($load[1], EXP_1, per_second)
|
17
|
+
$load[2] = average($load[2], EXP_5, per_second)
|
18
|
+
$load[3] = average($load[3], EXP_15, per_second)
|
19
|
+
$ticks = 0
|
20
|
+
$load
|
21
|
+
end
|
22
|
+
|
23
|
+
def average(load, exp, n)
|
24
|
+
load *= exp
|
25
|
+
load += n*(FIXED_1-exp)
|
26
|
+
((load * 1000).to_i >> FSHIFT) / 1000.0
|
27
|
+
end
|
28
|
+
|
29
|
+
def tick
|
30
|
+
$ticks += 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/pace/mock.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Ease testing by mocking the event loop
|
2
|
+
#
|
3
|
+
# Instead of having to detect and stop the event loop yourself, this helper
|
4
|
+
# simply returns all jobs in the queue and shuts down the loop.
|
5
|
+
#
|
6
|
+
# require "pace/mock"
|
7
|
+
#
|
8
|
+
# # Fire it up
|
9
|
+
# Pace::Mock.enable
|
10
|
+
#
|
11
|
+
# # Add some jobs
|
12
|
+
# Resque.enqueue(Work, ...)
|
13
|
+
# Resque.enqueue(Work, ...)
|
14
|
+
#
|
15
|
+
# # Create a worker with a block that doesn't need to stop the loop
|
16
|
+
# worker = Pace::Worker.new(:queue => "queue")
|
17
|
+
# worker.start do |job|
|
18
|
+
# puts job.inspect
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# # Turn it off when you're done
|
22
|
+
# Pace::Mock.disable
|
23
|
+
#
|
24
|
+
module Pace
|
25
|
+
module Mock
|
26
|
+
def self.enable
|
27
|
+
Pace.logger.info "Enabling Pace mock"
|
28
|
+
|
29
|
+
Pace::Worker.class_eval do
|
30
|
+
if instance_methods.include?(:start_with_mock)
|
31
|
+
alias :start :start_with_mock
|
32
|
+
else
|
33
|
+
def start_with_mock(&block)
|
34
|
+
jobs = nil
|
35
|
+
|
36
|
+
EM.run do
|
37
|
+
@redis = EM::Protocols::Redis.connect(@options)
|
38
|
+
@redis.lrange(queue, 0, -1) do |jobs|
|
39
|
+
jobs.each do |job|
|
40
|
+
block.call JSON.parse(job)
|
41
|
+
end
|
42
|
+
@redis.del(queue) { EM.stop_event_loop }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
alias :start_without_mock :start
|
48
|
+
alias :start :start_with_mock
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.disable
|
54
|
+
Pace.logger.info "Disabling Pace mock"
|
55
|
+
|
56
|
+
Pace::Worker.class_eval do
|
57
|
+
if instance_methods.include?(:start_without_mock)
|
58
|
+
alias :start :start_without_mock
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/pace/version.rb
ADDED
data/lib/pace/worker.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
module Pace
|
2
|
+
class Worker
|
3
|
+
attr_reader :redis, :queue
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@options = options.dup
|
7
|
+
@queue = @options.delete(:queue) || ENV["PACE_QUEUE"]
|
8
|
+
@namespace = @options.delete(:namespace)
|
9
|
+
|
10
|
+
if @queue.nil? || @queue.empty?
|
11
|
+
raise ArgumentError.new("Queue unspecified -- pass a queue name or set PACE_QUEUE")
|
12
|
+
end
|
13
|
+
|
14
|
+
@queue = fully_qualified_queue(@queue)
|
15
|
+
|
16
|
+
url = URI(@options.delete(:url) || ENV["PACE_REDIS"] || "redis://127.0.0.1:6379/0")
|
17
|
+
|
18
|
+
@options[:host] ||= url.host
|
19
|
+
@options[:port] ||= url.port
|
20
|
+
@options[:password] ||= url.password
|
21
|
+
@options[:db] ||= url.path[1..-1].to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
def start(&block)
|
25
|
+
@block = block
|
26
|
+
|
27
|
+
Pace.logger.info "Starting up"
|
28
|
+
register_signal_handlers
|
29
|
+
|
30
|
+
EM.run do
|
31
|
+
EventMachine::add_periodic_timer(Pace::LoadAverage::INTERVAL) do
|
32
|
+
Pace::LoadAverage.compute
|
33
|
+
Pace.logger.info("load averages: #{$load.join(' ')}")
|
34
|
+
end
|
35
|
+
|
36
|
+
@redis = EM::Protocols::Redis.connect(@options)
|
37
|
+
fetch_next_job
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def shutdown
|
42
|
+
Pace.logger.info "Shutting down"
|
43
|
+
EM.stop_event_loop
|
44
|
+
end
|
45
|
+
|
46
|
+
def on_error(&callback)
|
47
|
+
@error_callback = callback
|
48
|
+
end
|
49
|
+
|
50
|
+
def enqueue(queue, klass, *args, &block)
|
51
|
+
queue = fully_qualified_queue(queue)
|
52
|
+
job = {:class => klass.to_s, :args => args}.to_json
|
53
|
+
@redis.rpush(queue, job, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def fetch_next_job
|
59
|
+
@redis.blpop(queue, 0) do |queue, job|
|
60
|
+
EM.next_tick { fetch_next_job }
|
61
|
+
|
62
|
+
begin
|
63
|
+
@block.call JSON.parse(job)
|
64
|
+
Pace::LoadAverage.tick
|
65
|
+
rescue Exception => e
|
66
|
+
log_failed_job(job, e)
|
67
|
+
@error_callback.call(job, e) if @error_callback
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def fully_qualified_queue(queue)
|
73
|
+
parts = [queue]
|
74
|
+
parts.unshift("resque:queue") unless queue.index(":")
|
75
|
+
parts.unshift(@namespace) unless @namespace.nil?
|
76
|
+
parts.join(":")
|
77
|
+
end
|
78
|
+
|
79
|
+
def register_signal_handlers
|
80
|
+
trap('TERM') { shutdown }
|
81
|
+
trap('QUIT') { shutdown }
|
82
|
+
trap('INT') { shutdown }
|
83
|
+
end
|
84
|
+
|
85
|
+
def log_failed_job(job, exception)
|
86
|
+
message = "Job failed!\n#{job}\n#{exception.message}\n"
|
87
|
+
message << exception.backtrace.join("\n")
|
88
|
+
Pace.logger.error(message)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/pace.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- mode: ruby; encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "pace/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "pace"
|
7
|
+
s.version = Pace::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Dave Yeu"]
|
10
|
+
s.email = ["daveyeu@gmail.com"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Resque-compatible job processing in an event loop}
|
13
|
+
|
14
|
+
s.rubyforge_project = "pace"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency "em-redis", ">= 0.3.0"
|
22
|
+
|
23
|
+
s.add_development_dependency "resque", "~> 1.17.1"
|
24
|
+
s.add_development_dependency "rspec", "~> 2.6.0"
|
25
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "pace/mock"
|
3
|
+
|
4
|
+
describe Pace::Mock do
|
5
|
+
class Work
|
6
|
+
def self.queue
|
7
|
+
"pace"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
before do
|
12
|
+
Resque.dequeue(Work)
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
Pace::Mock.disable
|
17
|
+
end
|
18
|
+
|
19
|
+
describe ".enable" do
|
20
|
+
it "sets up the mock, which simply passes down Resque jobs and closes the event loop" do
|
21
|
+
Pace::Mock.enable
|
22
|
+
Resque.enqueue(Work, :n => 1)
|
23
|
+
Resque.enqueue(Work, :n => 2)
|
24
|
+
|
25
|
+
results = []
|
26
|
+
worker = Pace::Worker.new(:queue => "pace")
|
27
|
+
worker.start { |job| results << job }
|
28
|
+
results.should == [
|
29
|
+
{"class" => "Work", "args" => [{"n" => 1}]},
|
30
|
+
{"class" => "Work", "args" => [{"n" => 2}]}
|
31
|
+
]
|
32
|
+
|
33
|
+
# Clears out the queue
|
34
|
+
more_results = []
|
35
|
+
worker.start { |job| more_results << job }
|
36
|
+
more_results.should be_empty
|
37
|
+
end
|
38
|
+
|
39
|
+
it "works after disabling" do
|
40
|
+
Pace::Mock.enable
|
41
|
+
Pace::Mock.disable
|
42
|
+
Pace::Mock.enable
|
43
|
+
|
44
|
+
Resque.enqueue(Work, :n => 2)
|
45
|
+
|
46
|
+
results = []
|
47
|
+
worker = Pace::Worker.new(:queue => "pace")
|
48
|
+
worker.start do |job|
|
49
|
+
results << job
|
50
|
+
end
|
51
|
+
results.should == [{"class" => "Work", "args" => [{"n" => 2}]}]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe ".disable" do
|
56
|
+
it "tears down the mock and re-institutes the event loop" do
|
57
|
+
Pace::Mock.enable
|
58
|
+
Pace::Mock.disable
|
59
|
+
Resque.enqueue(Work, :n => 1)
|
60
|
+
Resque.enqueue(Work, :n => 2)
|
61
|
+
|
62
|
+
results = []
|
63
|
+
worker = Pace::Worker.new(:queue => "pace")
|
64
|
+
worker.start do |job|
|
65
|
+
results << job
|
66
|
+
EM.stop_event_loop
|
67
|
+
end
|
68
|
+
|
69
|
+
results.should have(1).items
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Pace::Worker do
|
4
|
+
class Work
|
5
|
+
def self.queue
|
6
|
+
"pace"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
before do
|
11
|
+
Resque.dequeue(Work)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#initialize" do
|
15
|
+
describe "sets the Redis connection options" do
|
16
|
+
before do
|
17
|
+
@connection = double(EM::Connection)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "uses 127.0.0.1:6379/0 by default" do
|
21
|
+
EM::Protocols::Redis.should_receive(:connect).with(
|
22
|
+
:host => "127.0.0.1",
|
23
|
+
:port => 6379,
|
24
|
+
:password => nil,
|
25
|
+
:db => 0
|
26
|
+
).and_return(@connection)
|
27
|
+
|
28
|
+
worker = Pace::Worker.new :queue => "normal"
|
29
|
+
worker.stub(:fetch_next_job).and_return { EM.stop_event_loop }
|
30
|
+
worker.start
|
31
|
+
worker.redis.should == @connection
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can use a custom URL string" do
|
35
|
+
EM::Protocols::Redis.should_receive(:connect).with(
|
36
|
+
:host => "some.host.local",
|
37
|
+
:port => 9999,
|
38
|
+
:password => "secret",
|
39
|
+
:db => 1
|
40
|
+
).and_return(@connection)
|
41
|
+
|
42
|
+
worker = Pace::Worker.new :url => "redis://user:secret@some.host.local:9999/1", :queue => "normal"
|
43
|
+
worker.stub(:fetch_next_job).and_return { EM.stop_event_loop }
|
44
|
+
worker.start
|
45
|
+
worker.redis.should == @connection
|
46
|
+
end
|
47
|
+
|
48
|
+
it "can be set using the PACE_REDIS environment variable" do
|
49
|
+
original_redis = ENV["PACE_REDIS"]
|
50
|
+
ENV["PACE_REDIS"] = "redis://user:secret@some.host.local:9999/1"
|
51
|
+
|
52
|
+
EM::Protocols::Redis.should_receive(:connect).with(
|
53
|
+
:host => "some.host.local",
|
54
|
+
:port => 9999,
|
55
|
+
:password => "secret",
|
56
|
+
:db => 1
|
57
|
+
).and_return(@connection)
|
58
|
+
|
59
|
+
worker = Pace::Worker.new :queue => "normal"
|
60
|
+
worker.stub(:fetch_next_job).and_return { EM.stop_event_loop }
|
61
|
+
worker.start
|
62
|
+
worker.redis.should == @connection
|
63
|
+
|
64
|
+
ENV["PACE_REDIS"] = original_redis
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "sets the queue_name" do
|
69
|
+
before do
|
70
|
+
EM::Protocols::Redis.stub(:connect).and_return(double(EM::Connection))
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when the given name has no colons" do
|
74
|
+
it "prepends the Resque default queue 'namespace'" do
|
75
|
+
worker = Pace::Worker.new(:queue => "normal")
|
76
|
+
worker.queue.should == "resque:queue:normal"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "when the given name has colons" do
|
81
|
+
it "does not prepend anything (absolute)" do
|
82
|
+
worker = Pace::Worker.new(:queue => "my:special:queue")
|
83
|
+
worker.queue.should == "my:special:queue"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "when a namespace is provided" do
|
88
|
+
it "prepends the namespace in either case" do
|
89
|
+
worker = Pace::Worker.new(:queue => "normal", :namespace => "test")
|
90
|
+
worker.queue.should == "test:resque:queue:normal"
|
91
|
+
|
92
|
+
worker = Pace::Worker.new(:queue => "special:queue", :namespace => "test")
|
93
|
+
worker.queue.should == "test:special:queue"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "when the queue argument is nil" do
|
98
|
+
before do
|
99
|
+
@pace_queue = ENV["PACE_QUEUE"]
|
100
|
+
end
|
101
|
+
|
102
|
+
after do
|
103
|
+
ENV["PACE_QUEUE"] = @pace_queue
|
104
|
+
end
|
105
|
+
|
106
|
+
it "falls back to the PACE_QUEUE environment variable" do
|
107
|
+
ENV["PACE_QUEUE"] = "high"
|
108
|
+
worker = Pace::Worker.new
|
109
|
+
worker.queue.should == "resque:queue:high"
|
110
|
+
|
111
|
+
ENV["PACE_QUEUE"] = "my:special:queue"
|
112
|
+
worker = Pace::Worker.new
|
113
|
+
worker.queue.should == "my:special:queue"
|
114
|
+
end
|
115
|
+
|
116
|
+
it "throws an exception if PACE_QUEUE is nil" do
|
117
|
+
ENV["PACE_QUEUE"] = nil
|
118
|
+
expect { Pace::Worker.new }.to raise_error(ArgumentError)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#start" do
|
125
|
+
before do
|
126
|
+
@worker = Pace::Worker.new(:queue => "pace")
|
127
|
+
end
|
128
|
+
|
129
|
+
it "yields a serialized Resque jobs" do
|
130
|
+
Resque.enqueue(Work, :foo => 1, :bar => 2)
|
131
|
+
|
132
|
+
@worker.start do |job|
|
133
|
+
job["class"].should == "Work"
|
134
|
+
job["args"].should == [{"foo" => 1, "bar" => 2}]
|
135
|
+
EM.stop_event_loop
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
it "continues to pop jobs until stopped" do
|
140
|
+
Resque.enqueue(Work, :n => 1)
|
141
|
+
Resque.enqueue(Work, :n => 2)
|
142
|
+
Resque.enqueue(Work, :n => 3)
|
143
|
+
Resque.enqueue(Work, :n => 4)
|
144
|
+
Resque.enqueue(Work, :n => 5)
|
145
|
+
|
146
|
+
results = []
|
147
|
+
|
148
|
+
@worker.start do |job|
|
149
|
+
n = job["args"].first["n"]
|
150
|
+
results << n
|
151
|
+
EM.stop_event_loop if n == 5
|
152
|
+
end
|
153
|
+
|
154
|
+
results.should == [1, 2, 3, 4, 5]
|
155
|
+
end
|
156
|
+
|
157
|
+
it "rescues any errors in the passed block" do
|
158
|
+
Resque.enqueue(Work, :n => 1)
|
159
|
+
Resque.enqueue(Work, :n => 2)
|
160
|
+
Resque.enqueue(Work, :n => 3)
|
161
|
+
|
162
|
+
results = []
|
163
|
+
|
164
|
+
@worker.start do |job|
|
165
|
+
n = job["args"].first["n"]
|
166
|
+
|
167
|
+
raise "FAIL" if n == 1
|
168
|
+
results << n
|
169
|
+
EM.stop_event_loop if n == 3
|
170
|
+
end
|
171
|
+
|
172
|
+
results.should == [2, 3]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "#on_error" do
|
177
|
+
it "creates a callback to run if there's an error while processing a job" do
|
178
|
+
Resque.enqueue(Work, :n => 1)
|
179
|
+
Resque.enqueue(Work, :n => 2)
|
180
|
+
exception = RuntimeError.new("FAIL")
|
181
|
+
|
182
|
+
worker = Pace::Worker.new(:queue => "pace")
|
183
|
+
worker.on_error do |job, error|
|
184
|
+
job.should == {"class" => "Work", "args" => [{"n" => 1}]}.to_json
|
185
|
+
error.should == exception
|
186
|
+
end
|
187
|
+
|
188
|
+
worker.start do |job|
|
189
|
+
n = job["args"].first["n"]
|
190
|
+
raise exception if n == 1
|
191
|
+
EM.stop_event_loop if n == 2
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe "#shutdown" do
|
197
|
+
it "stops the event loop on the next attempt to fetch a job" do
|
198
|
+
Resque.enqueue(Work, :n => 1)
|
199
|
+
Resque.enqueue(Work, :n => 2)
|
200
|
+
|
201
|
+
results = []
|
202
|
+
|
203
|
+
worker = Pace::Worker.new(:queue => "pace")
|
204
|
+
worker.start do |job|
|
205
|
+
worker.shutdown
|
206
|
+
results << job["args"].first["n"]
|
207
|
+
end
|
208
|
+
|
209
|
+
# Never runs the second job
|
210
|
+
results.should == [1]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
describe "#enqueue" do
|
215
|
+
class CallbackJob
|
216
|
+
def self.queue
|
217
|
+
"callback"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
it "adds a new, Resque-compatible job into the specified queue" do
|
222
|
+
Resque.enqueue(Work)
|
223
|
+
|
224
|
+
options = {:x => 1, :y => 2}
|
225
|
+
|
226
|
+
worker = Pace::Worker.new(:queue => "pace")
|
227
|
+
worker.start do |job|
|
228
|
+
worker.enqueue(CallbackJob.queue, CallbackJob, options) {
|
229
|
+
EM.stop_event_loop
|
230
|
+
}
|
231
|
+
end
|
232
|
+
|
233
|
+
new_job = Resque.pop(CallbackJob.queue)
|
234
|
+
new_job.should == {
|
235
|
+
"class" => "CallbackJob",
|
236
|
+
"args" => [{"x" => 1, "y" => 2}]
|
237
|
+
}
|
238
|
+
|
239
|
+
# It's identical to a job added w/ Resque (important!)
|
240
|
+
Resque.enqueue(CallbackJob, options)
|
241
|
+
resque_job = Resque.pop(CallbackJob.queue)
|
242
|
+
resque_job.should == new_job
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
describe "signal handling" do
|
247
|
+
before do
|
248
|
+
Resque.enqueue(Work, :n => 1)
|
249
|
+
Resque.enqueue(Work, :n => 2)
|
250
|
+
Resque.enqueue(Work, :n => 3)
|
251
|
+
|
252
|
+
@worker = Pace::Worker.new(:queue => "pace")
|
253
|
+
end
|
254
|
+
|
255
|
+
["QUIT", "TERM", "INT"].each do |signal|
|
256
|
+
it "handles SIG#{signal}" do
|
257
|
+
results = []
|
258
|
+
|
259
|
+
@worker.start do |job|
|
260
|
+
n = job["args"].first["n"]
|
261
|
+
Process.kill(signal, $$) if n == 1
|
262
|
+
results << n
|
263
|
+
end
|
264
|
+
|
265
|
+
# trap seems to interrupt the event loop randomly, so it does not appear
|
266
|
+
# possible to determine exactly how many jobs will be processed
|
267
|
+
results.should_not be_empty
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
data/spec/pace_spec.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Pace do
|
4
|
+
describe ".start" do
|
5
|
+
it "is a shortcut for instantiating and running a Pace::Worker" do
|
6
|
+
expected_block = Proc.new {}
|
7
|
+
|
8
|
+
worker = double(Pace::Worker)
|
9
|
+
worker.should_receive(:start).with(&expected_block)
|
10
|
+
Pace::Worker.should_receive(:new).with(
|
11
|
+
:url => "redis://127.0.0.1:6379/0",
|
12
|
+
:queue => "normal",
|
13
|
+
).and_return(worker)
|
14
|
+
|
15
|
+
Pace.start(:url => "redis://127.0.0.1:6379/0", :queue => "normal", &expected_block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pace
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 21
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 5
|
10
|
+
version: 0.0.5
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Dave Yeu
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-22 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: em-redis
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 19
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 3
|
33
|
+
- 0
|
34
|
+
version: 0.3.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: resque
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 81
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 17
|
49
|
+
- 1
|
50
|
+
version: 1.17.1
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rspec
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 23
|
62
|
+
segments:
|
63
|
+
- 2
|
64
|
+
- 6
|
65
|
+
- 0
|
66
|
+
version: 2.6.0
|
67
|
+
type: :development
|
68
|
+
version_requirements: *id003
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- daveyeu@gmail.com
|
72
|
+
executables: []
|
73
|
+
|
74
|
+
extensions: []
|
75
|
+
|
76
|
+
extra_rdoc_files: []
|
77
|
+
|
78
|
+
files:
|
79
|
+
- .gitignore
|
80
|
+
- .rspec
|
81
|
+
- Gemfile
|
82
|
+
- LICENSE
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- bench/pace_http.rb
|
86
|
+
- bench/resque_http.rb
|
87
|
+
- examples/defer.rb
|
88
|
+
- examples/echo.rb
|
89
|
+
- examples/http.rb
|
90
|
+
- examples/sleep.rb
|
91
|
+
- lib/pace.rb
|
92
|
+
- lib/pace/load_average.rb
|
93
|
+
- lib/pace/mock.rb
|
94
|
+
- lib/pace/version.rb
|
95
|
+
- lib/pace/worker.rb
|
96
|
+
- pace.gemspec
|
97
|
+
- spec/pace/mock_spec.rb
|
98
|
+
- spec/pace/worker_spec.rb
|
99
|
+
- spec/pace_spec.rb
|
100
|
+
- spec/spec_helper.rb
|
101
|
+
has_rdoc: true
|
102
|
+
homepage: ""
|
103
|
+
licenses: []
|
104
|
+
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
hash: 3
|
116
|
+
segments:
|
117
|
+
- 0
|
118
|
+
version: "0"
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
hash: 3
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
version: "0"
|
128
|
+
requirements: []
|
129
|
+
|
130
|
+
rubyforge_project: pace
|
131
|
+
rubygems_version: 1.6.2
|
132
|
+
signing_key:
|
133
|
+
specification_version: 3
|
134
|
+
summary: Resque-compatible job processing in an event loop
|
135
|
+
test_files:
|
136
|
+
- spec/pace/mock_spec.rb
|
137
|
+
- spec/pace/worker_spec.rb
|
138
|
+
- spec/pace_spec.rb
|
139
|
+
- spec/spec_helper.rb
|