que 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +1 -1
  4. data/CHANGELOG.md +21 -1
  5. data/Gemfile +5 -0
  6. data/README.md +7 -6
  7. data/docs/advanced_setup.md +14 -4
  8. data/docs/customizing_que.md +4 -4
  9. data/docs/error_handling.md +13 -1
  10. data/docs/managing_workers.md +2 -2
  11. data/docs/migrating.md +26 -0
  12. data/docs/multiple_queues.md +13 -0
  13. data/docs/shutting_down_safely.md +7 -0
  14. data/docs/writing_reliable_jobs.md +43 -0
  15. data/lib/generators/que/templates/add_que.rb +1 -1
  16. data/lib/que.rb +27 -41
  17. data/lib/que/adapters/base.rb +75 -4
  18. data/lib/que/job.rb +45 -28
  19. data/lib/que/migrations.rb +3 -2
  20. data/lib/que/migrations/{1-down.sql → 1/down.sql} +0 -0
  21. data/lib/que/migrations/{1-up.sql → 1/up.sql} +0 -0
  22. data/lib/que/migrations/{2-down.sql → 2/down.sql} +0 -0
  23. data/lib/que/migrations/{2-up.sql → 2/up.sql} +0 -0
  24. data/lib/que/migrations/3/down.sql +5 -0
  25. data/lib/que/migrations/3/up.sql +5 -0
  26. data/lib/que/sql.rb +24 -17
  27. data/lib/que/version.rb +1 -1
  28. data/lib/que/worker.rb +6 -5
  29. data/spec/adapters/active_record_spec.rb +6 -6
  30. data/spec/adapters/sequel_spec.rb +4 -4
  31. data/spec/gemfiles/Gemfile1 +18 -0
  32. data/spec/gemfiles/Gemfile2 +18 -0
  33. data/spec/support/helpers.rb +2 -1
  34. data/spec/support/shared_examples/adapter.rb +7 -3
  35. data/spec/support/shared_examples/multi_threaded_adapter.rb +2 -2
  36. data/spec/travis.rb +12 -4
  37. data/spec/unit/customization_spec.rb +148 -0
  38. data/spec/unit/{queue_spec.rb → enqueue_spec.rb} +115 -14
  39. data/spec/unit/logging_spec.rb +3 -2
  40. data/spec/unit/migrations_spec.rb +3 -2
  41. data/spec/unit/pool_spec.rb +30 -6
  42. data/spec/unit/run_spec.rb +12 -0
  43. data/spec/unit/states_spec.rb +29 -31
  44. data/spec/unit/stats_spec.rb +16 -14
  45. data/spec/unit/work_spec.rb +120 -25
  46. data/spec/unit/worker_spec.rb +55 -9
  47. data/tasks/safe_shutdown.rb +1 -1
  48. metadata +30 -17
@@ -1,3 +1,5 @@
1
+ require 'time' # For Time.parse.
2
+
1
3
  module Que
2
4
  module Adapters
3
5
  autoload :ActiveRecord, 'que/adapters/active_record'
@@ -25,11 +27,35 @@ module Que
25
27
  false
26
28
  end
27
29
 
28
- def execute(*args)
30
+ def execute(command, params = [])
31
+ params = params.map do |param|
32
+ case param
33
+ # The pg gem unfortunately doesn't convert fractions of time instances, so cast them to a string.
34
+ when Time then param.strftime("%Y-%m-%d %H:%M:%S.%6N %z")
35
+ when Array, Hash then JSON_MODULE.dump(param)
36
+ else param
37
+ end
38
+ end
39
+
40
+ cast_result \
41
+ case command
42
+ when Symbol then execute_prepared(command, params)
43
+ when String then execute_sql(command, params)
44
+ end
45
+ end
46
+
47
+ def in_transaction?
48
+ checkout { |conn| conn.transaction_status != ::PG::PQTRANS_IDLE }
49
+ end
50
+
51
+ private
52
+
53
+ def execute_sql(sql, params)
54
+ args = params.empty? ? [sql] : [sql, params]
29
55
  checkout { |conn| conn.async_exec(*args) }
30
56
  end
31
57
 
32
- def execute_prepared(name, params = [])
58
+ def execute_prepared(name, params)
33
59
  checkout do |conn|
34
60
  statements = @prepared_statements[conn] ||= {}
35
61
 
