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.
@@ -1,3 +1,3 @@
1
1
  module Que
2
- Version = '0.2.0'
2
+ Version = '0.3.0'
3
3
  end
@@ -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 state of the worker's thread, we
8
- # need to synchronize access to it.
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
- attr_reader :thread
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
- # We have to make sure the thread doesn't actually start the work loop
18
- # until it has a state and directive already set up, so use a queue to
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 @thread[:state] == :sleeping
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
- sleep 0.0001 until @thread.status == 'sleep'
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 do
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
- @thread[:state] = :working
47
+ @state = :working
83
48
  @thread.wakeup
84
49
  true
85
50
  end
86
51
  end
87
52
  end
88
53
 
89
- # This has to be called when trapping a SIGTERM, so it can't lock the monitor.
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[:directive] = :stop
67
+ @thread.raise Stop
92
68
  end
93
69
 
94
70
  def wait_until_stopped
95
- loop do
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!).each(&:wait_until_stopped)
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
- require 'spec_helper'
2
- require 'active_record'
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
- ActiveRecord::Base.establish_connection(QUE_URL)
5
- Que.connection = ActiveRecord
6
- QUE_ADAPTERS[:active_record] = Que.adapter
4
+ require 'spec_helper'
5
+ require 'active_record'
7
6
 
8
- describe "Que using the ActiveRecord adapter" do
9
- before { Que.adapter = QUE_ADAPTERS[:active_record] }
7
+ ActiveRecord::Base.establish_connection(QUE_URL)
8
+ Que.connection = ActiveRecord
9
+ QUE_ADAPTERS[:active_record] = Que.adapter
10
10
 
11
- it_behaves_like "a Que adapter"
12
- it_behaves_like "a multithreaded Que adapter"
11
+ describe "Que using the ActiveRecord adapter" do
12
+ before { Que.adapter = QUE_ADAPTERS[:active_record] }
13
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
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
- ActiveRecordJob.queue
23
- Que::Job.work
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
- $pid1.should == $pid2
26
- end
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
- 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
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
- 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
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
@@ -1,15 +1,14 @@
1
1
  require 'que'
2
+ require 'uri'
3
+ require 'pg'
2
4
  require 'json'
3
5
 
4
- Dir["./spec/support/**/*.rb"].sort.each &method(:require)
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
- # Adapters track information about their connections like which statements
25
- # have been prepared, and if Que.connection= is called before each spec, we're
26
- # constantly creating new adapters and losing that information, which is bad.
27
- # So instead, we hang onto a few adapters and assign them using Que.adapter=
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
- # Set up a dummy logger.
56
- Que.logger = $logger = Object.new
57
- def $logger.messages; @messages ||= []; end
58
- def $logger.method_missing(m, message); messages << message; end
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
@@ -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