simplejob 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/LICENSE.txt +23 -0
- data/README.md +50 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/examples/basic/README.md +10 -0
- data/examples/basic/consumer.rb +10 -0
- data/examples/basic/producer.rb +8 -0
- data/examples/multiple_jobs/README.md +11 -0
- data/examples/multiple_jobs/drink_consumer.rb +17 -0
- data/examples/multiple_jobs/food_consumer.rb +22 -0
- data/examples/multiple_jobs/producer.rb +11 -0
- data/examples/wildcard_handlers/README.md +10 -0
- data/examples/wildcard_handlers/consumer.rb +14 -0
- data/examples/wildcard_handlers/producer.rb +10 -0
- data/examples/wildcard_handlers/wiretap.rb +11 -0
- data/lib/simplejob.rb +23 -0
- data/lib/simplejob/client.rb +50 -0
- data/lib/simplejob/worker.rb +90 -0
- data/test/helper.rb +18 -0
- data/test/test_simplejob.rb +7 -0
- metadata +133 -0
data/.document
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2011 Ken Pratt (ken@kenpratt.net)
|
4
|
+
Copyright (c) 2011 Ruboss Technology Corp (peter@ruboss.com)
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
Simple Job Queue
|
2
|
+
================
|
3
|
+
|
4
|
+
Painless job queueing, backed by AMQP.
|
5
|
+
|
6
|
+
The design goals were:
|
7
|
+
|
8
|
+
* Super simple interface
|
9
|
+
* Robust jobs that never get lost
|
10
|
+
* Jobs don't get marked complete until they are finished (they survive reboots)
|
11
|
+
* One job queue per worker pool, with jobs mapped to that queue (for easy monitoring)
|
12
|
+
* Jobs which throw exceptions are logged and removed, preventing infinite job loops
|
13
|
+
|
14
|
+
Hello World
|
15
|
+
-----------
|
16
|
+
|
17
|
+
Start your [RabbitMQ](http://www.rabbitmq.com/) server (install one if necessary).
|
18
|
+
|
19
|
+
<pre>
|
20
|
+
producer.rb:
|
21
|
+
|
22
|
+
require "simplejob"
|
23
|
+
|
24
|
+
SimpleJob.send("hello")
|
25
|
+
</pre>
|
26
|
+
|
27
|
+
<pre>
|
28
|
+
consumer.rb:
|
29
|
+
|
30
|
+
require "simplejob"
|
31
|
+
|
32
|
+
SimpleJob::Worker.start do
|
33
|
+
handle "hello" do |props|
|
34
|
+
puts "got it!"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
</pre>
|
38
|
+
|
39
|
+
<pre>
|
40
|
+
$ ruby -rubygems consumer.rb
|
41
|
+
</pre>
|
42
|
+
|
43
|
+
<pre>
|
44
|
+
$ ruby -rubygems producer.rb
|
45
|
+
</pre>
|
46
|
+
|
47
|
+
More examples
|
48
|
+
-------------
|
49
|
+
|
50
|
+
See the [examples directory](http://github.com/kenpratt/simple-job-queue/tree/master/examples).
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gem|
|
8
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
9
|
+
gem.name = "simplejob"
|
10
|
+
gem.homepage = "http://github.com/kenpratt/simplejob"
|
11
|
+
gem.license = "MIT"
|
12
|
+
gem.summary = "Painless job queueing."
|
13
|
+
gem.description = "A simple AMQP-backed job queuing system."
|
14
|
+
gem.email = "ken@kenpratt.net"
|
15
|
+
gem.authors = ["Ken Pratt"]
|
16
|
+
gem.add_dependency "activesupport", ">= 3.0.1"
|
17
|
+
gem.add_dependency "json", ">= 1.5.1"
|
18
|
+
gem.add_dependency "amqp", "0.7.1"
|
19
|
+
end
|
20
|
+
Jeweler::RubygemsDotOrgTasks.new
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
Rake::TestTask.new(:test) do |test|
|
24
|
+
test.libs << 'lib' << 'test'
|
25
|
+
test.pattern = 'test/**/test_*.rb'
|
26
|
+
test.verbose = true
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
test.rcov_opts << '--exclude "gems/*"'
|
35
|
+
end
|
36
|
+
|
37
|
+
task :default => :test
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Basic producer/consumer
|
2
|
+
=======================
|
3
|
+
|
4
|
+
Open a couple of terminal windows with consumers, and then run the producer:
|
5
|
+
|
6
|
+
ruby -rubygems consumer.rb
|
7
|
+
ruby -rubygems consumer.rb
|
8
|
+
ruby -rubygems producer.rb
|
9
|
+
|
10
|
+
You should see the consumers pulling requests of the queue and processing them. Try killing and restarting one or both of them, or even restarting RabbitMQ. No jobs should be lost.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Multiple jobs mapped to different consumers
|
2
|
+
===========================================
|
3
|
+
|
4
|
+
Open a few terminal windows with various combinations of consumers, and then run the producer:
|
5
|
+
|
6
|
+
ruby -rubygems drink_consumer.rb
|
7
|
+
ruby -rubygems food_consumer.rb
|
8
|
+
ruby -rubygems food_consumer.rb
|
9
|
+
ruby -rubygems producer.rb
|
10
|
+
|
11
|
+
You should see the consumers pulling requests of the queue and processing them. Try killing and restarting them, or even restarting RabbitMQ. No jobs should be lost.
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$:.unshift(File.expand_path("../../../lib", __FILE__))
|
2
|
+
|
3
|
+
require "simplejob"
|
4
|
+
|
5
|
+
SimpleJob::Worker.start(:queue_name => "drinks") do
|
6
|
+
|
7
|
+
handle "coffee" do |props|
|
8
|
+
puts "caffeine, here i come"
|
9
|
+
sleep 1
|
10
|
+
end
|
11
|
+
|
12
|
+
handle "milkshake" do |props|
|
13
|
+
puts "a #{props[:flavor]} milkshake, excellent!"
|
14
|
+
sleep 1
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
$:.unshift(File.expand_path("../../../lib", __FILE__))
|
2
|
+
|
3
|
+
require "simplejob"
|
4
|
+
|
5
|
+
SimpleJob::Worker.start(:queue_name => "food") do
|
6
|
+
|
7
|
+
handle "pie" do |props|
|
8
|
+
puts "mmm, a #{props[:size]} #{props[:flavor]} pie, my favorite!"
|
9
|
+
sleep 3
|
10
|
+
end
|
11
|
+
|
12
|
+
handle "ice_cream" do |props|
|
13
|
+
puts "gross, #{props[:flavor]} ice cream"
|
14
|
+
sleep 1
|
15
|
+
end
|
16
|
+
|
17
|
+
handle "mousse" do |props|
|
18
|
+
puts "what's a mousse?"
|
19
|
+
sleep 0.1
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
$:.unshift(File.expand_path("../../../lib", __FILE__))
|
2
|
+
|
3
|
+
require "simplejob"
|
4
|
+
|
5
|
+
SimpleJob.send("coffee")
|
6
|
+
SimpleJob.send("pie", :flavor => "apple", :size => "12in")
|
7
|
+
SimpleJob.send("ice_cream", :flavor => "vanilla")
|
8
|
+
SimpleJob.send("milkshake", :flavor => "chocolate")
|
9
|
+
SimpleJob.send("mousse")
|
10
|
+
SimpleJob.send("mousse")
|
11
|
+
SimpleJob.send("mousse")
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Wildcard handlers
|
2
|
+
=================
|
3
|
+
|
4
|
+
Open the consumer and wiretap in terminal windows, and then run the producer:
|
5
|
+
|
6
|
+
ruby -rubygems consumer.rb
|
7
|
+
ruby -rubygems wiretap.rb
|
8
|
+
ruby -rubygems producer.rb
|
9
|
+
|
10
|
+
You should see the consumer pulling requests of the queue and processing them, and the wiretap printing everything out. Try killing and restarting them, or even restarting RabbitMQ. No jobs or wiretapped jobs should be lost.
|
@@ -0,0 +1,14 @@
|
|
1
|
+
$:.unshift(File.expand_path("../../../lib", __FILE__))
|
2
|
+
|
3
|
+
require "simplejob"
|
4
|
+
|
5
|
+
SimpleJob::Worker.start(:queue_name => "foo") do
|
6
|
+
|
7
|
+
handle "foo.*" do |props|
|
8
|
+
puts "normal foo: #{props[:topic]}"
|
9
|
+
end
|
10
|
+
|
11
|
+
handle "foo.#" do |props|
|
12
|
+
puts "catchall foo: #{props[:topic]}"
|
13
|
+
end
|
14
|
+
end
|
data/lib/simplejob.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "json"
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
require "simplejob/client"
|
5
|
+
require "simplejob/worker"
|
6
|
+
|
7
|
+
module SimpleJob
|
8
|
+
|
9
|
+
# Send a work request
|
10
|
+
def self.send(topic, props = {})
|
11
|
+
Client.start do
|
12
|
+
raise "Message properties should be a Hash" unless props.kind_of?(Hash)
|
13
|
+
log.info "[simplejob] New job: #{topic}, #{props.inspect}"
|
14
|
+
publish(topic, props.to_json)
|
15
|
+
stop
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
def log
|
22
|
+
Logger.new(STDOUT)
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "amqp"
|
2
|
+
|
3
|
+
module SimpleJob
|
4
|
+
DEFAULT_EXCHANGE_NAME = "simplejob"
|
5
|
+
|
6
|
+
class Client
|
7
|
+
def self.start(opts = {}, &proc)
|
8
|
+
instance = new
|
9
|
+
instance.start(opts, &proc)
|
10
|
+
instance
|
11
|
+
end
|
12
|
+
|
13
|
+
def start(opts, &proc)
|
14
|
+
client = self
|
15
|
+
AMQP.start(opts) do
|
16
|
+
Signal.trap("INT") { puts; stop }
|
17
|
+
Signal.trap("TERM") { puts; stop }
|
18
|
+
|
19
|
+
@channel = AMQP::Channel.new
|
20
|
+
@exchange = exchange(opts[:exchange_name] || DEFAULT_EXCHANGE_NAME)
|
21
|
+
|
22
|
+
instance_exec(opts, &proc)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
AMQP.stop { EM.stop }
|
28
|
+
end
|
29
|
+
|
30
|
+
def exchange(name)
|
31
|
+
@channel.topic(name, :durable => true, :auto_delete => false)
|
32
|
+
end
|
33
|
+
|
34
|
+
def queue(name)
|
35
|
+
@channel.queue(name, :durable => true, :auto_delete => false)
|
36
|
+
end
|
37
|
+
|
38
|
+
def bind(queue, key)
|
39
|
+
queue.bind(@exchange, :key => key)
|
40
|
+
end
|
41
|
+
|
42
|
+
def publish(topic, message)
|
43
|
+
@exchange.publish(message, :routing_key => topic, :persistent => true)
|
44
|
+
end
|
45
|
+
|
46
|
+
def subscribe(queue, &proc)
|
47
|
+
queue.subscribe({ :ack => true }, &proc)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "active_support/core_ext/hash"
|
2
|
+
|
3
|
+
module SimpleJob
|
4
|
+
class Worker < Client
|
5
|
+
def start(opts, &proc)
|
6
|
+
super do
|
7
|
+
# only fetch one message at a time instead of being greedy
|
8
|
+
@channel.prefetch(1)
|
9
|
+
|
10
|
+
# create the queue
|
11
|
+
@queue = queue(opts[:queue_name] || "job_queue")
|
12
|
+
|
13
|
+
# defer to instance to set up handlers
|
14
|
+
instance_exec(&proc)
|
15
|
+
|
16
|
+
# start processing jobs
|
17
|
+
start_handler_loop
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def handle(key, &proc)
|
22
|
+
# bind the message to the queue
|
23
|
+
bind(@queue, key)
|
24
|
+
|
25
|
+
# register the handler
|
26
|
+
install_handler(key, proc)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def start_handler_loop
|
32
|
+
subscribe(@queue) do |headers, body|
|
33
|
+
unless AMQP.closing?
|
34
|
+
puts headers.inspect
|
35
|
+
topic = headers.properties[:routing_key]
|
36
|
+
|
37
|
+
begin
|
38
|
+
start_time = Time.now
|
39
|
+
log.info "\n#{"="*80}\n#{topic.upcase} BEGIN -- #{start_time}\n#{"="*80}"
|
40
|
+
|
41
|
+
props = JSON.parse(body).with_indifferent_access
|
42
|
+
props[:topic] = topic
|
43
|
+
log.info "[worker] Props: #{props.inspect}"
|
44
|
+
|
45
|
+
if handler = find_handler(topic)
|
46
|
+
handler.call(props)
|
47
|
+
else
|
48
|
+
log.error "[worker] no handler installed for #{topic}"
|
49
|
+
end
|
50
|
+
|
51
|
+
end_time = Time.now
|
52
|
+
elapsed = ((end_time - start_time) * 10).round / 10.0
|
53
|
+
log.info "\n#{"="*80}\n#{topic.upcase} END -- #{end_time} (elapsed: #{elapsed}s)\n#{"="*80}\n"
|
54
|
+
rescue Exception => error
|
55
|
+
log.error "[worker] handle blew up for #{topic} with body:\n#{body}\n#{error}\n#{error.backtrace.join("\n")}"
|
56
|
+
end
|
57
|
+
|
58
|
+
headers.ack
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def install_handler(key, proc)
|
64
|
+
@handlers ||= {}
|
65
|
+
@handlers[key] = { :block => proc, :re => key_to_regex(key) }
|
66
|
+
|
67
|
+
@handler_keys ||= []
|
68
|
+
@handler_keys << key
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_handler(topic)
|
72
|
+
return nil unless @handlers
|
73
|
+
|
74
|
+
# check for direct hit
|
75
|
+
if handler = @handlers[topic]
|
76
|
+
handler[:block]
|
77
|
+
|
78
|
+
# otherwise, test regexes
|
79
|
+
else
|
80
|
+
if key = @handler_keys.detect {|k| @handlers[k][:re].match(topic) }
|
81
|
+
@handlers[key][:block]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def key_to_regex(key)
|
87
|
+
Regexp.compile("^" + key.gsub('*', '[^\.]+').gsub('#', '.*') + "$")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
|
+
require 'simplejob'
|
16
|
+
|
17
|
+
class Test::Unit::TestCase
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simplejob
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ken Pratt
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-05-06 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activesupport
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 5
|
29
|
+
segments:
|
30
|
+
- 3
|
31
|
+
- 0
|
32
|
+
- 1
|
33
|
+
version: 3.0.1
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: json
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 1
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 5
|
48
|
+
- 1
|
49
|
+
version: 1.5.1
|
50
|
+
type: :runtime
|
51
|
+
version_requirements: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: amqp
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - "="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 1
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
- 7
|
64
|
+
- 1
|
65
|
+
version: 0.7.1
|
66
|
+
type: :runtime
|
67
|
+
version_requirements: *id003
|
68
|
+
description: A simple AMQP-backed job queuing system.
|
69
|
+
email: ken@kenpratt.net
|
70
|
+
executables: []
|
71
|
+
|
72
|
+
extensions: []
|
73
|
+
|
74
|
+
extra_rdoc_files:
|
75
|
+
- LICENSE.txt
|
76
|
+
- README.md
|
77
|
+
files:
|
78
|
+
- .document
|
79
|
+
- LICENSE.txt
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- VERSION
|
83
|
+
- examples/basic/README.md
|
84
|
+
- examples/basic/consumer.rb
|
85
|
+
- examples/basic/producer.rb
|
86
|
+
- examples/multiple_jobs/README.md
|
87
|
+
- examples/multiple_jobs/drink_consumer.rb
|
88
|
+
- examples/multiple_jobs/food_consumer.rb
|
89
|
+
- examples/multiple_jobs/producer.rb
|
90
|
+
- examples/wildcard_handlers/README.md
|
91
|
+
- examples/wildcard_handlers/consumer.rb
|
92
|
+
- examples/wildcard_handlers/producer.rb
|
93
|
+
- examples/wildcard_handlers/wiretap.rb
|
94
|
+
- lib/simplejob.rb
|
95
|
+
- lib/simplejob/client.rb
|
96
|
+
- lib/simplejob/worker.rb
|
97
|
+
- test/helper.rb
|
98
|
+
- test/test_simplejob.rb
|
99
|
+
homepage: http://github.com/kenpratt/simplejob
|
100
|
+
licenses:
|
101
|
+
- MIT
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
hash: 3
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
version: "0"
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
hash: 3
|
122
|
+
segments:
|
123
|
+
- 0
|
124
|
+
version: "0"
|
125
|
+
requirements: []
|
126
|
+
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 1.8.1
|
129
|
+
signing_key:
|
130
|
+
specification_version: 3
|
131
|
+
summary: Painless job queueing.
|
132
|
+
test_files: []
|
133
|
+
|