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.
@@ -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