@@ -42,8 +68,53 @@ module Que
42
68
  end
43
69
  end
44
70
 
45
- def in_transaction?
46
- checkout { |conn| conn.transaction_status != ::PG::PQTRANS_IDLE }
71
+ HASH_DEFAULT_PROC = proc { |hash, key| hash[key.to_s] if Symbol === key }
72
+
73
+ INDIFFERENTIATOR = proc do |object|
74
+ case object
75
+ when Array
76
+ object.each(&INDIFFERENTIATOR)
77
+ when Hash
78
+ object.default_proc = HASH_DEFAULT_PROC
79
+ object.each { |key, value| object[key] = INDIFFERENTIATOR.call(value) }
80
+ object
81
+ else
82
+ object
83
+ end
84
+ end
85
+
86
+ CAST_PROCS = {}
87
+
88
+ # Integer, bigint, smallint:
89
+ CAST_PROCS[23] = CAST_PROCS[20] = CAST_PROCS[21] = proc(&:to_i)
90
+
91
+ # Timestamp with time zone.
92
+ CAST_PROCS[1184] = Time.method(:parse)
93
+
94
+ # JSON.
95
+ CAST_PROCS[114] = JSON_MODULE.method(:load)
96
+
97
+ # Boolean:
98
+ CAST_PROCS[16] = 't'.method(:==)
99
+
100
+ def cast_result(result)
101
+ output = result.to_a
102
+
103
+ result.fields.each_with_index do |field, index|
104
+ if converter = CAST_PROCS[result.ftype(index)]
105
+ output.each do |hash|
106
+ unless (value = hash[field]).nil?
107
+ hash[field] = converter.call(value)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ if result.first.respond_to?(:with_indifferent_access)
114
+ output.map(&:with_indifferent_access)
115
+ else
116
+ output.each(&INDIFFERENTIATOR)
117
+ end
47
118
  end
48
119
  end
49
120
  end
data/lib/que/job.rb CHANGED
@@ -3,12 +3,11 @@ module Que
3
3
  attr_reader :attrs
4
4
 
5
5
  def initialize(attrs)
6
- @attrs = attrs
7
- @attrs[:args] = Que.indifferentiate JSON_MODULE.load(@attrs[:args])
6
+ @attrs = attrs
8
7
  end
9
8
 
10
9
  # Subclasses should define their own run methods, but keep an empty one
11
- # here so that Que::Job.queue can queue an empty job in testing.
10
+ # here so that Que::Job.enqueue can queue an empty job in testing.
12
11
  def run(*args)
13
12
  end
14
13
 
@@ -20,45 +19,61 @@ module Que
20
19
  private
21
20
 
22
21
  def destroy
23
- Que.execute :destroy_job, attrs.values_at(:priority, :run_at, :job_id)
22
+ Que.execute :destroy_job, attrs.values_at(:queue, :priority, :run_at, :job_id)
24
23
  @destroyed = true
25
24
  end
26
25
 
26
+ @retry_interval = proc { |count| count ** 4 + 3 }
27
+
27
28
  class << self
28
- def queue(*args)
29
+ attr_reader :retry_interval
30
+
31
+ def enqueue(*args)
29
32
  if args.last.is_a?(Hash)
30
- options = args.pop
31
- run_at = options.delete(:run_at)
32
- priority = options.delete(:priority)
33
+ options = args.pop
34
+ queue = options.delete(:queue) || '' if options.key?(:queue)
35
+ job_class = options.delete(:job_class)
36
+ run_at = options.delete(:run_at)
37
+ priority = options.delete(:priority)
33
38
  args << options if options.any?
34
39
  end
35
40
 
36
- attrs = {:job_class => to_s, :args => JSON_MODULE.dump(args)}
41
+ attrs = {:job_class => job_class || to_s, :args => args}
42
+
43
+ if t = run_at || @run_at && @run_at.call || @default_run_at && @default_run_at.call
44
+ attrs[:run_at] = t
45
+ end
37
46
 
38
- if time = run_at || @default_run_at && @default_run_at.call
39
- attrs[:run_at] = time
47
+ if p = priority || @priority || @default_priority
48
+ attrs[:priority] = p
40
49
  end
41
50
 
42
- if pty = priority || @default_priority
43
- attrs[:priority] = pty
51
+ if q = queue || @queue
52
+ attrs[:queue] = q
44
53
  end
45
54
 
