que 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +36 -51
- data/lib/generators/que/install_generator.rb +1 -1
- data/lib/que.rb +3 -3
- data/lib/que/job.rb +18 -16
- data/lib/que/railtie.rb +8 -4
- data/lib/que/rake_tasks.rb +14 -9
- data/lib/que/sql.rb +21 -55
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +64 -60
- data/spec/adapters/active_record_spec.rb +55 -27
- data/spec/adapters/sequel_spec.rb +30 -0
- data/spec/spec_helper.rb +48 -17
- data/spec/unit/pool_spec.rb +26 -0
- data/spec/unit/worker_spec.rb +21 -2
- data/tasks/benchmark.rb +1 -93
- data/tasks/safe_shutdown.rb +63 -0
- metadata +23 -23
- data/tasks/benchmark_queues.rb +0 -398
data/lib/que/version.rb
CHANGED
data/lib/que/worker.rb
CHANGED
@@ -4,74 +4,39 @@ module Que
|
|
4
4
|
class Worker
|
5
5
|
# Each worker has a thread that does the actual work of running jobs.
|
6
6
|
# Since both the worker's thread and whatever thread is managing the
|
7
|
-
# worker are capable of affecting the
|
8
|
-
#
|
9
|
-
|
7
|
+
# worker are capable of affecting the worker's state, we need to
|
8
|
+
# synchronize access to it.
|
10
9
|
include MonitorMixin
|
11
10
|
|
12
|
-
|
11
|
+
# A custom exception to immediately kill a worker and its current job.
|
12
|
+
class Stop < Interrupt; end
|
13
|
+
|
14
|
+
attr_reader :thread, :state
|
13
15
|
|
14
16
|
def initialize
|
15
17
|
super # For MonitorMixin.
|
18
|
+
@state = :working
|
19
|
+
@thread = Thread.new { work_loop }
|
20
|
+
end
|
16
21
|
|
17
|
-
|
18
|
-
|
19
|
-
# temporarily block it.
|
20
|
-
q = Queue.new
|
21
|
-
|
22
|
-
@thread = Thread.new do
|
23
|
-
q.pop
|
24
|
-
|
25
|
-
loop do
|
26
|
-
job = Job.work
|
27
|
-
|
28
|
-
# Grab the lock and figure out what we should do next.
|
29
|
-
synchronize do
|
30
|
-
if @thread[:directive] == :stop
|
31
|
-
@thread[:state] = :stopping
|
32
|
-
elsif not job
|
33
|
-
# No work, go to sleep.
|
34
|
-
@thread[:state] = :sleeping
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
if @thread[:state] == :sleeping
|
39
|
-
sleep
|
40
|
-
|
41
|
-
# Now that we're woken up, grab the lock figure out if we're stopping.
|
42
|
-
synchronize do
|
43
|
-
@thread[:state] = :stopping if @thread[:directive] == :stop
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
break if @thread[:state] == :stopping
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
synchronize do
|
52
|
-
@thread[:directive] = :work
|
53
|
-
@thread[:state] = :working
|
54
|
-
end
|
55
|
-
|
56
|
-
q.push :go!
|
22
|
+
def alive?
|
23
|
+
!!@thread.status
|
57
24
|
end
|
58
25
|
|
59
26
|
def sleeping?
|
60
27
|
synchronize do
|
61
|
-
if @
|
28
|
+
if @state == :sleeping
|
62
29
|
# There's a very small period of time between when the Worker marks
|
63
30
|
# itself as sleeping and when it actually goes to sleep. Only report
|
64
31
|
# true when we're certain the thread is sleeping.
|
65
|
-
|
32
|
+
wait until @thread.status == 'sleep'
|
66
33
|
true
|
67
34
|
end
|
68
35
|
end
|
69
36
|
end
|
70
37
|
|
71
38
|
def working?
|
72
|
-
synchronize
|
73
|
-
@thread[:state] == :working
|
74
|
-
end
|
39
|
+
synchronize { @state == :working }
|
75
40
|
end
|
76
41
|
|
77
42
|
def wake!
|
@@ -79,30 +44,56 @@ module Que
|
|
79
44
|
if sleeping?
|
80
45
|
# Have to set the state here so that another thread checking
|
81
46
|
# immediately after this won't see the worker as asleep.
|
82
|
-
@
|
47
|
+
@state = :working
|
83
48
|
@thread.wakeup
|
84
49
|
true
|
85
50
|
end
|
86
51
|
end
|
87
52
|
end
|
88
53
|
|
89
|
-
#
|
54
|
+
# #stop informs the worker that it should shut down after its next job,
|
55
|
+
# while #stop! kills the job and worker immediately. #stop! is bad news
|
56
|
+
# because its results are unpredictable (it can leave the DB connection
|
57
|
+
# in an unusable state), so it should only be used when we're shutting
|
58
|
+
# down the whole process anyway and side effects aren't a big deal.
|
59
|
+
def stop
|
60
|
+
synchronize do
|
61
|
+
@stop = true
|
62
|
+
@thread.wakeup if sleeping?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
90
66
|
def stop!
|
91
|
-
@thread
|
67
|
+
@thread.raise Stop
|
92
68
|
end
|
93
69
|
|
94
70
|
def wait_until_stopped
|
95
|
-
|
96
|
-
case @thread.status
|
97
|
-
when false then break
|
98
|
-
when 'sleep' then @thread.wakeup
|
99
|
-
end
|
100
|
-
sleep 0.0001
|
101
|
-
end
|
71
|
+
wait while alive?
|
102
72
|
end
|
103
73
|
|
104
74
|
private
|
105
75
|
|
76
|
+
# Sleep very briefly while waiting for a thread to get somewhere.
|
77
|
+
def wait
|
78
|
+
sleep 0.0001
|
79
|
+
end
|
80
|
+
|
81
|
+
def work_loop
|
82
|
+
loop do
|
83
|
+
job = Job.work
|
84
|
+
|
85
|
+
# Grab the lock and figure out what we should do next.
|
86
|
+
synchronize { @state = :sleeping unless @stop || job }
|
87
|
+
|
88
|
+
sleep if @state == :sleeping
|
89
|
+
break if @stop
|
90
|
+
end
|
91
|
+
rescue Stop
|
92
|
+
# This process is shutting down; let it.
|
93
|
+
ensure
|
94
|
+
@state = :stopped
|
95
|
+
end
|
96
|
+
|
106
97
|
# Defaults for the Worker pool.
|
107
98
|
@worker_count = 0
|
108
99
|
@sleep_period = 5
|
@@ -131,7 +122,7 @@ module Que
|
|
131
122
|
if count > workers.count
|
132
123
|
(count - workers.count).times { workers << new }
|
133
124
|
elsif count < workers.count
|
134
|
-
workers.pop(workers.count - count).each(&:stop
|
125
|
+
workers.pop(workers.count - count).each(&:stop).each(&:wait_until_stopped)
|
135
126
|
end
|
136
127
|
end
|
137
128
|
|
@@ -140,6 +131,19 @@ module Que
|
|
140
131
|
wrangler.wakeup if period
|
141
132
|
end
|
142
133
|
|
134
|
+
def stop!
|
135
|
+
# The behavior of Worker#stop! is unpredictable - what it does is
|
136
|
+
# dependent on what the Worker is currently doing. Sometimes it won't
|
137
|
+
# work the first time, so we need to try again, but sometimes it'll
|
138
|
+
# never do anything, so we can't repeat indefinitely. So, compromise.
|
139
|
+
5.times do
|
140
|
+
break if workers.select(&:alive?).each(&:stop!).none?
|
141
|
+
sleep 0.001
|
142
|
+
end
|
143
|
+
|
144
|
+
workers.clear
|
145
|
+
end
|
146
|
+
|
143
147
|
def wake!
|
144
148
|
workers.find &:wake!
|
145
149
|
end
|
@@ -1,39 +1,67 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# Don't run these specs in JRuby until jruby-pg is compatible with ActiveRecord.
|
2
|
+
unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
QUE_ADAPTERS[:active_record] = Que.adapter
|
4
|
+
require 'spec_helper'
|
5
|
+
require 'active_record'
|
7
6
|
|
8
|
-
|
9
|
-
|
7
|
+
ActiveRecord::Base.establish_connection(QUE_URL)
|
8
|
+
Que.connection = ActiveRecord
|
9
|
+
QUE_ADAPTERS[:active_record] = Que.adapter
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
describe "Que using the ActiveRecord adapter" do
|
12
|
+
before { Que.adapter = QUE_ADAPTERS[:active_record] }
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
it_behaves_like "a Que adapter"
|
15
|
+
it_behaves_like "a multithreaded Que adapter"
|
16
|
+
|
17
|
+
it "should use the same connection that ActiveRecord does" do
|
18
|
+
class ActiveRecordJob < Que::Job
|
19
|
+
def run
|
20
|
+
$pid1 = Que.execute("SELECT pg_backend_pid()").first['pg_backend_pid'].to_i
|
21
|
+
$pid2 = ActiveRecord::Base.connection.select_all("select pg_backend_pid()").rows.first.first.to_i
|
22
|
+
end
|
19
23
|
end
|
24
|
+
|
25
|
+
ActiveRecordJob.queue
|
26
|
+
Que::Job.work
|
27
|
+
|
28
|
+
$pid1.should == $pid2
|
20
29
|
end
|
21
30
|
|
22
|
-
|
23
|
-
|
31
|
+
it "should instantiate args as ActiveSupport::HashWithIndifferentAccess" do
|
32
|
+
ArgsJob.queue :param => 2
|
33
|
+
Que::Job.work
|
34
|
+
$passed_args.first[:param].should == 2
|
35
|
+
$passed_args.first.should be_an_instance_of ActiveSupport::HashWithIndifferentAccess
|
36
|
+
end
|
24
37
|
|
25
|
-
|
26
|
-
|
38
|
+
it "should support Rails' special extensions for times" do
|
39
|
+
Que::Job.queue :run_at => 1.minute.from_now
|
40
|
+
DB[:que_jobs].get(:run_at).should be_within(3).of Time.now + 60
|
41
|
+
end
|
27
42
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
43
|
+
it "should safely roll back in-process transactions when using Que.stop!" do
|
44
|
+
begin
|
45
|
+
class ARInterruptJob < BlockJob
|
46
|
+
def run
|
47
|
+
ActiveRecord::Base.transaction do
|
48
|
+
Que.execute "INSERT INTO que_jobs (job_id, job_class) VALUES (0, 'Que::Job')"
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
ARInterruptJob.queue
|
55
|
+
Que.mode = :async
|
56
|
+
$q1.pop
|
57
|
+
Que.stop!
|
34
58
|
|
35
|
-
|
36
|
-
|
37
|
-
|
59
|
+
DB[:que_jobs].where(:job_id => 0).should be_empty
|
60
|
+
ensure
|
61
|
+
# Que.stop! can affect DB connections in an unpredictable fashion, so
|
62
|
+
# force a reconnection for the sake of the other specs.
|
63
|
+
ActiveRecord::Base.establish_connection(QUE_URL)
|
64
|
+
end
|
65
|
+
end
|
38
66
|
end
|
39
67
|
end
|
@@ -22,4 +22,34 @@ describe "Que using the Sequel adapter" do
|
|
22
22
|
|
23
23
|
$pid1.should == $pid2
|
24
24
|
end
|
25
|
+
|
26
|
+
it "should safely roll back in-process transactions when using Que.stop!" do
|
27
|
+
begin
|
28
|
+
class SequelInterruptJob < BlockJob
|
29
|
+
def run
|
30
|
+
SEQUEL_ADAPTER_DB.transaction do
|
31
|
+
Que.execute "INSERT INTO que_jobs (job_id, job_class) VALUES (0, 'Que::Job')"
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
SequelInterruptJob.queue
|
38
|
+
Que.mode = :async
|
39
|
+
$q1.pop
|
40
|
+
Que.stop!
|
41
|
+
|
42
|
+
DB[:que_jobs].where(:job_id => 0).should be_empty
|
43
|
+
ensure
|
44
|
+
# Que.stop! can affect DB connections in an unpredictable fashion, and
|
45
|
+
# Sequel's built-in reconnection logic may not be able to recover them.
|
46
|
+
# So, force a reconnection for the sake of the other specs...
|
47
|
+
SEQUEL_ADAPTER_DB.disconnect
|
48
|
+
|
49
|
+
# ...and that's not even foolproof, because threads may have died with
|
50
|
+
# connections checked out.
|
51
|
+
SEQUEL_ADAPTER_DB.pool.allocated.each_value(&:close)
|
52
|
+
SEQUEL_ADAPTER_DB.pool.allocated.clear
|
53
|
+
end
|
54
|
+
end
|
25
55
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
require 'que'
|
2
|
+
require 'uri'
|
3
|
+
require 'pg'
|
2
4
|
require 'json'
|
3
5
|
|
4
|
-
Dir[
|
5
|
-
|
6
|
-
QUE_URL = ENV["DATABASE_URL"] || "postgres://postgres:@localhost/que-test"
|
6
|
+
Dir['./spec/support/**/*.rb'].sort.each &method(:require)
|
7
7
|
|
8
8
|
|
9
|
+
# Handy constants for initializing PG connections:
|
10
|
+
QUE_URL = ENV['DATABASE_URL'] || 'postgres://postgres:@localhost/que-test'
|
9
11
|
|
10
|
-
# Handy proc to instantiate new PG connections:
|
11
|
-
require 'uri'
|
12
|
-
require 'pg'
|
13
12
|
NEW_PG_CONNECTION = proc do
|
14
13
|
uri = URI.parse(QUE_URL)
|
15
14
|
PG::Connection.open :host => uri.host,
|
@@ -20,12 +19,11 @@ NEW_PG_CONNECTION = proc do
|
|
20
19
|
end
|
21
20
|
|
22
21
|
|
23
|
-
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# as needed. The plain pg adapter is the default.
|
22
|
+
# Adapters track which statements have been prepared for their connections,
|
23
|
+
# and if Que.connection= is called before each spec, we're constantly creating
|
24
|
+
# new adapters and losing that information, which is bad. So instead, we hang
|
25
|
+
# onto a few adapters and assign them using Que.adapter= as needed. The plain
|
26
|
+
# pg adapter is the default.
|
29
27
|
|
30
28
|
# Also, let Que initialize the adapter itself, to make sure that the
|
31
29
|
# recognition logic works. Similar code can be found in the adapter specs.
|
@@ -33,13 +31,26 @@ Que.connection = NEW_PG_CONNECTION.call
|
|
33
31
|
QUE_ADAPTERS = {:pg => Que.adapter}
|
34
32
|
|
35
33
|
|
36
|
-
|
37
34
|
# We use Sequel to introspect the database in specs.
|
38
35
|
require 'sequel'
|
39
36
|
DB = Sequel.connect(QUE_URL)
|
40
37
|
DB.drop_table? :que_jobs
|
41
38
|
DB.run Que::SQL[:create_table]
|
42
39
|
|
40
|
+
|
41
|
+
# Set up a dummy logger.
|
42
|
+
Que.logger = $logger = Object.new
|
43
|
+
|
44
|
+
def $logger.messages
|
45
|
+
@messages ||= []
|
46
|
+
end
|
47
|
+
|
48
|
+
def $logger.method_missing(m, message)
|
49
|
+
messages << message
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# Clean up between specs.
|
43
54
|
RSpec.configure do |config|
|
44
55
|
config.before do
|
45
56
|
DB[:que_jobs].delete
|
@@ -51,8 +62,28 @@ RSpec.configure do |config|
|
|
51
62
|
end
|
52
63
|
|
53
64
|
|
65
|
+
# Optionally log to STDOUT which spec is running at the moment. This is loud,
|
66
|
+
# but helpful in tracking down what spec is hanging, if any.
|
67
|
+
if ENV['LOG_SPEC']
|
68
|
+
require 'logger'
|
69
|
+
logger = Logger.new(STDOUT)
|
54
70
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
71
|
+
description_builder = -> hash do
|
72
|
+
if g = hash[:example_group]
|
73
|
+
"#{description_builder.call(g)} #{hash[:description_args].first}"
|
74
|
+
else
|
75
|
+
hash[:description_args].first
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
RSpec.configure do |config|
|
80
|
+
config.around do |example|
|
81
|
+
data = example.metadata
|
82
|
+
desc = description_builder.call(data)
|
83
|
+
line = "rspec #{data[:file_path]}:#{data[:line_number]}"
|
84
|
+
logger.info "Running spec: #{desc} @ #{line}"
|
85
|
+
|
86
|
+
example.run
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/spec/unit/pool_spec.rb
CHANGED
@@ -6,6 +6,11 @@ describe "Managing the Worker pool" do
|
|
6
6
|
$logger.messages.should == ["[Que] Set mode to :off"]
|
7
7
|
end
|
8
8
|
|
9
|
+
it "Que.stop! should do nothing if there are no workers running" do
|
10
|
+
Que::Worker.workers.should be_empty
|
11
|
+
Que.stop!
|
12
|
+
end
|
13
|
+
|
9
14
|
describe "Que.mode = :sync" do
|
10
15
|
it "should make jobs run in the same thread as they are queued" do
|
11
16
|
Que.mode = :sync
|
@@ -25,6 +30,11 @@ describe "Managing the Worker pool" do
|
|
25
30
|
ArgsJob.queue(5, :testing => "synchronous", :run_at => Time.now + 60)
|
26
31
|
DB[:que_jobs].select_map(:job_class).should == ["ArgsJob"]
|
27
32
|
end
|
33
|
+
|
34
|
+
it "then Que.stop! should do nothing" do
|
35
|
+
Que::Worker.workers.should be_empty
|
36
|
+
Que.stop!
|
37
|
+
end
|
28
38
|
end
|
29
39
|
|
30
40
|
describe "Que.mode = :async" do
|
@@ -122,5 +132,21 @@ describe "Managing the Worker pool" do
|
|
122
132
|
Que.sleep_period = nil
|
123
133
|
end
|
124
134
|
end
|
135
|
+
|
136
|
+
it "then Que.stop! should interrupt all running jobs" do
|
137
|
+
begin
|
138
|
+
# Que.stop! can unpredictably affect connections, which may affect
|
139
|
+
# other tests, so use a new one.
|
140
|
+
pg = NEW_PG_CONNECTION.call
|
141
|
+
Que.connection = pg
|
142
|
+
|
143
|
+
BlockJob.queue
|
144
|
+
Que.mode = :async
|
145
|
+
$q1.pop
|
146
|
+
Que.stop!
|
147
|
+
ensure
|
148
|
+
pg.close if pg
|
149
|
+
end
|
150
|
+
end
|
125
151
|
end
|
126
152
|
end
|