que 0.4.0 → 0.5.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.
@@ -2,7 +2,32 @@ module Que
2
2
  module Adapters
3
3
  class ActiveRecord < Base
4
4
  def checkout
5
- ::ActiveRecord::Base.connection_pool.with_connection { |conn| yield conn.raw_connection }
5
+ checkout_activerecord_adapter { |conn| yield conn.raw_connection }
6
+ end
7
+
8
+ def wake_worker_after_commit
9
+ # Works with ActiveRecord 3.2 and 4 (possibly earlier, didn't check)
10
+ if in_transaction?
11
+ checkout_activerecord_adapter { |adapter| adapter.add_transaction_record(CommittedCallback.new) }
12
+ else
13
+ Que.wake!
14
+ end
15
+ end
16
+
17
+ class CommittedCallback
18
+ def has_transactional_callbacks?
19
+ true
20
+ end
21
+
22
+ def committed!
23
+ Que.wake!
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def checkout_activerecord_adapter(&block)
30
+ ::ActiveRecord::Base.connection_pool.with_connection(&block)
6
31
  end
7
32
  end
8
33
  end
@@ -7,7 +7,7 @@ module Que
7
7
 
8
8
  class Base
9
9
  def initialize(thing = nil)
10
- @statement_mutex = Mutex.new
10
+ @prepared_statements = {}
11
11
  end
12
12
 
13
13
  # The only method that adapters really need to implement. Should lock a
@@ -18,7 +18,7 @@ module Que
18
18
  raise NotImplementedError
19
19
  end
20
20
 
21
- # Called after a job is queued in async mode, to prompts a worker to
21
+ # Called after a job is queued in async mode, to prompt a worker to
22
22
  # wake up after the current transaction commits. Not all adapters will
23
23
  # implement this.
24
24
  def wake_worker_after_commit
@@ -31,26 +31,19 @@ module Que
31
31
 
32
32
  def execute_prepared(name, params = [])
33
33
  checkout do |conn|
34
- unless statements_prepared(conn)[name]
34
+ statements = @prepared_statements[conn] ||= {}
35
+
36
+ unless statements[name]
35
37
  conn.prepare("que_#{name}", SQL[name])
36
- statements_prepared(conn)[name] = true
38
+ statements[name] = true
37
39
  end
38
40
 
39
41
  conn.exec_prepared("que_#{name}", params)
40
42
  end
41
43
  end
42
44
 
43
- private
44
-
45
- # Each adapter needs to remember which of its connections have prepared
46
- # which statements. This is a shared data structure, so protect it. We
47
- # assume that the hash of statements for a particular connection is only
48
- # being accessed by the thread that's checked it out, though.
49
- def statements_prepared(conn)
50
- @statement_mutex.synchronize do
51
- @statements_prepared ||= {}
52
- @statements_prepared[conn] ||= {}
53
- end
45
+ def in_transaction?
46
+ checkout { |conn| conn.transaction_status != ::PG::PQTRANS_IDLE }
54
47
  end
55
48
  end
56
49
  end
data/lib/que/job.rb CHANGED
@@ -1,12 +1,10 @@
1
- require 'multi_json'
2
-
3
1
  module Que
4
2
  class Job
5
3
  attr_reader :attrs
6
4
 
7
5
  def initialize(attrs)
8
6
  @attrs = attrs
9
- @attrs[:args] = Que.indifferentiate MultiJson.load(@attrs[:args])
7
+ @attrs[:args] = Que.indifferentiate JSON_MODULE.load(@attrs[:args])
10
8
  end
11
9
 
12
10
  # Subclasses should define their own run methods, but keep an empty one
@@ -15,10 +13,8 @@ module Que
15
13
  end
16
14
 
17
15
  def _run
18
- time = Time.now
19
16
  run *attrs[:args]
20
17
  destroy unless @destroyed
21
- Que.log :info, "Worked job in #{((Time.now - time) * 1000).round(1)} ms: #{inspect}"
22
18
  end