46
- if Que.mode == :sync && !time
47
- run_job(attrs)
55
+ if Que.mode == :sync && !t
56
+ run(*attrs[:args])
48
57
  else
49
- values = Que.execute(:insert_job, attrs.values_at(:priority, :run_at, :job_class, :args)).first
50
- Que.adapter.wake_worker_after_commit unless time
58
+ values = Que.execute(:insert_job, attrs.values_at(:queue, :priority, :run_at, :job_class, :args)).first
59
+ Que.adapter.wake_worker_after_commit unless t
51
60
  new(values)
52
61
  end
53
62
  end
54
63
 
55
- def work
64
+ alias queue enqueue
65
+
66
+ def run(*args)
67
+ new(:args => args).tap(&:_run)
68
+ end
69
+
70
+ def work(queue = '')
56
71
  # Since we're taking session-level advisory locks, we have to hold the
57
72
  # same connection throughout the process of getting a job, working it,
58
73
  # deleting it, and removing the lock.
59
74
  Que.adapter.checkout do
60
75
  begin
61
- if job = Que.execute(:lock_job).first
76
+ if job = Que.execute(:lock_job, [queue]).first
62
77
  # Edge case: It's possible for the lock_job query to have
63
78
  # grabbed a job that's already been worked, if it took its MVCC
64
79
  # snapshot while the job was processing, but didn't attempt the
@@ -69,10 +84,12 @@ module Que
69
84
  # Note that there is currently no spec for this behavior, since
70
85
  # I'm not sure how to reliably commit a transaction that deletes
71
86
  # the job in a separate thread between lock_job and check_job.
72
- if Que.execute(:check_job, job.values_at(:priority, :run_at, :job_id)).none?
87
+ if Que.execute(:check_job, job.values_at(:queue, :priority, :run_at, :job_id)).none?
73
88
  {:event => :job_race_condition}
74
89
  else
75
- {:event => :job_worked, :job => run_job(job).attrs}
90
+ klass = class_for(job[:job_class])
91
+ klass.new(job)._run
92
+ {:event => :job_worked, :job => job}
76
93
  end
77
94
  else
78
95
  {:event => :job_unavailable}
@@ -80,11 +97,11 @@ module Que
80
97
  rescue => error
81
98
  begin
82
99
  if job
83
- # Borrowed the backoff formula and error data format from delayed_job.
84
- count = job[:error_count].to_i + 1
85
- delay = count ** 4 + 3
86
- message = "#{error.message}\n#{error.backtrace.join("\n")}"
87
- Que.execute :set_error, [count, delay, message] + job.values_at(:priority, :run_at, :job_id)
100
+ count = job[:error_count].to_i + 1
101
+ interval = klass && klass.retry_interval || retry_interval
102
+ delay = interval.respond_to?(:call) ? interval.call(count) : interval
103
+ message = "#{error.message}\n#{error.backtrace.join("\n")}"
104
+ Que.execute :set_error, [count, delay, message] + job.values_at(:queue, :priority, :run_at, :job_id)
88
105
  end
89
106
  rescue
90
107
  # If we can't reach the database for some reason, too bad, but
@@ -111,8 +128,8 @@ module Que
111
128
 
112
129
  private
113
130
 
114
- def run_job(attrs)
115
- attrs[:job_class].split('::').inject(Object, &:const_get).new(attrs).tap(&:_run)
131
+ def class_for(string)
132
+ string.split('::').inject(Object, &:const_get)
116
133
  end
117
134
  end
118
135
  end
@@ -3,7 +3,7 @@ module Que
3
3
  # In order to ship a schema change, add the relevant up and down sql files
4
4
  # to the migrations directory, and bump the version both here and in the
5
5
  # add_que generator template.
6
- CURRENT_VERSION = 2
6
+ CURRENT_VERSION = 3
7
7
 
8
8
  class << self
9
9
  def migrate!(options = {:version => CURRENT_VERSION})
@@ -21,7 +21,8 @@ module Que
21
21
  end
22
22
 
23
23
  steps.each do |step|
24
- Que.execute File.read("#{File.dirname(__FILE__)}/migrations/#{step}-#{direction}.sql")
24
+ sql = File.read("#{File.dirname(__FILE__)}/migrations/#{step}/#{direction}.sql")
25
+ Que.execute(sql)
25
26
  end
26
27
 
