queue_classic 0.2.2 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,16 +2,16 @@ require 'json'
2
2
  require 'pg'
3
3
  require 'uri'
4
4
 
5
- $: << File.expand_path("lib")
5
+ $: << File.expand_path(__FILE__, 'lib')
6
6
 
7
7
  require 'queue_classic/durable_array'
8
+ require 'queue_classic/database'
8
9
  require 'queue_classic/worker'
9
10
  require 'queue_classic/queue'
10
- require 'queue_classic/api'
11
11
  require 'queue_classic/job'
12
12
 
13
13
  module QC
14
- extend Api
14
+ def self.method_missing(sym, *args, &block)
15
+ Queue.send(sym, *args, &block)
16
+ end
15
17
  end
16
-
17
- QC::Queue.instance.setup :data_store => QC::DurableArray.new(:database => ENV["DATABASE_URL"])
@@ -0,0 +1,128 @@
1
+ module QC
2
+ class Database
3
+
4
+ MAX_TOP_BOUND = 9
5
+ DEFAULT_QUEUE_NAME = "queue_classic_jobs"
6
+
7
+ def self.create_queue(name)
8
+ db = new(name)
9
+ db.create_table
10
+ db.disconnect
11
+ true
12
+ end
13
+
14
+ attr_reader :table_name
15
+
16
+ def initialize(queue_name=nil)
17
+ @top_boundry = ENV["TOP_BOUND"] || MAX_TOP_BOUND
18
+ @table_name = queue_name || DEFAULT_QUEUE_NAME
19
+ @db_params = URI.parse(ENV["DATABASE_URL"])
20
+ end
21
+
22
+ def init_db
23
+ drop_table
24
+ create_table
25
+ load_functions
26
+ end
27
+
28
+ def waiting_conns
29
+ execute("SELECT * FROM pg_stat_activity WHERE datname = '#{@name}' AND waiting = 't' AND application_name = 'queue_classic'")
30
+ end
31
+
32
+ def all_conns
33
+ execute("SELECT * FROM pg_stat_activity WHERE datname = '#{@name}' AND application_name = 'queue_classic'")
34
+ end
35
+
36
+ def silence_warnings
37
+ execute("SET client_min_messages TO 'warning'")
38
+ end
39
+
40
+ def execute(sql)
41
+ connection.exec(sql)
42
+ end
43
+
44
+ def disconnect
45
+ connection.finish
46
+ end
47
+
48
+ def connection
49
+ if defined? @connection
50
+ @connection
51
+ else
52
+ @name = @db_params.path.gsub("/","")
53
+ @connection = PGconn.connect(
54
+ :dbname => @db_params.path.gsub("/",""),
55
+ :user => @db_params.user,
56
+ :password => @db_params.password,
57
+ :host => @db_params.host
58
+ )
59
+ @connection.exec("LISTEN queue_classic_jobs")
60
+ @connection.exec("SET application_name = 'queue_classic'")
61
+ silence_warnings unless ENV["LOGGING_ENABLED"]
62
+ @connection
63
+ end
64
+ end
65
+
66
+ def drop_table
67
+ execute("DROP TABLE IF EXISTS #{@table_name} CASCADE")
68
+ end
69
+
70
+ def create_table
71
+ execute("CREATE TABLE #{@table_name} (id serial, details text, locked_at timestamp)")
72
+ execute("CREATE INDEX #{@table_name}_id_idx ON #{@table_name} (id)")
73
+ end
74
+
75
+ def load_functions
76
+ execute(<<-EOD)
77
+ -- We are declaring the return type to be queue_classic_jobs.
78
+ -- This is ok since I am assuming that all of the users added queues will
79
+ -- have identical columns to queue_classic_jobs.
80
+ -- When QC supports queues with columns other than the default, we will have to change this.
81
+
82
+ CREATE OR REPLACE FUNCTION lock_head(tname varchar) RETURNS SETOF queue_classic_jobs AS $$
83
+ DECLARE
84
+ unlocked integer;
85
+ relative_top integer;
86
+ job_count integer;
87
+ BEGIN
88
+ SELECT TRUNC(random() * #{@top_boundry} + 1) INTO relative_top;
89
+ EXECUTE 'SELECT count(*) FROM' || tname || '' INTO job_count;
90
+
91
+ IF job_count < 10 THEN
92
+ relative_top = 0;
93
+ END IF;
94
+
95
+ LOOP
96
+ BEGIN
97
+ EXECUTE 'SELECT id FROM '
98
+ || tname::regclass
99
+ || ' WHERE locked_at IS NULL'
100
+ || ' ORDER BY id ASC'
101
+ || ' LIMIT 1'
102
+ || ' OFFSET ' || relative_top
103
+ || ' FOR UPDATE NOWAIT'
104
+ INTO unlocked;
105
+ EXIT;
106
+ EXCEPTION
107
+ WHEN lock_not_available THEN
108
+ -- do nothing. loop again and hope we get a lock
109
+ END;
110
+ END LOOP;
111
+
112
+
113
+ RETURN QUERY EXECUTE 'UPDATE '
114
+ || tname::regclass
115
+ || ' SET locked_at = (CURRENT_TIMESTAMP)'
116
+ || ' WHERE id = $1'
117
+ || ' AND locked_at is NULL'
118
+ || ' RETURNING *'
119
+ USING unlocked;
120
+
121
+ RETURN;
122
+ END;
123
+ $$ LANGUAGE plpgsql;
124
+ EOD
125
+ end
126
+
127
+ end
128
+ end
@@ -1,77 +1,62 @@
1
1
  module QC
2
2
  class DurableArray
3
3
 
4
- def initialize(args={})
5
- @db_string = args[:database]
4
+ def initialize(database)
5
+ @database = database
6
+ @table_name = @database.table_name
6
7
  end
7
8
 
8
9
  def <<(details)
9
- execute("INSERT INTO jobs (details) VALUES ('#{details.to_json}')")
10
- execute("NOTIFY jobs, 'new-job'")
10
+ execute("INSERT INTO #{@table_name} (details) VALUES ('#{details.to_json}')")
11
+ execute("NOTIFY queue_classic_jobs, 'new-job'")
11
12
  end
12
13
 
13
14
  def count
14
- execute("SELECT COUNT(*) from jobs")[0]["count"].to_i
15
+ execute("SELECT COUNT(*) FROM #{@table_name}")[0]["count"].to_i
15
16
  end
16
17
 
17
18
  def delete(job)
18
- execute("DELETE FROM jobs WHERE id = #{job.id}")
19
+ execute("DELETE FROM #{@table_name} WHERE id = #{job.id}")
19
20
  job
20
21
  end
21
22
 
22
23
  def find(job)
23
- find_one {"SELECT * FROM jobs WHERE id = #{job.id}"}
24
+ find_one {"SELECT * FROM #{@table_name} WHERE id = #{job.id}"}
25
+ end
26
+
27
+ def search_details_column(q)
28
+ find_many { "SELECT * FROM #{@table_name} WHERE details LIKE '%#{q}%'" }
24
29
  end
25
30
 
26
31
  def lock_head
27
- job = nil
28
- connection.transaction do
29
- if job = find_one {"SELECT * FROM jobs WHERE locked_at IS NULL ORDER BY id ASC LIMIT 1 FOR UPDATE"}
30
- execute("UPDATE jobs SET locked_at = (CURRENT_TIMESTAMP) WHERE id = #{job.id} AND locked_at IS NULL")
31
- end
32
- end
33
- job
32
+ find_one { "SELECT * FROM lock_head('#{@table_name}')" }
34
33
  end
35
34
 
36
35
  def first
37
36
  if job = lock_head
38
37
  job
39
38
  else
40
- execute("LISTEN jobs")
41
- connection.wait_for_notify {|e,p,msg| job = lock_head if msg == "new-job" }
39
+ @database.connection.wait_for_notify(1) {|e,p,msg| job = lock_head if msg == "new-job" }
42
40
  job
43
41
  end
44
42
  end
45
43
 
46
44
  def each
47
- execute("SELECT * FROM jobs ORDER BY id ASC").each do |r|
45
+ execute("SELECT * FROM #{@table_name} ORDER BY id ASC").each do |r|
48
46
  yield Job.new(r)
49
47
  end
50
48
  end
51
49
 
52
- def execute(sql)
53
- connection.exec(sql)
50
+ def find_one(&blk)
51
+ find_many(&blk).pop
54
52
  end
55
53
 
56
- def find_one
57
- res = execute(yield)
58
- if res.count > 0
59
- res.map {|r| Job.new(r)}.pop
60
- end
54
+ def find_many
55
+ execute(yield).map {|r| Job.new(r)}
61
56
  end
62
57
 
63
- def connection
64
- db_params = URI.parse(@db_string)
65
- @connection ||= if db_params.scheme == "postgres"
66
- PGconn.connect(
67
- :dbname => db_params.path.gsub("/",""),
68
- :user => db_params.user,
69
- :password => db_params.password,
70
- :host => db_params.host
71
- )
72
- else
73
- PGconn.connect(:dbname => @db_string)
74
- end
58
+ def execute(sql)
59
+ @database.execute(sql)
75
60
  end
76
61
 
77
62
  end
@@ -30,5 +30,13 @@ module QC
30
30
  end
31
31
  end
32
32
 
33
+ def work
34
+ if params.class == Array
35
+ klass.send(method,*params)
36
+ else
37
+ klass.send(method,params)
38
+ end
39
+ end
40
+
33
41
  end
34
42
  end
@@ -1,30 +1,85 @@
1
- require 'singleton'
1
+ module QC
2
+ module AbstractQueue
2
3
 
3
- module QC
4
- class Queue
5
- include Singleton
6
- def setup(args={})
7
- @data = args[:data_store]
4
+ def enqueue(job,*params)
5
+ if job.respond_to?(:details) and job.respond_to?(:params)
6
+ job = job.signature
7
+ params = *job.params
8
+ end
9
+ array << {"job" => job, "params" => params}
8
10
  end
9
11
 
10
- def enqueue(job,params)
11
- @data << {"job" => job, "params" => params}
12
+ def dequeue
13
+ array.first
12
14
  end
13
15
 
14
- def dequeue
15
- @data.first
16
+ def query(signature)
17
+ array.search_details_column(signature)
16
18
  end
17
19
 
18
20
  def delete(job)
19
- @data.delete(job)
21
+ array.delete(job)
20
22
  end
21
23
 
22
24
  def delete_all
23
- @data.each {|j| delete(j) }
25
+ array.each {|j| delete(j) }
24
26
  end
25
27
 
26
28
  def length
27
- @data.count
29
+ array.count
30
+ end
31
+
32
+ end
33
+ end
34
+
35
+ module QC
36
+ module ConnectionHelper
37
+
38
+ def connection_status
39
+ {:total => database.all_conns.count, :waiting => database.waiting_conns.count}
40
+ end
41
+
42
+ def disconnect
43
+ database.disconnect
28
44
  end
45
+
46
+ end
47
+ end
48
+
49
+ module QC
50
+ class Queue
51
+
52
+ include AbstractQueue
53
+ extend AbstractQueue
54
+
55
+ include ConnectionHelper
56
+ extend ConnectionHelper
57
+
58
+ def self.array
59
+ if defined? @@array
60
+ @@array
61
+ else
62
+ @@database = Database.new
63
+ @@array = DurableArray.new(@@database)
64
+ end
65
+ end
66
+
67
+ def self.database
68
+ @@database
69
+ end
70
+
71
+ def initialize(queue_name)
72
+ @database = Database.new(queue_name)
73
+ @array = DurableArray.new(@database)
74
+ end
75
+
76
+ def array
77
+ @array
78
+ end
79
+
80
+ def database
81
+ @database
82
+ end
83
+
29
84
  end
30
85
  end
@@ -1,13 +1,29 @@
1
1
  namespace :jobs do
2
+
2
3
  task :work => :environment do
3
4
  QC::Worker.new.start
4
5
  end
6
+
5
7
  end
8
+
6
9
  namespace :qc do
7
- task :work do
10
+
11
+ task :work => :environment do
8
12
  QC::Worker.new.start
9
13
  end
10
- task :jobs do
14
+
15
+ task :jobs => :environment do
11
16
  QC.queue_length
12
17
  end
18
+
19
+ task :load_functions => :environment do
20
+ db = QC::Database.new
21
+ db.load_functions
22
+ db.disconnect
23
+ end
24
+
25
+ task :create_queue, :name, :needs => :environment do |t,args|
26
+ QC::Database.create_queue(args[:name])
27
+ end
28
+
13
29
  end
@@ -1,18 +1,42 @@
1
1
  module QC
2
2
  class Worker
3
3
 
4
+ def initialize
5
+ @running = true
6
+ @queue = QC::Queue.new(ENV["QUEUE"])
7
+ handle_signals
8
+ end
9
+
10
+ def running?
11
+ @running
12
+ end
13
+
14
+ def handle_signals
15
+ %W(INT TRAP).each do |sig|
16
+ trap(sig) do
17
+ if running?
18
+ @running = false
19
+ else
20
+ raise Interrupt
21
+ end
22
+ end
23
+ end
24
+ end
25
+
4
26
  def start
5
- loop { work }
27
+ while running? do
28
+ work
29
+ end
6
30
  end
7
31
 
8
32
  def work
9
- if job = QC.dequeue #blocks until we have a job
33
+ if job = @queue.dequeue #blocks until we have a job
10
34
  begin
11
- QC.work(job)
35
+ job.work
12
36
  rescue Object => e
13
37
  handle_failure(job,e)
14
38
  ensure
15
- QC.delete(job)
39
+ @queue.delete(job)
16
40
  end
17
41
  end
18
42
  end
@@ -0,0 +1,33 @@
1
+ # Queue Classic
2
+ __Beta 0.3.1__
3
+
4
+ __Queue Classic 0.3.1 is in Beta.__ I have been using this library with 30-150 Heroku workers and have had great results.
5
+
6
+ I am using this in production applications and plan to maintain and support this library for a long time.
7
+
8
+ Queue Classic is a queueing library for Ruby apps (Rails, Sinatra, Etc...) Queue Classic features a blocking dequeue, database maintained locks and
9
+ no ridiculous dependencies. As a matter of fact, Queue Classic only requires the __pg__ and __json__.
10
+
11
+ [Discussion Group](http://groups.google.com/group/queue_classic)
12
+
13
+ [Wiki](https://github.com/ryandotsmith/queue_classic/wiki)
14
+
15
+ ## Installation
16
+
17
+ 1. $ gem install queue_classic
18
+ 2. psql=# CREATE TABLE queue_classic_jobs (id serial, details text, locked_at timestamp);
19
+ 3. psql=# CREATE INDEX queue_classic_jobs_id_idx ON queue_classic_jobs (id);
20
+ 4. $ rake qc:load_functions
21
+ 5. irb: QC.enqueue "Class.method", "arg"
22
+ 6. $ rake jobs:work
23
+
24
+ ### Upgrade from < 0.2.3 to 0.3.0
25
+
26
+ $ psql your_database
27
+ your_database=# ALTER TABLE jobs RENAME TO queue_classic_jobs;
28
+
29
+ ### Dependencies
30
+
31
+ Postgres version 9
32
+
33
+ Ruby (gems: pg, json)
@@ -1,49 +1,11 @@
1
1
  module DatabaseHelpers
2
2
 
3
- def clean_database
4
- drop_table
5
- create_table
6
- disconnect
3
+ def init_db(table_name=nil)
4
+ database = QC::Database.new(table_name)
5
+ database.silence_warnings
6
+ database.init_db
7
+ database.disconnect
8
+ true
7
9
  end
8
10
 
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
- jobs_db.exec("CREATE INDEX jobs_id_idx ON jobs (id)")
27
- end
28
-
29
- def drop_table
30
- jobs_db.exec("DROP TABLE IF EXISTS jobs")
31
- end
32
-
33
- def disconnect
34
- jobs_db.finish
35
- postgres.finish
36
- end
37
-
38
- def jobs_db
39
- @jobs_db ||= PGconn.connect(:dbname => ENV["DATABASE_URL"])
40
- @jobs_db.exec("SET client_min_messages TO 'warning'")
41
- @jobs_db
42
- end
43
-
44
- def postgres
45
- @postgres ||= PGconn.connect(:dbname => 'postgres')
46
- @postgres.exec("SET client_min_messages TO 'warning'")
47
- @postgres
48
- end
49
11
  end
@@ -1,95 +1,92 @@
1
1
  require File.expand_path("../helper.rb", __FILE__)
2
2
 
3
- class DurableArrayTest < MiniTest::Unit::TestCase
4
- include DatabaseHelpers
3
+ context "DurableArray" do
5
4
 
6
- def test_first_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.first.details)
5
+ setup do
6
+ @database = QC::Database.new
7
+ @database.drop_table
8
+ @database.init_db
9
+ @array = QC::DurableArray.new(@database)
12
10
  end
13
11
 
14
- def test_count_returns_number_of_rows
15
- clean_database
16
- array = QC::DurableArray.new(:database => "queue_classic_test")
12
+ teardown do
13
+ @database.disconnect
14
+ end
17
15
 
18
- array << {"test" => "ok"}
19
- assert_equal 1, array.count
20
- array << {"test" => "ok"}
21
- assert_equal 2, array.count
16
+ test "decode json into hash" do
17
+ @array << {"test" => "ok"}
18
+ assert_equal({"test" => "ok"}, @array.first.details)
22
19
  end
23
20
 
24
- def test_first_returns_fsrst_job
25
- clean_database
26
- array = QC::DurableArray.new(:database => "queue_classic_test")
21
+ test "count returns number of rows" do
22
+ @array << {"test" => "ok"}
23
+ assert_equal 1, @array.count
24
+ @array << {"test" => "ok"}
25
+ assert_equal 2, @array.count
26
+ end
27
27
 
28
+ test "first returns first job" do
28
29
  job = {"job" => "one"}
29
- array << job
30
- assert_equal job, array.first.details
30
+ @array << job
31
+ assert_equal job, @array.first.details
31
32
  end
32
33
 
33
- def test_first_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.first.details)
34
+ test "first returns first job when many are in the array" do
35
+ @array << {"job" => "one"}
36
+ @array << {"job" => "two"}
37
+ assert_equal({"job" => "one"}, @array.first.details)
40
38
  end
41
39
 
42
- def test_delete_removes_job_from_array
43
- clean_database
44
- array = QC::DurableArray.new(:database => "queue_classic_test")
40
+ test "find_many returns empty array when nothing is found" do
41
+ assert_equal([], @array.find_many {"select * from queue_classic_jobs"})
42
+ end
45
43
 
46
- array << {"job" => "one"}
47
- job = array.first
44
+ test "delete removes job from the array" do
45
+ @array << {"job" => "one"}
46
+ job = @array.first
48
47
 
49
48
  assert_equal( {"job" => "one"}, job.details)
50
49
 
51
- assert_equal(1,array.count)
52
- array.delete(job)
53
- assert_equal(0,array.count)
50
+ assert_equal(1,@array.count)
51
+ @array.delete(job)
52
+ assert_equal(0,@array.count)
54
53
  end
55
54
 
56
- def test_delete_returns_job_after_delete
57
- clean_database
58
- array = QC::DurableArray.new(:database => "queue_classic_test")
59
-
60
- array << {"job" => "one"}
61
- job = array.first
55
+ test "delete returns job after delete" do
56
+ @array << {"job" => "one"}
57
+ job = @array.first
62
58
 
63
59
  assert_equal({"job" => "one"}, job.details)
64
60
 
65
- res = array.delete(job)
61
+ res = @array.delete(job)
66
62
  assert_equal({"job" => "one"}, res.details)
67
63
  end
68
64
 
69
- def test_each_yields_the_details_for_each_job
70
- clean_database
71
- array = QC::DurableArray.new(:database => "queue_classic_test")
72
-
73
- array << {"job" => "one"}
74
- array << {"job" => "two"}
65
+ test "each yields the details for each job" do
66
+ @array << {"job" => "one"}
67
+ @array << {"job" => "two"}
75
68
  results = []
76
- array.each {|v| results << v.details}
69
+ @array.each {|v| results << v.details}
77
70
  assert_equal([{"job" => "one"},{"job" => "two"}], results)
78
71
  end
79
72
 
80
- def test_connection_builds_db_connection_for_uri
81
- array = QC::DurableArray.new(:database => "postgres://ryandotsmith:@localhost/queue_classic_test")
82
- assert_equal "ryandotsmith", array.connection.user
83
- assert_equal "localhost", array.connection.host
84
- assert_equal "queue_classic_test", array.connection.db
73
+ test "connection build db connection from uri" do
74
+ a = QC::Database.new("postgres://ryandotsmith:@localhost/queue_classic_test")
75
+ assert_equal "ryandotsmith", a.connection.user
76
+ assert_equal "localhost", a.connection.host
77
+ assert_equal "queue_classic_test", a.connection.db
78
+ a.disconnect
85
79
  end
86
80
 
87
- def test_connection_builds_db_connection_for_database
88
- array = QC::DurableArray.new(:database => "queue_classic_test")
89
-
90
- # FIXME not everyone will have a postgres user named: ryandotsmith
91
- assert_equal "ryandotsmith", array.connection.user
92
- assert_equal "queue_classic_test", array.connection.db
81
+ test "seach" do
82
+ @array << {"job" => "A.signature"}
83
+ jobs = @array.search_details_column("A.signature")
84
+ assert_equal "A.signature", jobs.first.signature
93
85
  end
94
86
 
87
+ test "seach when data will not match" do
88
+ @array << {"job" => "A.signature"}
89
+ jobs = @array.search_details_column("B.signature")
90
+ assert_equal [], jobs
91
+ end
95
92
  end
@@ -1,22 +1,12 @@
1
1
  $: << File.expand_path("lib")
2
2
  $: << File.expand_path("test")
3
3
 
4
- ENV["DATABASE_URL"] = 'queue_classic_test'
5
-
6
4
  require 'queue_classic'
7
5
  require 'database_helpers'
8
6
 
9
7
  require 'minitest/unit'
10
8
  MiniTest::Unit.autorun
11
9
 
12
- def set_data_store(store=nil)
13
- QC::Queue.instance.setup(
14
- :data_store => (
15
- store || QC::DurableArray.new(:database => ENV["DATABASE_URL"])
16
- )
17
- )
18
- end
19
-
20
10
  def context(*args, &block)
21
11
  return super unless (name = args.first) && block
22
12
  klass = Class.new(MiniTest::Unit::TestCase) do
@@ -1,6 +1,6 @@
1
1
  require File.expand_path("../helper.rb", __FILE__)
2
2
 
3
- context "QC::Job" do
3
+ context "Job" do
4
4
 
5
5
  test "initialize takes details as JSON" do
6
6
  job = QC::Job.new(
@@ -1,33 +1,60 @@
1
1
  require File.expand_path("../helper.rb", __FILE__)
2
2
 
3
- class QueueTest < MiniTest::Unit::TestCase
4
- include DatabaseHelpers
3
+ context "Queue" do
5
4
 
6
- def test_queue_is_singleton
7
- assert_equal QC::Queue, QC::Queue.instance.class
5
+ setup { init_db }
6
+
7
+ test "Queue class responds to enqueue" do
8
+ QC::Queue.enqueue("Klass.method")
9
+ end
10
+
11
+ test "Queue class has a default table name" do
12
+ QC::Queue.enqueue("Klass.method")
13
+ end
14
+
15
+ test "Queue class responds to dequeue" do
16
+ QC::Queue.enqueue("Klass.method")
17
+ assert_equal "Klass.method", QC::Queue.dequeue.signature
8
18
  end
9
19
 
10
- def test_queue_setup
11
- QC::Queue.instance.setup :data_store => []
12
- assert_equal [], QC::Queue.instance.instance_variable_get(:@data)
20
+ test "Queue class responds to delete" do
21
+ QC::Queue.enqueue("Klass.method")
22
+ job = QC::Queue.dequeue
23
+ QC::Queue.delete(job)
13
24
  end
14
25
 
15
- def test_queue_length
16
- QC::Queue.instance.setup :data_store => []
17
- QC::Queue.instance.enqueue "job","params"
26
+ test "Queue class responds to delete_all" do
27
+ 2.times { QC::Queue.enqueue("Klass.method") }
28
+ job1,job2 = QC::Queue.dequeue, QC::Queue.dequeue
29
+ QC::Queue.delete_all
30
+ end
18
31
 
19
- assert_equal 1, QC::Queue.instance.length
32
+ test "Queue class return the length of the queue" do
33
+ 2.times { QC::Queue.enqueue("Klass.method") }
34
+ assert_equal 2, QC::Queue.length
20
35
  end
21
36
 
22
- def test_queue_delete_all
23
- QC::Queue.instance.setup :data_store => []
37
+ test "Queue class finds jobs using query method" do
38
+ QC::Queue.enqueue("Something.hard_to_find")
39
+ jobs = QC::Queue.query("Something.hard_to_find")
40
+ assert_equal 1, jobs.length
41
+ assert_equal "Something.hard_to_find", jobs.first.signature
42
+ end
24
43
 
25
- QC::Queue.instance.enqueue "job","params"
26
- QC::Queue.instance.enqueue "job","params"
44
+ test "queue instance responds to enqueue" do
45
+ QC::Queue.enqueue("Something.hard_to_find")
46
+ init_db(:custom_queue_name)
47
+ @queue = QC::Queue.new(:custom_queue_name)
48
+ @queue.enqueue "Klass.method"
49
+ @queue.disconnect
50
+ end
27
51
 
28
- assert_equal 2, QC::Queue.instance.length
29
- QC::Queue.instance.delete_all
30
- assert_equal 0, QC::Queue.instance.length
52
+ test "queue only uses 1 connection per class" do
53
+ QC::Queue.length
54
+ QC::Queue.enqueue "Klass.method"
55
+ QC::Queue.delete QC::Queue.dequeue
56
+ QC::Queue.dequeue
57
+ assert_equal 1, QC.connection_status[:total]
31
58
  end
32
59
 
33
60
  end
@@ -6,7 +6,7 @@ class TestNotifier
6
6
  end
7
7
 
8
8
  # This not only allows me to test what happens
9
- # when a failure occures but it also demonstrates
9
+ # when a failure occurs but it also demonstrates
10
10
  # how to override the worker to handle failures the way
11
11
  # you want.
12
12
  class TestWorker < QC::Worker
@@ -20,31 +20,26 @@ class TestWorker < QC::Worker
20
20
  end
21
21
  end
22
22
 
23
- class WorkerTest < MiniTest::Unit::TestCase
24
- include DatabaseHelpers
23
+ context "Worker" do
25
24
 
26
- def test_working_a_job
27
- set_data_store
28
- clean_database
25
+ setup do
26
+ init_db
27
+ @worker = TestWorker.new
28
+ end
29
29
 
30
- QC.enqueue "TestNotifier.deliver", {}
31
- worker = TestWorker.new
30
+ test "working a job" do
31
+ QC::Queue.enqueue "TestNotifier.deliver", {}
32
32
 
33
- assert_equal(1, QC.queue_length)
34
- worker.work
35
- assert_equal(0, QC.queue_length)
36
- assert_equal(0, worker.failed_count)
33
+ assert_equal(1, QC::Queue.length)
34
+ @worker.work
35
+ assert_equal(0, QC::Queue.length)
36
+ assert_equal(0, @worker.failed_count)
37
37
  end
38
38
 
39
- def test_rescue_failed_jobs
40
- set_data_store
41
- clean_database
42
-
43
- QC.enqueue "TestNotifier.no_method", {}
44
- worker = TestWorker.new
39
+ test "resuce failed job" do
40
+ QC::Queue.enqueue "TestNotifier.no_method", {}
45
41
 
46
- worker.work
47
- assert_equal 1, worker.failed_count
42
+ @worker.work
43
+ assert_equal 1, @worker.failed_count
48
44
  end
49
-
50
45
  end
metadata CHANGED
@@ -1,12 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: queue_classic
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 2
8
- - 2
9
- version: 0.2.2
4
+ prerelease:
5
+ version: 0.3.1
10
6
  platform: ruby
11
7
  authors:
12
8
  - Ryan Smith
@@ -14,7 +10,7 @@ autorequire:
14
10
  bindir: bin
15
11
  cert_chain: []
16
12
 
17
- date: 2011-02-16 00:00:00 -08:00
13
+ date: 2011-04-26 00:00:00 -07:00
18
14
  default_executable:
19
15
  dependencies:
20
16
  - !ruby/object:Gem::Dependency
@@ -25,10 +21,6 @@ dependencies:
25
21
  requirements:
26
22
  - - ">="
27
23
  - !ruby/object:Gem::Version
28
- segments:
29
- - 0
30
- - 10
31
- - 1
32
24
  version: 0.10.1
33
25
  type: :runtime
34
26
  version_requirements: *id001
@@ -40,12 +32,10 @@ dependencies:
40
32
  requirements:
41
33
  - - ">="
42
34
  - !ruby/object:Gem::Version
43
- segments:
44
- - 0
45
35
  version: "0"
46
36
  type: :runtime
47
37
  version_requirements: *id002
48
- 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.
38
+ description: Queue Classic (beta) is a 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.
49
39
  email: ryan@heroku.com
50
40
  executables: []
51
41
 
@@ -54,15 +44,14 @@ extensions: []
54
44
  extra_rdoc_files: []
55
45
 
56
46
  files:
57
- - readme.markdown
58
- - lib/queue_classic/api.rb
47
+ - readme.md
48
+ - lib/queue_classic/database.rb
59
49
  - lib/queue_classic/durable_array.rb
60
50
  - lib/queue_classic/job.rb
61
51
  - lib/queue_classic/queue.rb
62
52
  - lib/queue_classic/tasks.rb
63
53
  - lib/queue_classic/worker.rb
64
54
  - lib/queue_classic.rb
65
- - test/api_test.rb
66
55
  - test/database_helpers.rb
67
56
  - test/durable_array_test.rb
68
57
  - test/helper.rb
@@ -70,7 +59,7 @@ files:
70
59
  - test/queue_test.rb
71
60
  - test/worker_test.rb
72
61
  has_rdoc: true
73
- homepage: http://github.com/ryandotsmith/Queue-Classic
62
+ homepage: http://github.com/ryandotsmith/queue_classic
74
63
  licenses: []
75
64
 
76
65
  post_install_message:
@@ -83,26 +72,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
72
  requirements:
84
73
  - - ">="
85
74
  - !ruby/object:Gem::Version
86
- segments:
87
- - 0
88
75
  version: "0"
89
76
  required_rubygems_version: !ruby/object:Gem::Requirement
90
77
  none: false
91
78
  requirements:
92
79
  - - ">="
93
80
  - !ruby/object:Gem::Version
94
- segments:
95
- - 0
96
81
  version: "0"
97
82
  requirements: []
98
83
 
99
84
  rubyforge_project:
100
- rubygems_version: 1.3.7
85
+ rubygems_version: 1.6.2
101
86
  signing_key:
102
87
  specification_version: 3
103
- 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)
88
+ summary: Queue Classic (beta) is a 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)
104
89
  test_files:
105
- - test/api_test.rb
106
90
  - test/durable_array_test.rb
107
91
  - test/job_test.rb
108
92
  - test/queue_test.rb
@@ -1,50 +0,0 @@
1
- module QC
2
- module Api
3
-
4
- def queue
5
- @queue ||= Queue.instance
6
- end
7
-
8
- def enqueue(job,*params)
9
- if job.respond_to?(:details) and job.respond_to?(:params)
10
- p = *job.params
11
- queue.enqueue(job.signature, p)
12
- else
13
- queue.enqueue(job,params)
14
- end
15
- end
16
-
17
- def dequeue
18
- queue.dequeue
19
- end
20
-
21
- def delete(job)
22
- queue.delete(job)
23
- end
24
-
25
- def delete_all
26
- queue.delete_all
27
- end
28
-
29
- def queue_length
30
- queue.length
31
- end
32
-
33
- def work(job)
34
- klass = job.klass
35
- method = job.method
36
- params = job.params
37
-
38
- if params.class == Array
39
- klass.send(method,*params)
40
- else
41
- klass.send(method,params)
42
- end
43
- end
44
-
45
- def logging_enabled?
46
- ENV["LOGGING"]
47
- end
48
-
49
- end
50
- end
@@ -1,218 +0,0 @@
1
- # Queue Classic
2
- __Beta 0.2.2__
3
-
4
- __Queue Classic 0.2.2 is in Beta.__ I have been using this library with 30-50 Heroku workers and have had great results.
5
-
6
- I am using this in production applications and plan to maintain and support this library for a long time.
7
-
8
- Queue Classic is an alternative queueing library for Ruby apps (Rails, Sinatra, Etc...) Queue Classic features a blocking dequeue, database maintained locks and
9
- no ridiculous dependencies. As a matter of fact, Queue Classic only requires the __pg__ and __json__.
10
-
11
- ## Installation
12
-
13
- ### TL;DR
14
- 1. gem install queue_classic
15
- 2. add jobs table to your database
16
- 3. QC.enqueue "Class.method", :arg1 => val1
17
- 4. rake qc:work
18
-
19
- ### Dependencies
20
-
21
- Postgres version 9
22
-
23
- Ruby (gems: pg, json)
24
-
25
- ### Gem
26
-
27
- gem install queue_classic
28
-
29
- ### Database
30
-
31
- 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:
32
- echo $DATABASE_URL
33
- in your shell. If you are using Heroku, this var is set and pointing to your primary database.
34
-
35
- 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:
36
-
37
- class CreateJobsTable < ActiveRecord::Migration
38
- def self.up
39
- create_table :jobs do |t|
40
- t.text :details
41
- t.timestamp :locked_at
42
- t.index :id
43
- end
44
- end
45
-
46
- def self.down
47
- drop_table :jobs
48
- end
49
- end
50
- 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.
51
-
52
- Be sure and add the index to the id column. This will help out the worker if the queue should ever reach an obscene length. It made a huge difference
53
- when running the benchmark.
54
-
55
- __script/console__
56
- QC.enqueue "Class.method"
57
- __Terminal__
58
- psql you_database_name
59
- select * from jobs;
60
- You should see the job "Class.method"
61
-
62
- ### Rakefile
63
-
64
- As a convenience, I added a rake task that responds to `rake jobs:work` There are also rake tasks in the `qc` name space.
65
- To get access to these tasks, Add `require 'queue_classic/tasks'` to your Rakefile.
66
-
67
- ## Fundamentals
68
-
69
- ### Enqueue
70
-
71
- To place a job onto the queue, you should specify a class and a class method. There are a few ways to enqueue:
72
-
73
- QC.enqueue('Class.method', :arg1 => 'value1', :arg2 => 'value2')
74
-
75
- Requires:
76
-
77
- class Class
78
- def self.method(args)
79
- puts args["arg1"]
80
- end
81
- end
82
-
83
- QC.enqueue('Class.method', 'value1', 'value2')
84
-
85
- Requires:
86
-
87
- class Class
88
- def self.method(arg1,arg2)
89
- puts arg1
90
- puts arg2
91
- end
92
- end
93
-
94
-
95
- The job gets stored in the jobs table with a details field set to: `{ job: Class.method, params: {arg1: value1, arg2: value2}}` (as JSON)
96
- Here is a more concrete example of a job implementation using a Rails ActiveRecord Model:
97
-
98
- class Invoice < ActiveRecord::Base
99
- def self.process(invoice_id)
100
- invoice = find(invoice_id)
101
- invoice.process!
102
- end
103
-
104
- def self.process_all
105
- Invoice.all do |invoice|
106
- QC.enqueue "Invoice.process", invoice.id
107
- end
108
- end
109
- end
110
-
111
-
112
- ### Dequeue
113
-
114
- Traditionally, a queue's dequeue operation will remove the item from the queue. However, Queue Classic will not delete the item from the queue right away; instead, the workers will lock
115
- the job 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
116
- database backed queing libraries, Queue Classic uses the database time to lock. This allows you to be more relaxed about the time synchronization amongst your worker machines.
117
-
118
- Queue Classic takes advantage of Postgres' PUB/SUB featuers to dequeue a job. Basically there is a channel in which the workers LISTEN. When a new job is added to the queue, the queue sends NOTIFY
119
- 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. This eliminates
120
- the need to Sleep & Select.
121
-
122
- ### The Worker
123
-
124
- The worker calls dequeue and then calls the enqueued method with the supplied arguments. Once the method terminates, the job is deleted from the queue. In the case that your method
125
- does not terminate, or the worker unexpectingly dies, Queue Classic will do following:
126
-
127
- * Rescue the Exception %
128
- * Call handle_failure(job,exception)
129
- * Delete the job
130
-
131
- % - To my knowledge, the only thing that can usurp ensure is a segfault.
132
-
133
- By default, handle_failure will puts the job and the exception. This is not very good and you should override this method. It is simple to do so.
134
- If you are using Queue Classic with Rails, You should:
135
-
136
- 1. Remove require 'queue_classic/tasks' from Rakefile
137
- 2. Create new file in lib/tasks. Call it queue_classic.rb (name is arbitrary)
138
- 3. Insert something like the following:
139
-
140
- require 'queue_classic'
141
-
142
- class MyWorker < QC::Worker
143
- def handle_failure(job,exception)
144
- # You can do many things inside of this method. Here are a few examples:
145
-
146
- # Log to Exceptional
147
- Exceptional.handle(exception, "Background Job Failed" + job.inspect)
148
-
149
- # Log to Hoptoad
150
- HoptoadNotifier.notify(
151
- :error_class => "Background Job",
152
- :error_message => "Special Error: #{e.message}",
153
- :parameters => job.details
154
- )
155
-
156
- # Log to STDOUT (Heroku Logplex listens to stdout)
157
- puts job.inspect
158
- puts exception.inspect
159
- puts exception.backtrace
160
-
161
- # Retry the job
162
- QC.enqueue(job)
163
-
164
- end
165
- end
166
-
167
- namespace :jobs do
168
- task :work => :environment do
169
- MyWorker.new.start
170
- end
171
- end
172
-
173
- ## Performance
174
- I am pleased at the performance of Queue Classic. It ran 3x faster than the DJ. (I have yet to benchmark redis backed queues)
175
-
176
- ruby benchmark.rb
177
- user system total real
178
- 0.950000 0.620000 1.570000 ( 9.479941)
179
-
180
- Hardware: Mac Book Pro 2.8 GHz Intel Core i7. SSD. 4 GB memory.
181
-
182
- Software: Ruby 1.9.2-p0, PostgreSQL 9.0.2
183
-
184
- It is fast because:
185
-
186
- * I wrote my own SQL
187
- * I do not create many Ruby Objects
188
- * I do not call very many methods
189
-
190
- ## FAQ
191
-
192
- How is this different than DJ?
193
- > 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).
194
-
195
- > __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
196
- quite simple.
197
-
198
- > __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
199
- the status of a job. Classic Queue locks a job using Postgres' TIMESTAMP function.
200
-
201
- > __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"
202
- 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
203
- 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.
204
-
205
- > __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.
206
-
207
- Why doesn't your queue retry failed jobs?
208
- > I believe the Class method should handle any sort of exception. Also, I think
209
- that the model you are working on should know about it's state. For instance, if you are
210
- creating jobs for the emailing of newsletters; put a emailed_at column on your newsletter model
211
- and then right before the job quits, touch the emailed_at column. That being said, you can do whatever you
212
- want in handle_failure. I will not decide what is best for your application.
213
-
214
- Can I use this library with 50 Heroku Workers?
215
- > Yes.
216
-
217
- Is Queue Classic ready for production? Can I do it live?!?
218
- > I started this project on 1/24/2011. I have been using this in production for some high-traffic apps at Heroku since 2/24/2011.
@@ -1,40 +0,0 @@
1
- require File.expand_path("../helper.rb", __FILE__)
2
-
3
- context "QC::Api" do
4
- setup { clean_database }
5
-
6
- test "enqueue takes a job without params" do
7
- QC.enqueue "Notifier.send"
8
-
9
- job = QC.dequeue
10
- assert_equal({"job" => "Notifier.send", "params" => [] }, job.details)
11
- end
12
-
13
- test "enqueue takes a hash" do
14
- QC.enqueue "Notifier.send", {:arg => 1}
15
-
16
- job = QC.dequeue
17
- assert_equal({"job" => "Notifier.send", "params" => [{"arg" => 1}] }, job.details)
18
- end
19
-
20
- test "enqueue takes a job" do
21
- h = {"id" => 1, "details" => {"job" => 'Notifier.send'}.to_json, "locked_at" => nil}
22
- job = QC::Job.new(h)
23
- QC.enqueue(job)
24
-
25
- job = QC.dequeue
26
- assert_equal({"job" => "Notifier.send", "params" => []}, job.details)
27
- end
28
-
29
- test "enqueue takes a job and maintain params" do
30
- h = {"id" => 1, "details" => {"job" => 'Notifier.send', "params" => ["1"]}.to_json, "locked_at" => nil}
31
- job = QC::Job.new(h)
32
- QC.enqueue(job)
33
-
34
- job = QC.dequeue
35
- assert_equal({"job" => "Notifier.send", "params" => ["1"]}, job.details)
36
- end
37
-
38
-
39
- end
40
-