23
19
 
24
20
  private
@@ -37,7 +33,7 @@ module Que
37
33
  args << options if options.any?
38
34
  end
39
35
 
40
- attrs = {:job_class => to_s, :args => MultiJson.dump(args)}
36
+ attrs = {:job_class => to_s, :args => JSON_MODULE.dump(args)}
41
37
 
42
38
  if time = run_at || @default_run_at && @default_run_at.call
43
39
  attrs[:run_at] = time
@@ -57,12 +53,6 @@ module Que
57
53
  end
58
54
 
59
55
  def work
60
- # Job.work is typically called in a loop, where we sleep when there's
61
- # no more work to be done, so its return value should reflect whether
62
- # we should look for another job or not. So, return truthy if we
63
- # worked a job or encountered a typical error while working a job, and
64
- # falsy if we found nothing to do or hit a connection error.
65
-
66
56
  # Since we're taking session-level advisory locks, we have to hold the
67
57
  # same connection throughout the process of getting a job, working it,
68
58
  # deleting it, and removing the lock.
@@ -79,12 +69,13 @@ module Que
79
69
  # Note that there is currently no spec for this behavior, since
80
70
  # I'm not sure how to reliably commit a transaction that deletes
81
71
  # the job in a separate thread between lock_job and check_job.
82
- return true if Que.execute(:check_job, job.values_at(:priority, :run_at, :job_id)).none?
83
-
84
- run_job(job)
72
+ if Que.execute(:check_job, job.values_at(:priority, :run_at, :job_id)).none?
73
+ {:event => :job_race_condition}
74
+ else
75
+ {:event => :job_worked, :job => run_job(job).attrs}
76
+ end
85
77
  else
86
- Que.log :info, "No jobs available..."
87
- nil
78
+ {:event => :job_unavailable}
88
79
  end
89
80
  rescue => error
90
81
  begin
@@ -105,12 +96,7 @@ module Que
105
96
  Que.error_handler.call(error) rescue nil
106
97
  end
107
98
 
108
- # If it's a garden variety error, we can just return true, pick up
109
- # another job, no big deal. If it's a PG::Error, though, assume
110
- # it's a disconnection or something and that we shouldn't just hit
111
- # the database again right away. We could be a lot more
112
- # sophisticated about what errors we delay for, though.
113
- return !error.is_a?(PG::Error)
99
+ return {:event => :job_errored, :error => error, :job => job}
114
100
  ensure
115
101
  # Clear the advisory lock we took when locking the job. Important
116
102
  # to do this so that they don't pile up in the database. Again, if