27
28
  set_db_version(version)
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,5 @@
1
+ ALTER TABLE que_jobs
2
+ DROP CONSTRAINT que_jobs_pkey,
3
+ DROP COLUMN queue,
4
+ ALTER COLUMN priority TYPE integer,
5
+ ADD CONSTRAINT que_jobs_pkey PRIMARY KEY (priority, run_at, job_id);
@@ -0,0 +1,5 @@
1
+ ALTER TABLE que_jobs
2
+ DROP CONSTRAINT que_jobs_pkey,
3
+ ALTER COLUMN priority TYPE smallint,
4
+ ADD COLUMN queue TEXT NOT NULL DEFAULT '',
5
+ ADD CONSTRAINT que_jobs_pkey PRIMARY KEY (queue, priority, run_at, job_id);
data/lib/que/sql.rb CHANGED
@@ -7,7 +7,8 @@ module Que
7
7
  FROM (
8
8
  SELECT j
9
9
  FROM que_jobs AS j
10
- WHERE run_at <= now()
10
+ WHERE queue = $1::text
11
+ AND run_at <= now()
11
12
  ORDER BY priority, run_at, job_id
12
13
  LIMIT 1
13
14
  ) AS t1
@@ -17,7 +18,9 @@ module Que
17
18
  SELECT (
18
19
  SELECT j
19
20
  FROM que_jobs AS j
20
- WHERE run_at <= now() AND (priority, run_at, job_id) > (job.priority, job.run_at, job.job_id)
21
+ WHERE queue = $1::text
22
+ AND run_at <= now()
23
+ AND (priority, run_at, job_id) > (job.priority, job.run_at, job.job_id)
21
24
  ORDER BY priority, run_at, job_id
22
25
  LIMIT 1
23
26
  ) AS j
@@ -27,7 +30,7 @@ module Que
27
30
  ) AS t1
28
31
  )
29
32
  )
30
- SELECT priority, run_at, job_id, job_class, args, error_count
33
+ SELECT queue, priority, run_at, job_id, job_class, args, error_count
31
34
  FROM job
32
35
  WHERE locked
33
36
  LIMIT 1
@@ -36,9 +39,10 @@ module Que
36
39
  :check_job => %{
37
40
  SELECT 1 AS one
38
41
  FROM que_jobs
39
- WHERE priority = $1::integer
40
- AND run_at = $2::timestamptz
41
- AND job_id = $3::bigint
42
+ WHERE queue = $1::text
43
+ AND priority = $2::smallint
44
+ AND run_at = $3::timestamptz
45
+ AND job_id = $4::bigint
42
46
  }.freeze,
43
47
 
44
48
  :set_error => %{
@@ -46,28 +50,31 @@ module Que
46
50
  SET error_count = $1::integer,
47
51
  run_at = now() + $2::integer * '1 second'::interval,
48
52
  last_error = $3::text
49
- WHERE priority = $4::integer
50
- AND run_at = $5::timestamptz
51
- AND job_id = $6::bigint
53
+ WHERE queue = $4::text
54
+ AND priority = $5::smallint
55
+ AND run_at = $6::timestamptz
56
+ AND job_id = $7::bigint
52
57
  }.freeze,
53
58
 
54
59
  :insert_job => %{
55
60
  INSERT INTO que_jobs
56
- (priority, run_at, job_class, args)
61
+ (queue, priority, run_at, job_class, args)
57
62
  VALUES
58
- (coalesce($1, 100)::integer, coalesce($2, 'now')::timestamptz, $3::text, coalesce($4, '[]')::json)
63
+ (coalesce($1, '')::text, coalesce($2, 100)::smallint, coalesce($3, 'now')::timestamptz, $4::text, coalesce($5, '[]')::json)
59
64
  RETURNING *
60
65
  }.freeze,
61
66
 
62
67
  :destroy_job => %{
63
68
  DELETE FROM que_jobs
64
- WHERE priority = $1::integer
65
- AND run_at = $2::timestamptz
66
- AND job_id = $3::bigint
69
+ WHERE queue = $1::text
70
+ AND priority = $2::smallint
71
+ AND run_at = $3::timestamptz
72
+ AND job_id = $4::bigint
67
73
  }.freeze,
68
74
 
