que 0.0.1 → 0.1.0

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.
data/que.gemspec CHANGED
@@ -5,12 +5,12 @@ require 'que/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'que'
8
- spec.version = Que::VERSION
8
+ spec.version = Que::Version
9
9
  spec.authors = ["Chris Hanks"]
10
10
  spec.email = ['christopher.m.hanks@gmail.com']
11
- spec.description = %q{Durable job queueing with PostgreSQL.}
12
- spec.summary = %q{Durable, efficient job queueing with PostgreSQL.}
13
- spec.homepage = ''
11
+ spec.description = %q{A job queue that uses PostgreSQL's advisory locks for speed and reliability.}
12
+ spec.summary = %q{A PostgreSQL-based Job Queue}
13
+ spec.homepage = 'https://github.com/chanks/que'
14
14
  spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
@@ -23,6 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency 'rspec', '~> 2.14.1'
24
24
  spec.add_development_dependency 'pry'
25
25
 
26
- spec.add_dependency 'sequel'
27
- spec.add_dependency 'pg'
26
+ spec.add_development_dependency 'sequel'
27
+ spec.add_development_dependency 'activerecord'
28
+ spec.add_development_dependency 'pg'
29
+ spec.add_development_dependency 'connection_pool'
28
30
  end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+ require 'active_record'
3
+
4
+ ActiveRecord::Base.establish_connection(QUE_URL)
5
+ Que.connection = ActiveRecord
6
+ QUE_ADAPTERS[:active_record] = Que.adapter
7
+
8
+ describe "Que using the ActiveRecord adapter" do
9
+ before { Que.adapter = QUE_ADAPTERS[:active_record] }
10
+
11
+ it_behaves_like "a Que adapter"
12
+ it_behaves_like "a multithreaded Que adapter"
13
+
14
+ it "should use the same connection that ActiveRecord does" do
15
+ class ActiveRecordJob < Que::Job
16
+ def run
17
+ $pid1 = Que.execute("SELECT pg_backend_pid()").first['pg_backend_pid'].to_i
18
+ $pid2 = ActiveRecord::Base.connection.select_all("select pg_backend_pid()").rows.first.first.to_i
19
+ end
20
+ end
21
+
22
+ ActiveRecordJob.queue
23
+ Que::Job.work
24
+
25
+ $pid1.should == $pid2
26
+ end
27
+
28
+ it "should instantiate args as ActiveSupport::HashWithIndifferentAccess" do
29
+ ArgsJob.queue :param => 2
30
+ Que::Job.work
31
+ $passed_args.first[:param].should == 2
32
+ $passed_args.first.should be_an_instance_of ActiveSupport::HashWithIndifferentAccess
33
+ end
34
+
35
+ it "should support Rails' special extensions for times" do
36
+ Que::Job.queue :run_at => 1.minute.from_now
37
+ DB[:que_jobs].get(:run_at).should be_within(3).of Time.now + 60
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+ require 'connection_pool'
3
+
4
+ Que.connection = ConnectionPool.new &NEW_PG_CONNECTION
5
+ QUE_ADAPTERS[:connection_pool] = Que.adapter
6
+
7
+ describe "Que using the ConnectionPool adapter" do
8
+ before { Que.adapter = QUE_ADAPTERS[:connection_pool] }
9
+
10
+ it_behaves_like "a Que adapter"
11
+ it_behaves_like "a multithreaded Que adapter"
12
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Que using a bare PG connection" do
4
+ it_behaves_like "a Que adapter"
5
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ Que.connection = SEQUEL_ADAPTER_DB = Sequel.connect(QUE_URL)
4
+ QUE_ADAPTERS[:sequel] = Que.adapter
5
+
6
+ describe "Que using the Sequel adapter" do
7
+ before { Que.adapter = QUE_ADAPTERS[:sequel] }
8
+
9
+ it_behaves_like "a Que adapter"
10
+ it_behaves_like "a multithreaded Que adapter"
11
+
12
+ it "should use the same connection that Sequel does" do
13
+ class SequelJob < Que::Job
14
+ def run
15
+ $pid1 = Que.execute("SELECT pg_backend_pid()").first['pg_backend_pid'].to_i
16
+ $pid2 = SEQUEL_ADAPTER_DB.get{pg_backend_pid{}}
17
+ end
18
+ end
19
+
20
+ SequelJob.queue
21
+ Que::Job.work
22
+
23
+ $pid1.should == $pid2
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe Que do
4
+ it ".connection= with an unsupported connection should raise an error" do
5
+ proc{Que.connection = "ferret"}.should raise_error RuntimeError, /Que connection not recognized: "ferret"/
6
+ end
7
+
8
+ it ".adapter when no connection has been established should raise an error" do
9
+ Que.connection = nil
10
+ proc{Que.adapter}.should raise_error RuntimeError, /Que connection not established!/
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Que, 'helpers' do
4
+ it "should be able to drop and create the jobs table" do
5
+ DB.table_exists?(:que_jobs).should be true
6
+ Que.drop!
7
+ DB.table_exists?(:que_jobs).should be false
8
+ Que.execute "SET client_min_messages TO 'warning'" # Avoid annoying NOTICE messages.
9
+ Que.create!
10
+ DB.table_exists?(:que_jobs).should be true
11
+ end
12
+
13
+ it "should be able to clear the jobs table" do
14
+ DB[:que_jobs].insert :job_class => "Que::Job"
15
+ DB[:que_jobs].count.should be 1
16
+ Que.clear!
17
+ DB[:que_jobs].count.should be 0
18
+ end
19
+ end
data/spec/pool_spec.rb ADDED
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Managing the Worker pool" do
4
+ it "should log mode changes" do
5
+ Que.mode = :off
6
+ $logger.messages.should == ["[Que] Set mode to :off"]
7
+ end
8
+
9
+ it "Que.mode = :sync should make jobs run in the same thread as they are queued" do
10
+ Que.mode = :sync
11
+
12
+ ArgsJob.queue(5, :testing => "synchronous").should be_an_instance_of ArgsJob
13
+ $passed_args.should == [5, {'testing' => "synchronous"}]
14
+ DB[:que_jobs].count.should be 0
15
+
16
+ $logger.messages.length.should be 2
17
+ $logger.messages[0].should == "[Que] Set mode to :sync"
18
+ $logger.messages[1].should =~ /\A\[Que\] Worked job in/
19
+ end
20
+
21
+ describe "Que.mode = :async" do
22
+ it "should spin up 4 workers" do
23
+ Que.mode = :async
24
+ workers = Que::Worker.workers
25
+ workers.count.should be 4
26
+ sleep_until { workers.all?(&:sleeping?) }
27
+ end
28
+
29
+ it "then Que.worker_count = 2 should gracefully decrease the number of workers" do
30
+ Que.mode = :async
31
+ workers = Que::Worker.workers.dup
32
+ workers.count.should be 4
33
+
34
+ Que.worker_count = 2
35
+ Que::Worker.workers.count.should be 2
36
+ sleep_until { Que::Worker.workers.all?(&:sleeping?) }
37
+
38
+ workers[0..1].should == Que::Worker.workers
39
+ workers[2..3].each do |worker|
40
+ worker.should be_an_instance_of Que::Worker
41
+ worker.thread.status.should == false
42
+ end
43
+ end
44
+
45
+ it "then Que.worker_count = 6 should gracefully increase the number of workers" do
46
+ Que.mode = :async
47
+ workers = Que::Worker.workers.dup
48
+ workers.count.should be 4
49
+
50
+ Que.worker_count = 6
51
+ Que::Worker.workers.count.should be 6
52
+ sleep_until { workers.all?(&:sleeping?) }
53
+
54
+ workers.should == Que::Worker.workers[0..3]
55
+ end
56
+
57
+ it "then Que.mode = :off should gracefully shut down workers" do
58
+ Que.mode = :async
59
+ workers = Que::Worker.workers.dup
60
+ workers.count.should be 4
61
+
62
+ Que.mode = :off
63
+ Que::Worker.workers.length.should be 0
64
+
65
+ workers.count.should be 4
66
+ workers.each { |worker| worker.thread.status.should be false }
67
+ end
68
+
69
+ it "then Que::Worker.wake! should wake up a single worker" do
70
+ Que.mode = :async
71
+ sleep_until { Que::Worker.workers.all? &:sleeping? }
72
+
73
+ BlockJob.queue
74
+ Que::Worker.wake!
75
+
76
+ $q1.pop
77
+ Que::Worker.workers.first.should be_working
78
+ Que::Worker.workers[1..3].each { |w| w.should be_sleeping }
79
+ DB[:que_jobs].count.should be 1
80
+ $q2.push nil
81
+
82
+ sleep_until { Que::Worker.workers.all? &:sleeping? }
83
+ DB[:que_jobs].count.should be 0
84
+ end
85
+
86
+ it "then Que::Worker.wake_all! should wake up all workers" do
87
+ # This spec requires at least four connections.
88
+ Que.adapter = QUE_ADAPTERS[:connection_pool]
89
+
90
+ Que.mode = :async
91
+ sleep_until { Que::Worker.workers.all? &:sleeping? }
92
+
93
+ 4.times { BlockJob.queue }
94
+ Que::Worker.wake_all!
95
+ 4.times { $q1.pop }
96
+
97
+ Que::Worker.workers.each{ |worker| worker.should be_working }
98
+ 4.times { $q2.push nil }
99
+
100
+ sleep_until { Que::Worker.workers.all? &:sleeping? }
101
+ DB[:que_jobs].count.should be 0
102
+ end if QUE_ADAPTERS[:connection_pool]
103
+
104
+ it "should poke a worker every Que.sleep_period seconds" do
105
+ begin
106
+ Que.sleep_period = 0.001 # 1 ms
107
+ Que.mode = :async
108
+ sleep_until { Que::Worker.workers.all? &:sleeping? }
109
+ Que::Job.queue
110
+ sleep_until { DB[:que_jobs].count == 0 }
111
+ ensure
112
+ Que.sleep_period = nil
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ describe Que::Job, '.queue' do
4
+ it "should be able to queue a job" do
5
+ DB[:que_jobs].count.should be 0
6
+ Que::Job.queue
7
+ DB[:que_jobs].count.should be 1
8
+
9
+ job = DB[:que_jobs].first
10
+ job[:priority].should be 1
11
+ job[:run_at].should be_within(3).of Time.now
12
+ job[:job_class].should == "Que::Job"
13
+ JSON.load(job[:args]).should == []
14
+ end
15
+
16
+ it "should be able to queue a job with arguments" do
17
+ DB[:que_jobs].count.should be 0
18
+ Que::Job.queue 1, 'two'
19
+ DB[:que_jobs].count.should be 1
20
+
21
+ job = DB[:que_jobs].first
22
+ job[:priority].should be 1
23
+ job[:run_at].should be_within(3).of Time.now
24
+ job[:job_class].should == "Que::Job"
25
+ JSON.load(job[:args]).should == [1, 'two']
26
+ end
27
+
28
+ it "should be able to queue a job with complex arguments" do
29
+ DB[:que_jobs].count.should be 0
30
+ Que::Job.queue 1, 'two', :string => "string",
31
+ :integer => 5,
32
+ :array => [1, "two", {:three => 3}],
33
+ :hash => {:one => 1, :two => 'two', :three => [3]}
34
+
35
+ DB[:que_jobs].count.should be 1
36
+
37
+ job = DB[:que_jobs].first
38
+ job[:priority].should be 1
39
+ job[:run_at].should be_within(3).of Time.now
40
+ job[:job_class].should == "Que::Job"
41
+ JSON.load(job[:args]).should == [
42
+ 1,
43
+ 'two',
44
+ {
45
+ 'string' => 'string',
46
+ 'integer' => 5,
47
+ 'array' => [1, "two", {"three" => 3}],
48
+ 'hash' => {'one' => 1, 'two' => 'two', 'three' => [3]}
49
+ }
50
+ ]
51
+ end
52
+
53
+ it "should be able to queue a job with a specific time to run" do
54
+ DB[:que_jobs].count.should be 0
55
+ Que::Job.queue 1, :run_at => Time.now + 60
56
+ DB[:que_jobs].count.should be 1
57
+
58
+ job = DB[:que_jobs].first
59
+ job[:priority].should be 1
60
+ job[:run_at].should be_within(3).of Time.now + 60
61
+ job[:job_class].should == "Que::Job"
62
+ JSON.load(job[:args]).should == [1]
63
+ end
64
+
65
+ it "should be able to queue a job with a specific priority" do
66
+ DB[:que_jobs].count.should be 0
67
+ Que::Job.queue 1, :priority => 4
68
+ DB[:que_jobs].count.should be 1
69
+
70
+ job = DB[:que_jobs].first
71
+ job[:priority].should be 4
72
+ job[:run_at].should be_within(3).of Time.now
73
+ job[:job_class].should == "Que::Job"
74
+ JSON.load(job[:args]).should == [1]
75
+ end
76
+
77
+ it "should be able to queue a job with queueing options in addition to argument options" do
78
+ DB[:que_jobs].count.should be 0
79
+ Que::Job.queue 1, :string => "string", :run_at => Time.now + 60, :priority => 4
80
+ DB[:que_jobs].count.should be 1
81
+
82
+ job = DB[:que_jobs].first
83
+ job[:priority].should be 4
84
+ job[:run_at].should be_within(3).of Time.now + 60
85
+ job[:job_class].should == "Que::Job"
86
+ JSON.load(job[:args]).should == [1, {'string' => 'string'}]
87
+ end
88
+
89
+ it "should respect a default (but overridable) priority for the job class" do
90
+ class DefaultPriorityJob < Que::Job
91
+ @default_priority = 3
92
+ end
93
+
94
+ DB[:que_jobs].count.should be 0
95
+ DefaultPriorityJob.queue 1
96
+ DefaultPriorityJob.queue 1, :priority => 4
97
+ DB[:que_jobs].count.should be 2
98
+
99
+ first, second = DB[:que_jobs].order(:job_id).all
100
+
101
+ first[:priority].should be 3
102
+ first[:run_at].should be_within(3).of Time.now
103
+ first[:job_class].should == "DefaultPriorityJob"
104
+ JSON.load(first[:args]).should == [1]
105
+
106
+ second[:priority].should be 4
107
+ second[:run_at].should be_within(3).of Time.now
108
+ second[:job_class].should == "DefaultPriorityJob"
109
+ JSON.load(second[:args]).should == [1]
110
+ end
111
+
112
+ it "should respect a default (but overridable) run_at for the job class" do
113
+ class DefaultRunAtJob < Que::Job
114
+ @default_run_at = -> { Time.now + 60 }
115
+ end
116
+
117
+ DB[:que_jobs].count.should be 0
118
+ DefaultRunAtJob.queue 1
119
+ DefaultRunAtJob.queue 1, :run_at => Time.now + 30
120
+ DB[:que_jobs].count.should be 2
121
+
122
+ first, second = DB[:que_jobs].order(:job_id).all
123
+
124
+ first[:priority].should be 1
125
+ first[:run_at].should be_within(3).of Time.now + 60
126
+ first[:job_class].should == "DefaultRunAtJob"
127
+ JSON.load(first[:args]).should == [1]
128
+
129
+ second[:priority].should be 1
130
+ second[:run_at].should be_within(3).of Time.now + 30
131
+ second[:job_class].should == "DefaultRunAtJob"
132
+ JSON.load(second[:args]).should == [1]
133
+ end
134
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,34 +1,57 @@
1
- require 'sequel'
2
1
  require 'que'
3
2
 
4
- DB = Sequel.connect "postgres://postgres:@localhost/que"
5
-
6
- DB.drop_table? :jobs
7
- DB.run <<-SQL
8
- CREATE TABLE jobs
9
- (
10
- priority integer NOT NULL,
11
- run_at timestamp with time zone NOT NULL DEFAULT now(),
12
- job_id bigserial NOT NULL,
13
- created_at timestamp with time zone NOT NULL DEFAULT now(),
14
- type text NOT NULL,
15
- args json NOT NULL DEFAULT '[]'::json,
16
- data json NOT NULL DEFAULT '{}'::json,
17
- CONSTRAINT jobs_pkey PRIMARY KEY (priority, run_at, job_id),
18
- CONSTRAINT valid_priority CHECK (priority >= 1 AND priority <= 5)
19
- );
20
- SQL
3
+ Dir["./spec/support/**/*.rb"].sort.each &method(:require)
4
+
5
+ QUE_URL = ENV["DATABASE_URL"] || "postgres://postgres:@localhost/que-test"
6
+
7
+
8
+
9
+ # Handy proc to instantiate new PG connections:
10
+ require 'uri'
11
+ require 'pg'
12
+ NEW_PG_CONNECTION = proc do
13
+ uri = URI.parse(QUE_URL)
14
+ PG::Connection.open :host => uri.host,
15
+ :user => uri.user,
16
+ :password => uri.password,
17
+ :port => uri.port || 5432,
18
+ :dbname => uri.path[1..-1]
19
+ end
20
+
21
+
22
+
23
+ # Adapters track information about their connections like which statements
24
+ # have been prepared, and if Que.connection= is called before each spec, we're
25
+ # constantly creating new adapters and losing that information, which is bad.
26
+ # So instead, we hang onto a few adapters and assign them using Que.adapter=
27
+ # as needed. The plain pg adapter is the default.
28
+
29
+ # Also, let Que initialize the adapter itself, to make sure that the
30
+ # recognition logic works. Similar code can be found in the adapter specs.
31
+ Que.connection = NEW_PG_CONNECTION.call
32
+ QUE_ADAPTERS = {:pg => Que.adapter}
33
+
34
+
35
+
36
+ # We use Sequel to introspect the database in specs.
37
+ require 'sequel'
38
+ DB = Sequel.connect(QUE_URL)
39
+ DB.drop_table? :que_jobs
40
+ DB.run Que::SQL[:create_table]
21
41
 
22
42
  RSpec.configure do |config|
23
43
  config.before do
24
- Que::Worker.state = :off
25
- DB[:jobs].delete
44
+ DB[:que_jobs].delete
45
+ Que.adapter = QUE_ADAPTERS[:pg]
46
+ Que.mode = :off
47
+ Que.sleep_period = nil
48
+ $logger.messages.clear
26
49
  end
27
50
  end
28
51
 
29
- Que::Worker.state = :async # Boot up.
30
52
 
31
- # For use when debugging specs:
32
- # require 'logger'
33
- # Que.logger = Logger.new(STDOUT)
34
- # DB.loggers << Logger.new(STDOUT)
53
+
54
+ # Set up a dummy logger.
55
+ Que.logger = $logger = Object.new
56
+ def $logger.messages; @messages ||= []; end
57
+ def $logger.method_missing(m, message); messages << message; end