queue_classic 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/queue_classic.rb +15 -0
- data/lib/queue_classic/api.rb +32 -0
- data/lib/queue_classic/durable_array.rb +124 -0
- data/lib/queue_classic/queue.rb +25 -0
- data/lib/queue_classic/tasks.rb +13 -0
- data/lib/queue_classic/worker.rb +25 -0
- data/readme.markdown +118 -0
- data/test/api_test.rb +14 -0
- data/test/database_helpers.rb +48 -0
- data/test/durable_array_test.rb +90 -0
- data/test/helper.rb +8 -0
- data/test/queue_test.rb +5 -0
- data/test/worker_test.rb +22 -0
- metadata +104 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'pg'
|
3
|
+
|
4
|
+
$: << File.expand_path("lib")
|
5
|
+
|
6
|
+
require 'queue_classic/durable_array'
|
7
|
+
require 'queue_classic/worker'
|
8
|
+
require 'queue_classic/queue'
|
9
|
+
require 'queue_classic/api'
|
10
|
+
|
11
|
+
QC::Queue.setup :data_store => QC::DurableArray.new(:database => ENV["DATABASE_URL"])
|
12
|
+
|
13
|
+
module QC
|
14
|
+
extend Api
|
15
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module QC
|
2
|
+
module Api
|
3
|
+
|
4
|
+
def enqueue(job,*params)
|
5
|
+
Queue.enqueue(job,params)
|
6
|
+
end
|
7
|
+
|
8
|
+
def dequeue
|
9
|
+
Queue.dequeue
|
10
|
+
end
|
11
|
+
|
12
|
+
def delete(job)
|
13
|
+
Queue.delete(job)
|
14
|
+
end
|
15
|
+
|
16
|
+
def queue_length
|
17
|
+
Queue.length
|
18
|
+
end
|
19
|
+
|
20
|
+
def work(job)
|
21
|
+
klass = job.klass
|
22
|
+
method = job.method
|
23
|
+
params = job.params
|
24
|
+
|
25
|
+
klass.send(method,params)
|
26
|
+
delete(job)
|
27
|
+
rescue ArgumentError => e
|
28
|
+
puts "ArgumentError: #{e.inspect}"
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'pg'
|
3
|
+
|
4
|
+
module QC
|
5
|
+
class Job
|
6
|
+
attr_accessor :id, :details, :locked_at
|
7
|
+
def initialize(args={})
|
8
|
+
@id = args["id"]
|
9
|
+
@details = args["details"]
|
10
|
+
@locked_at = args["locked_at"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def klass
|
14
|
+
Kernel.const_get(details["job"].split(".").first)
|
15
|
+
end
|
16
|
+
|
17
|
+
def method
|
18
|
+
details["job"].split(".").last
|
19
|
+
end
|
20
|
+
|
21
|
+
def params
|
22
|
+
params = details["params"]
|
23
|
+
if params.length == 1
|
24
|
+
return params[0]
|
25
|
+
else
|
26
|
+
params
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class DurableArray
|
33
|
+
def initialize(args={})
|
34
|
+
@db_string = args[:database]
|
35
|
+
@connection = connection
|
36
|
+
execute("SET client_min_messages TO 'warning'")
|
37
|
+
execute("LISTEN jobs")
|
38
|
+
end
|
39
|
+
|
40
|
+
def <<(details)
|
41
|
+
execute(
|
42
|
+
"INSERT INTO jobs" +
|
43
|
+
"(details)" +
|
44
|
+
"VALUES ('#{details.to_json}')"
|
45
|
+
)
|
46
|
+
execute("NOTIFY jobs, 'new-job'")
|
47
|
+
end
|
48
|
+
|
49
|
+
def count
|
50
|
+
execute("SELECT COUNT(*) from jobs")[0]["count"].to_i
|
51
|
+
end
|
52
|
+
|
53
|
+
def delete(job)
|
54
|
+
execute("DELETE FROM jobs WHERE id = #{job.id}")
|
55
|
+
job
|
56
|
+
end
|
57
|
+
|
58
|
+
def find(job)
|
59
|
+
find_one {"SELECT * FROM jobs WHERE id = #{job.id}"}
|
60
|
+
end
|
61
|
+
|
62
|
+
def head
|
63
|
+
find_one {"SELECT * FROM jobs ORDER BY id ASC LIMIT 1"}
|
64
|
+
end
|
65
|
+
alias :first :head
|
66
|
+
|
67
|
+
def lock_head
|
68
|
+
job = nil
|
69
|
+
@connection.transaction do
|
70
|
+
job = find_one {"SELECT * FROM jobs WHERE locked_at IS NULL ORDER BY id ASC LIMIT 1 FOR UPDATE"}
|
71
|
+
return nil unless job
|
72
|
+
locked = execute("UPDATE jobs SET locked_at = (CURRENT_TIMESTAMP) WHERE id = #{job.id} AND locked_at IS NULL")
|
73
|
+
end
|
74
|
+
job
|
75
|
+
end
|
76
|
+
|
77
|
+
def b_head
|
78
|
+
if job = lock_head
|
79
|
+
job
|
80
|
+
else
|
81
|
+
@connection.wait_for_notify {|e,p,msg| job = lock_head if msg == "new-job" }
|
82
|
+
job
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def each
|
87
|
+
execute("SELECT * FROM jobs ORDER BY id ASC").each do |r|
|
88
|
+
yield(JSON.parse(r["details"]))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def execute(sql)
|
93
|
+
@connection.async_exec(sql)
|
94
|
+
end
|
95
|
+
|
96
|
+
def find_one
|
97
|
+
res = execute(yield)
|
98
|
+
if res.cmd_tuples > 0
|
99
|
+
res.map do |r|
|
100
|
+
Job.new(
|
101
|
+
"id" => r["id"],
|
102
|
+
"details" => JSON.parse( r["details"]),
|
103
|
+
"locked_at" => r["locked_at"]
|
104
|
+
)
|
105
|
+
end.pop
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def connection
|
110
|
+
db_params = URI.parse(@db_string)
|
111
|
+
if db_params.scheme == "postgres"
|
112
|
+
PGconn.connect(
|
113
|
+
:dbname => db_params.path.gsub("/",""),
|
114
|
+
:user => db_params.user,
|
115
|
+
:password => db_params.password,
|
116
|
+
:host => db_params.host
|
117
|
+
)
|
118
|
+
else
|
119
|
+
PGconn.connect(:dbname => @db_string)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module QC
|
2
|
+
class Queue
|
3
|
+
def self.setup(args={})
|
4
|
+
@@data = args[:data_store] || []
|
5
|
+
self
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.enqueue(job,params)
|
9
|
+
@@data << {"job" => job, "params" => params}
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.dequeue
|
13
|
+
@@data.b_head
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.delete(job)
|
17
|
+
@@data.delete(job)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.length
|
21
|
+
@@data.count
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module QC
|
2
|
+
class Worker
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@worker_id = rand(1000)
|
6
|
+
end
|
7
|
+
|
8
|
+
def start
|
9
|
+
puts "#{@worker_id} ready for work"
|
10
|
+
loop { work }
|
11
|
+
end
|
12
|
+
|
13
|
+
def work
|
14
|
+
job = QC.dequeue
|
15
|
+
# if we are here, dequeue has unblocked
|
16
|
+
# and we may have a job.
|
17
|
+
if job
|
18
|
+
puts "#{@worker_id} working job"
|
19
|
+
QC.work(job)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/readme.markdown
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# Queue Classic
|
2
|
+
__Alpha 0.1.5__
|
3
|
+
|
4
|
+
_Queue Classic 0.1.5 is not ready for production. However, it is under active development and I expect a beta release within the following months._
|
5
|
+
|
6
|
+
Queue Classic is an alternative queueing library for Ruby apps (Rails, Sinatra, Etc...) Queue Classic features __asynchronous__ job polling, database maintained locks and
|
7
|
+
no ridiculous dependencies. As a matter of fact, Queue Classic only requires the __pg__ and __json__.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
### TL;DR
|
12
|
+
1. gem install queue_classic
|
13
|
+
2. add jobs table to your database
|
14
|
+
3. QC.enqueue "Class.method", :arg1 => val1
|
15
|
+
4. rake qc:work
|
16
|
+
|
17
|
+
### Gem
|
18
|
+
|
19
|
+
gem install queue_classic
|
20
|
+
|
21
|
+
### Database
|
22
|
+
|
23
|
+
Queue Classic needs a database, so make sure that DATABASE_URL points to your database. If you are unsure about whether this var is set, run:
|
24
|
+
echo $DATABASE_URL
|
25
|
+
in your shell. If you are using Heroku, this var is set and pointing to your primary database.
|
26
|
+
|
27
|
+
Once your Database is set, you will need to add a jobs table. If you are using rails, add a migration with the following tables:
|
28
|
+
|
29
|
+
class CreateJobsTable < ActiveRecord::Migration
|
30
|
+
def self.up
|
31
|
+
create_table :jobs do |t|
|
32
|
+
t.text :details
|
33
|
+
t.timestamp :locked_at
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.down
|
38
|
+
drop_table :jobs
|
39
|
+
end
|
40
|
+
end
|
41
|
+
After running this migration, your database should be ready to go. As a sanity check, enqueue a job and then issue a SELECT in the postgres console.
|
42
|
+
|
43
|
+
__script/console__
|
44
|
+
QC.enqueue "Class.method"
|
45
|
+
__Terminal__
|
46
|
+
psql you_database_name
|
47
|
+
select * from jobs;
|
48
|
+
You should see the job "Class.method"
|
49
|
+
|
50
|
+
### Rakefile
|
51
|
+
|
52
|
+
As a convenience, I added a rake task that responds to `rake jobs:work` There are also rake tasks in the `qc` name space.
|
53
|
+
To get access to these tasks, Add `require 'queue_classic/tasks'` to your Rakefile.
|
54
|
+
|
55
|
+
## Fundamentals
|
56
|
+
|
57
|
+
### Enqueue
|
58
|
+
|
59
|
+
To place a job onto the queue, you should specify a class and a class method. The syntax should be:
|
60
|
+
|
61
|
+
QC.enqueue('Class.method', :arg1 => 'value1', :arg2 => 'value2')
|
62
|
+
|
63
|
+
The job gets stored in the jobs table with a details field set to: `{ job: Class.method, params: {arg1: value1, arg2: value2}}` (as JSON)
|
64
|
+
Class can be any class and method can be anything that Class will respond to. For example:
|
65
|
+
|
66
|
+
class Invoice < ActiveRecord::Base
|
67
|
+
def self.process(invoice_id)
|
68
|
+
invoice = find(invoice_id)
|
69
|
+
invoice.process!
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.process_all
|
73
|
+
Invoice.all do |invoice|
|
74
|
+
QC.enqueue "Invoice.process", invoice.id
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
### Dequeue
|
81
|
+
|
82
|
+
Traditionally, a queue's dequeue operation will remove the item from the queue. However, Queue Classic will not delete the item from the queue, it will lock it
|
83
|
+
and then the worker will delete the job once it has finished working it. Queue Classic's greatest strength is it's ability to safely lock jobs. Unlike other
|
84
|
+
database backed queing libraries, Classic Queue uses the database time to lock. This allows you to be more relaxed about the time synchronization of your worker machines.
|
85
|
+
|
86
|
+
Finally, the strongest feature of Queue Classic is it's ability to block on on dequeue. This design removes the need to __ Sleep & SELECT. __ Queue Classic takes advantage
|
87
|
+
of the wonderul PUB/SUB featuers built in to Postgres. Basically there is a channel in which the workers LISTEN. When a new job is added to the queue, the queue sends NOTIFY
|
88
|
+
messages on the channel. Once a NOTIFY is sent, each worker races to acquire a lock on a job. A job is awareded to the victor while the rest go back to wait for another job.
|
89
|
+
|
90
|
+
## FAQ
|
91
|
+
|
92
|
+
How is this different than DJ?
|
93
|
+
> TL;DR = Store job as JSON (better introspection), Queue manages the time for locking jobs (workers can be out of sync.), No magic (less code), Small footprint (ORM Free).
|
94
|
+
|
95
|
+
> __Introspection__ I want the data in the queue to be as simple as possible. Since we only store the Class, Method and Args, introspection into the queue is
|
96
|
+
quite simpler.
|
97
|
+
|
98
|
+
> __Locking__ You might have noticed that DJ's worker calls Time.now(). In a cloud environment, this could allow for workers to be confused about
|
99
|
+
the status of a job. Classic Queue locks a job using Postgres' TIMESTAMP function.
|
100
|
+
|
101
|
+
> __Magic__ I find disdain for methods on my objects that have nothing to do with the purpose of the object. Methods like "should" and "delay"
|
102
|
+
are quite distasteful and obscure what is actually going on. If you use TestUnit for this reason, you might like Queue Classic. Anyway, I think
|
103
|
+
the fundamental concept of a message queue is not that difficult to grasp; therefore, I have taken the time to make Queue Classic as transparent as possilbe.
|
104
|
+
|
105
|
+
> __Footprint__ You don't need ActiveRecord or any other ORM to find the head or add to the tail. Take a look at the DurableArray class to see the SQL Classic Queue employees.
|
106
|
+
|
107
|
+
Why doesn't your queue retry failed jobs?
|
108
|
+
> I believe the Class method should handle any sort of exception. Also, I think
|
109
|
+
that the model you are working on should know about it's state. For instance, if you are
|
110
|
+
creating jobs for the emailing of newsletters; put a emailed_at column on your newsletter model
|
111
|
+
and then right before the job quits, touch the emailed_at column.
|
112
|
+
|
113
|
+
Can I use this library with 50 Heroku Workers?
|
114
|
+
> Maybe. I haven't tested 50 workers yet. But it is definitely a goal for Queue Classic. I am not sure when,
|
115
|
+
but you can count on this library being able to handle all Heroku can throw at it.
|
116
|
+
|
117
|
+
Why does this project seem incomplete? Will you make it production ready?
|
118
|
+
> I started this project on 1/24/2011. Check back soon! Also, feel free to contact me to find out how passionate I am about queueing.
|
data/test/api_test.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.expand_path("../helper.rb", __FILE__)
|
2
|
+
|
3
|
+
class ApiTest < MiniTest::Unit::TestCase
|
4
|
+
include DatabaseHelpers
|
5
|
+
|
6
|
+
def test_enqueue_takes_a_job
|
7
|
+
clean_database
|
8
|
+
|
9
|
+
assert_equal 0, QC.queue_length
|
10
|
+
res = QC.enqueue "Notifier.send", {}
|
11
|
+
assert_equal 1, QC.queue_length
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module DatabaseHelpers
|
2
|
+
|
3
|
+
def clean_database
|
4
|
+
drop_table
|
5
|
+
create_table
|
6
|
+
disconnect
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_database
|
10
|
+
postgres.exec "CREATE DATABASE #{ENV['DATABASE_URL']}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def drop_database
|
14
|
+
postgres.exec "DROP DATABASE IF EXISTS #{ENV['DATABASE_URL']}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_table
|
18
|
+
jobs_db.exec(
|
19
|
+
"CREATE TABLE jobs" +
|
20
|
+
"(" +
|
21
|
+
"id SERIAL," +
|
22
|
+
"details text," +
|
23
|
+
"locked_at timestamp without time zone" +
|
24
|
+
");"
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def drop_table
|
29
|
+
jobs_db.exec("DROP TABLE IF EXISTS jobs")
|
30
|
+
end
|
31
|
+
|
32
|
+
def disconnect
|
33
|
+
jobs_db.finish
|
34
|
+
postgres.finish
|
35
|
+
end
|
36
|
+
|
37
|
+
def jobs_db
|
38
|
+
@jobs_db ||= PGconn.connect(:dbname => ENV["DATABASE_URL"])
|
39
|
+
@jobs_db.exec("SET client_min_messages TO 'warning'")
|
40
|
+
@jobs_db
|
41
|
+
end
|
42
|
+
|
43
|
+
def postgres
|
44
|
+
@postgres ||= PGconn.connect(:dbname => 'postgres')
|
45
|
+
@postgres.exec("SET client_min_messages TO 'warning'")
|
46
|
+
@postgres
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require File.expand_path("../helper.rb", __FILE__)
|
2
|
+
|
3
|
+
class DurableArrayTest < MiniTest::Unit::TestCase
|
4
|
+
include DatabaseHelpers
|
5
|
+
|
6
|
+
def test_head_decodes_json
|
7
|
+
clean_database
|
8
|
+
array = QC::DurableArray.new(:database => "queue_classic_test")
|
9
|
+
|
10
|
+
array << {"test" => "ok"}
|
11
|
+
assert_equal({"test" => "ok"}, array.head.details)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_count_returns_number_of_rows
|
15
|
+
clean_database
|
16
|
+
array = QC::DurableArray.new(:database => "queue_classic_test")
|
17
|
+
|
18
|
+
array << {"test" => "ok"}
|
19
|
+
assert_equal 1, array.count
|
20
|
+
array << {"test" => "ok"}
|
21
|
+
assert_equal 2, array.count
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_head_returns_first_job
|
25
|
+
clean_database
|
26
|
+
array = QC::DurableArray.new(:database => "queue_classic_test")
|
27
|
+
|
28
|
+
job = {"job" => "one"}
|
29
|
+
array << job
|
30
|
+
assert_equal job, array.head.details
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_head_returns_first_job_when_many_are_in_array
|
34
|
+
clean_database
|
35
|
+
array = QC::DurableArray.new(:database => "queue_classic_test")
|
36
|
+
|
37
|
+
array << {"job" => "one"}
|
38
|
+
array << {"job" => "two"}
|
39
|
+
assert_equal({"job" => "one"}, array.head.details)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_delete_removes_job_from_array
|
43
|
+
clean_database
|
44
|
+
array = QC::DurableArray.new(:database => "queue_classic_test")
|
45
|
+
|
46
|
+
array << {"job" => "one"}
|
47
|
+
assert_equal( {"job" => "one"}, array.head.details)
|
48
|
+
array.delete(array.head)
|
49
|
+
assert_nil array.head
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_delete_returns_job_after_delete
|
53
|
+
clean_database
|
54
|
+
array = QC::DurableArray.new(:database => "queue_classic_test")
|
55
|
+
|
56
|
+
array << {"job" => "one"}
|
57
|
+
assert_equal({"job" => "one"}, array.head.details)
|
58
|
+
|
59
|
+
res = array.delete(array.head)
|
60
|
+
assert_nil(array.head)
|
61
|
+
assert_equal({"job" => "one"}, res.details)
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_each_yields_the_details_for_each_job
|
65
|
+
clean_database
|
66
|
+
array = QC::DurableArray.new(:database => "queue_classic_test")
|
67
|
+
|
68
|
+
array << {"job" => "one"}
|
69
|
+
array << {"job" => "two"}
|
70
|
+
results = []
|
71
|
+
array.each {|v| results << v}
|
72
|
+
assert_equal([{"job" => "one"},{"job" => "two"}], results)
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_connection_builds_db_connection_for_uri
|
76
|
+
array = QC::DurableArray.new(:database => "postgres://ryandotsmith:@localhost/queue_classic_test")
|
77
|
+
assert_equal "ryandotsmith", array.connection.user
|
78
|
+
assert_equal "localhost", array.connection.host
|
79
|
+
assert_equal "queue_classic_test", array.connection.db
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_connection_builds_db_connection_for_database
|
83
|
+
array = QC::DurableArray.new(:database => "queue_classic_test")
|
84
|
+
|
85
|
+
# FIXME not everyone will have a postgres user named: ryandotsmith
|
86
|
+
assert_equal "ryandotsmith", array.connection.user
|
87
|
+
assert_equal "queue_classic_test", array.connection.db
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
data/test/helper.rb
ADDED
data/test/queue_test.rb
ADDED
data/test/worker_test.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path("../helper.rb", __FILE__)
|
2
|
+
|
3
|
+
class TestNotifier
|
4
|
+
def self.deliver(args={})
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class WorkerTest < MiniTest::Unit::TestCase
|
9
|
+
include DatabaseHelpers
|
10
|
+
|
11
|
+
def test_working_a_job
|
12
|
+
clean_database
|
13
|
+
|
14
|
+
QC.enqueue "TestNotifier.deliver", {}
|
15
|
+
worker = QC::Worker.new
|
16
|
+
|
17
|
+
assert_equal(1, QC.queue_length)
|
18
|
+
worker.work
|
19
|
+
assert_equal(0, QC.queue_length)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: queue_classic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 5
|
9
|
+
version: 0.1.5
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Ryan Smith
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-01-28 00:00:00 -08:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: pg
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: json
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :runtime
|
45
|
+
version_requirements: *id002
|
46
|
+
description: Queue Classic is an alternative queueing library for Ruby apps (Rails, Sinatra, Etc...) Queue Classic features asynchronous job polling, database maintained locks and no ridiculous dependencies. As a matter of fact, Queue Classic only requires the pg and json.
|
47
|
+
email: ryan@heroku.com
|
48
|
+
executables: []
|
49
|
+
|
50
|
+
extensions: []
|
51
|
+
|
52
|
+
extra_rdoc_files: []
|
53
|
+
|
54
|
+
files:
|
55
|
+
- readme.markdown
|
56
|
+
- lib/queue_classic/api.rb
|
57
|
+
- lib/queue_classic/durable_array.rb
|
58
|
+
- lib/queue_classic/queue.rb
|
59
|
+
- lib/queue_classic/tasks.rb
|
60
|
+
- lib/queue_classic/worker.rb
|
61
|
+
- lib/queue_classic.rb
|
62
|
+
- test/api_test.rb
|
63
|
+
- test/database_helpers.rb
|
64
|
+
- test/durable_array_test.rb
|
65
|
+
- test/helper.rb
|
66
|
+
- test/queue_test.rb
|
67
|
+
- test/worker_test.rb
|
68
|
+
has_rdoc: true
|
69
|
+
homepage: http://github.com/ryandotsmith/Queue-Classic
|
70
|
+
licenses: []
|
71
|
+
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
version: "0"
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
93
|
+
requirements: []
|
94
|
+
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.3.7
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: Queue Classic is an alternative queueing library for Ruby apps (Rails, Sinatra, Etc...) Queue Classic features asynchronous job polling, database maintained locks and no ridiculous dependencies. As a matter of fact, Queue Classic only requires the pg and json.(simple)
|
100
|
+
test_files:
|
101
|
+
- test/api_test.rb
|
102
|
+
- test/durable_array_test.rb
|
103
|
+
- test/queue_test.rb
|
104
|
+
- test/worker_test.rb
|