pace 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ bench/*.log
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ # -*- mode: ruby; -*-
2
+ source :rubygems
3
+ gemspec
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.
@@ -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) }
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ require "pace"
2
+
3
+ Pace.start(:queue => (ENV["PACE_QUEUE"] || "normal")) do |job|
4
+ Pace.log(job.inspect, Time.now)
5
+ end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Pace
2
+ VERSION = "0.0.5"
3
+ end
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ require "rubygems"
2
+ require "bundler"
3
+ Bundler.require :default, :development
4
+
5
+ RSpec.configure do |config|
6
+ config.before(:each) do
7
+ Pace.stub(:logger).and_return(Logger.new("/dev/null"))
8
+ end
9
+ end
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