69
75
  :job_stats => %{
70
- SELECT job_class,
76
+ SELECT queue,
77
+ job_class,
71
78
  count(*) AS count,
72
79
  count(locks.job_id) AS count_working,
73
80
  sum((error_count > 0)::int) AS count_errored,
@@ -79,7 +86,7 @@ module Que
79
86
  FROM pg_locks
80
87
  WHERE locktype = 'advisory'
81
88
  ) locks USING (job_id)
82
- GROUP BY job_class
89
+ GROUP BY queue, job_class
83
90
  ORDER BY count(*) DESC
84
91
  }.freeze,
85
92
 
@@ -100,5 +107,5 @@ module Que
100
107
  WHERE locktype = 'advisory'
101
108
  ) pg USING (job_id)
102
109
  }.freeze
103
- }
110
+ }.freeze
104
111
  end
data/lib/que/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Que
2
- Version = '0.5.0'
2
+ Version = '0.6.0'
3
3
  end
data/lib/que/worker.rb CHANGED
@@ -8,10 +8,11 @@ module Que
8
8
  # synchronize access to it.
9
9
  include MonitorMixin
10
10
 
11
- attr_reader :thread, :state
11
+ attr_reader :thread, :state, :queue
12
12
 
13
- def initialize
14
- super # For MonitorMixin.
13
+ def initialize(queue = '')
14
+ super() # For MonitorMixin.
15
+ @queue = queue
15
16
  @state = :working
16
17
  @thread = Thread.new { work_loop }
17
18
  end
@@ -71,7 +72,7 @@ module Que
71
72
  loop do
72
73
  time = Time.now
73
74
  cycle = nil
74
- result = Job.work
75
+ result = Job.work(queue)
75
76
 
76
77
  case result[:event]
77
78
  when :job_unavailable
@@ -167,7 +168,7 @@ module Que
167
168
  Que.log :event => 'worker_count_change', :value => count.to_s
168
169
 
169
170
  if count > worker_count
170
- workers.push *(count - worker_count).times.map{new}
171
+ workers.push *(count - worker_count).times.map{new(ENV['QUE_QUEUE'] || '')}
171
172
  elsif count < worker_count
172
173
  workers.pop(worker_count - count).each(&:stop).each(&:wait_until_stopped)
173
174
  end
@@ -22,7 +22,7 @@ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
22
22
  end
23
23
  end
24
24
 
25
- ActiveRecordJob.queue
25
+ ActiveRecordJob.enqueue
26
26
  Que::Job.work
27
27
 
28
28
  $pid1.should == $pid2
@@ -32,7 +32,7 @@ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
32
32
  end
33
33
 
34
34
  it "should instantiate args as ActiveSupport::HashWithIndifferentAccess" do
35
- ArgsJob.queue :param => 2
35
+ ArgsJob.enqueue :param => 2
36
36
  Que::Job.work
37
37
  $passed_args.first[:param].should == 2
38
38
  $passed_args.first.should be_an_instance_of ActiveSupport::HashWithIndifferentAccess
@@ -42,7 +42,7 @@ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
42
42
  Que.mode = :async
43
43
  sleep_until { Que::Worker.workers.all? &:sleeping? }
44
44
 
45
- Que::Job.queue :run_at => 1.minute.ago
45
+ Que::Job.enqueue :run_at => 1.minute.ago
46
46
  DB[:que_jobs].get(:run_at).should be_within(3).of Time.now - 60
47
47
 
48
48
  Que.wake_interval = 0.005.seconds
@@ -54,17 +54,17 @@ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
54
54
  sleep_until { Que::Worker.workers.all? &:sleeping? }
55
55
 
56
56
  # Wakes a worker immediately when not in a transaction.
57
- Que::Job.queue
57
+ Que::Job.enqueue
58
58
  sleep_until { Que::Worker.workers.all?(&:sleeping?) && DB[:que_jobs].empty? }
59
59
 
60
60
  ActiveRecord::Base.transaction do
61
- Que::Job.queue
61
+ Que::Job.enqueue
62
62
  Que::Worker.workers.each { |worker| worker.should be_sleeping }
63
63
  end
64
64
  sleep_until { Que::Worker.workers.all?(&:sleeping?) && DB[:que_jobs].empty? }
65
65
 
66
66
  # Do nothing when queueing with a specific :run_at.
67
- BlockJob.queue :run_at => Time.now
67
+ BlockJob.enqueue :run_at => Time.now
68
68
  Que::Worker.workers.each { |worker| worker.should be_sleeping }
69
69
  end
70
70