que 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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