@@ -0,0 +1,78 @@
1
+ module Que
2
+ module Migrations
3
+ # In order to ship a schema change, add the relevant up and down sql files
4
+ # to the migrations directory, and bump the version both here and in the
5
+ # add_que generator template.
6
+ CURRENT_VERSION = 2
7
+
8
+ class << self
9
+ def migrate!(options = {:version => CURRENT_VERSION})
10
+ transaction do
11
+ version = options[:version]
12
+
13
+ if (current = db_version) == version
14
+ return
15
+ elsif current < version
16
+ direction = 'up'
17
+ steps = ((current + 1)..version).to_a
18
+ elsif current > version
19
+ direction = 'down'
20
+ steps = ((version + 1)..current).to_a.reverse
21
+ end
22
+
23
+ steps.each do |step|
24
+ Que.execute File.read("#{File.dirname(__FILE__)}/migrations/#{step}-#{direction}.sql")
25
+ end
26
+
27
+ set_db_version(version)
28
+ end
29
+ end
30
+
31
+ def db_version
32
+ result = Que.execute <<-SQL
33
+ SELECT relname, description
34
+ FROM pg_class
35
+ LEFT JOIN pg_description ON pg_description.objoid = pg_class.oid
36
+ WHERE relname = 'que_jobs'
37
+ SQL
38
+
39
+ if result.none?
40
+ # No table in the database at all.
41
+ 0
42
+ elsif (d = result.first[:description]).nil?
43
+ # There's a table, it was just created before the migration system existed.
44
+ 1
45
+ else
46
+ d.to_i
47
+ end
48
+ end
49
+
50
+ def set_db_version(version)
51
+ i = version.to_i
52
+ Que.execute "COMMENT ON TABLE que_jobs IS '#{i}'" unless i.zero?
53
+ end
54
+
55
+ def transaction
56
+ Que.adapter.checkout do
57
+ if Que.adapter.in_transaction?
58
+ yield
59
+ else
60
+ begin
61
+ Que.execute "BEGIN"
62
+ yield
63
+ rescue => error
64
+ raise
65
+ ensure
66
+ # Handle a raised error or a killed thread.
67
+ if error || Thread.current.status == 'aborting'
68
+ Que.execute "ROLLBACK"
69
+ else
70
+ Que.execute "COMMIT"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1 @@
1
+ DROP TABLE que_jobs;
@@ -0,0 +1,12 @@
1
+ CREATE TABLE que_jobs
2
+ (
3
+ priority integer NOT NULL DEFAULT 1,
4
+ run_at timestamptz NOT NULL DEFAULT now(),
5
+ job_id bigserial NOT NULL,
6
+ job_class text NOT NULL,
7
+ args json NOT NULL DEFAULT '[]'::json,
8
+ error_count integer NOT NULL DEFAULT 0,
9
+ last_error text,
10
+
11
+ CONSTRAINT que_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
12
+ );
@@ -0,0 +1 @@
1
+ ALTER TABLE que_jobs ALTER COLUMN priority SET DEFAULT 1;
@@ -0,0 +1,3 @@
1
+ -- 1 was a bad default. Starting from there, you couldn't tweak some jobs to
2
+ -- be more important without going into negative priorities, which is weird.
3
+ ALTER TABLE que_jobs ALTER COLUMN priority SET DEFAULT 100;
data/lib/que/railtie.rb CHANGED
@@ -3,7 +3,7 @@ module Que
3
3
  config.que = Que
4
4
 
5
5
  Que.mode = :sync if Rails.env.test?
6
- Que.connection = ::ActiveRecord if defined?(::ActiveRecord)
6
+ Que.connection = ::ActiveRecord if defined? ::ActiveRecord
7
7
 
8
8
  rake_tasks do
9
9
  load 'que/rake_tasks.rb'
@@ -16,8 +16,13 @@ module Que
16
16
  # Only start up the worker pool if running as a server.
17
17
  Que.mode ||= :async if defined? Rails::Server
18
18
 
19
- # When the process exits, safely interrupt any jobs that are still running.
20
- at_exit { Que.stop! }
19
+ at_exit do
20
+ if Que.mode == :async
21
+ $stdout.puts "Finishing Que's current jobs before exiting..."
22
+ Que.mode = :off
23
+ $stdout.puts "Que's jobs finished, exiting..."
24
+ end
25
+ end
21
26
  end
22
27
  end
23
28
  end
@@ -3,19 +3,23 @@ namespace :que do
3
3
  task :work => :environment do
4
4
  require 'logger'
5
5
 
6
- Que.logger = Logger.new(STDOUT)
7
- Que.worker_count = (ENV['WORKER_COUNT'] || 4).to_i
6
+ Que.logger = Logger.new(STDOUT)
7
+ Que.logger.level = Logger.const_get((ENV['QUE_LOG_LEVEL'] || 'INFO').upcase)
8
+ Que.worker_count = (ENV['QUE_WORKER_COUNT'] || 4).to_i
9
+ Que.wake_interval = (ENV['QUE_WAKE_INTERVAL'] || 0.1).to_f
8
10
 
9
11
  # When changing how signals are caught, be sure to test the behavior with
10
12
  # the rake task in tasks/safe_shutdown.rb.
11
- at_exit do
12
- puts "Stopping Que..."
13
- Que.stop!
14
- end
15
13
 
16
14
  stop = false
17
15
  trap('INT'){stop = true}
18
16
 
17
+ at_exit do
18
+ $stdout.puts "Finishing Que's current jobs before exiting..."
19
+ Que.mode = :off
20
+ $stdout.puts "Que's jobs finished, exiting..."
21
+ end
22
+
19
23
  loop do
20
24
  sleep 0.01
21
25
  break if stop
data/lib/que/sql.rb CHANGED
@@ -55,7 +55,7 @@ module Que
55
55
  INSERT INTO que_jobs
56
56
  (priority, run_at, job_class, args)
57
57
  VALUES
58
- (coalesce($1, 1)::integer, coalesce($2, 'now')::timestamptz, $3::text, coalesce($4, '[]')::json)
58
+ (coalesce($1, 100)::integer, coalesce($2, 'now')::timestamptz, $3::text, coalesce($4, '[]')::json)
59
59
  RETURNING *
60
60
  }.freeze,
61
61
 
@@ -99,34 +99,6 @@ module Que
99
99
  JOIN pg_stat_activity USING (pid)
100
100
  WHERE locktype = 'advisory'
101
101
  ) pg USING (job_id)
102
- }.freeze,
103
-
104
- :create_table => %{
105
- CREATE TABLE que_jobs
106
- (
107
- priority integer NOT NULL DEFAULT 1,
108
- run_at timestamptz NOT NULL DEFAULT now(),
109
- job_id bigserial NOT NULL,
110
- job_class text NOT NULL,
111
- args json NOT NULL DEFAULT '[]'::json,
112
- error_count integer NOT NULL DEFAULT 0,
113
- last_error text,
114
-
115
- CONSTRAINT que_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
116
- )
117
102
  }.freeze
118
-
119
- # Note: if schema changes to the que_jobs table become necessary later on,
120
- # a simple versioning scheme would be:
121
-
122
- # Set version:
123
- # COMMENT ON TABLE que_jobs IS '2'
124
-
125
- # Get version:
126
- # SELECT description
127
- # FROM pg_description
128
- # JOIN pg_class
129
- # ON pg_description.objoid = pg_class.oid
130
- # WHERE relname = 'que_jobs'
131
103
  }
132
104
  end
data/lib/que/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Que
2
- Version = '0.4.0'
2
+ Version = '0.5.0'
3
3
  end
data/lib/que/worker.rb CHANGED
@@ -8,9 +8,6 @@ module Que
8
8
  # synchronize access to it.
9
9
  include MonitorMixin
10
10
 
11
- # A custom exception to immediately kill a worker and its current job.
12
- class Stop < Interrupt; end
13
-
14
11
  attr_reader :thread, :state
15
12
 
16
13
  def initialize
@@ -24,15 +21,7 @@ module Que
24
21
  end
25
22
 
26
23
  def sleeping?
27
- synchronize do
28
- if @state == :sleeping
29
- # There's a very small period of time between when the Worker marks
30
- # itself as sleeping and when it actually goes to sleep. Only report
31
- # true when we're certain the thread is sleeping.
32
- wait until @thread.status == 'sleep'
33
- true
34
- end
35
- end
24
+ synchronize { _sleeping? }
36
25
  end
37
26
 
38
27
  def working?
@@ -51,20 +40,10 @@ module Que
51
40
  end
52
41
  end
53
42
 
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.
43
+ # This needs to be called when trapping a signal, so it can't lock the monitor.
59
44
  def stop
60
- synchronize do
61
- @stop = true
62
- @thread.wakeup if sleeping?
63
- end
64
- end
65
-
66
- def stop!
67
- @thread.raise Stop
45
+ @stop = true
46
+ @thread.wakeup if _sleeping?
68
47
  end
69
48
 
70
49
  def wait_until_stopped
@@ -78,15 +57,47 @@ module Que
78
57
  sleep 0.0001
79
58
  end
80
59
 
60
+ def _sleeping?
61
+ if @state == :sleeping
62
+ # There's a very small period of time between when the Worker marks
63
+ # itself as sleeping and when it actually goes to sleep. Only report
64
+ # true when we're certain the thread is sleeping.
65
+ wait until @thread.status == 'sleep'
66
+ true
67
+ end
68
+ end
69
+
81
70
  def work_loop
82
71
  loop do
83
- job = Job.work
84
- synchronize { @state = :sleeping unless @stop || job }
72
+ time = Time.now
73
+ cycle = nil
74
+ result = Job.work
75
+
76
+ case result[:event]
77
+ when :job_unavailable
78
+ cycle = false
79
+ result[:level] = :debug
80
+ when :job_race_condition
81
+ cycle = true
82
+ result[:level] = :debug
83
+ when :job_worked
84
+ cycle = true
85
+ result[:elapsed] = (Time.now - time).round(5)
86
+ when :job_errored
87
+ # For PG::Errors, assume we had a problem reaching the database, and
88
+ # don't hit it again right away.
89
+ cycle = !result[:error].is_a?(PG::Error)
90
+ result[:error] = {:class => result[:error].class.to_s, :message => result[:error].message}
91
+ else
92
+ raise "Unknown Event: #{result[:event].inspect}"
93
+ end
94
+
95
+ Que.log(result)
96
+
97
+ synchronize { @state = :sleeping unless cycle || @stop }
85
98
  sleep if @state == :sleeping
86
99
  break if @stop
87
100
  end
88
- rescue Stop
89
- # This process is shutting down - let it.
90
101
  ensure
91
102
  @state = :stopped
92
103
  end
@@ -97,7 +108,12 @@ module Que
97
108
  # a worker, and make sure to wake up the wrangler when @wake_interval is
98
109
  # changed in Que.wake_interval= below.
99
110
  @wake_interval = 5
100
- @wrangler = Thread.new { loop { sleep(*@wake_interval); wake! if @wake_interval } }
111
+ @wrangler = Thread.new do
112
+ loop do
113
+ sleep *@wake_interval
114
+ wake! if @wake_interval
115
+ end
116
+ end
101
117
 
102
118
  class << self
103
119
  attr_reader :mode, :wake_interval
@@ -129,19 +145,6 @@ module Que
129
145
  @wrangler.wakeup
130
146
  end
131
147
 
132
- def stop!
133
- # The behavior of Worker#stop! is unpredictable - what it does is
134
- # dependent on what the Worker is currently doing. Sometimes it won't
135
- # work the first time, so we need to try again, but sometimes it'll
136
- # never do anything, so we can't repeat indefinitely. So, compromise.
137
- 5.times do
138
- break if workers.select(&:alive?).each(&:stop!).none?
139
- sleep 0.001
140
- end
141
-
142
- workers.clear
143
- end
144
-
145
148
  def wake!
146
149
  workers.find &:wake!
147
150
  end
@@ -154,17 +157,17 @@ module Que
154
157
 
155
158
  def set_mode(mode)
156
159
  if mode != @mode
157
- Que.log :info, "Set mode to #{mode.inspect}"
160
+ Que.log :event => 'mode_change', :value => mode.to_s
158
161
  @mode = mode
159
162
  end
160
163
  end
161
164
 
162
165
  def set_worker_count(count)
163
166
  if count != worker_count
164
- Que.log :info, "Set worker_count to #{count.inspect}"
167
+ Que.log :event => 'worker_count_change', :value => count.to_s
165
168
 
166
169
  if count > worker_count
167
- workers.push *(count - worker_count).times.map { new }
170
+ workers.push *(count - worker_count).times.map{new}
168
171
  elsif count < worker_count
169
172
  workers.pop(worker_count - count).each(&:stop).each(&:wait_until_stopped)
170
173
  end