que 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/que.rb CHANGED
@@ -39,19 +39,47 @@ module Que
39
39
  execute "DELETE FROM que_jobs"
40
40
  end
41
41
 
42
+ def job_stats
43
+ execute :job_stats
44
+ end
45
+
46
+ def worker_states
47
+ execute :worker_states
48
+ end
49
+
42
50
  def execute(command, *args)
43
- case command
44
- when Symbol then adapter.execute_prepared(command, *args)
45
- when String then adapter.execute(command, *args)
46
- end
51
+ indifferentiate case command
52
+ when Symbol then adapter.execute_prepared(command, *args)
53
+ when String then adapter.execute(command, *args)
54
+ end.to_a
47
55
  end
48
56
 
49
57
  def log(level, text)
50
58
  logger.send level, "[Que] #{text}" if logger
51
59
  end
52
60
 
61
+ # Helper for making hashes indifferently-accessible, even when nested
62
+ # within each other and within arrays.
63
+ def indifferentiate(object)
64
+ case object
65
+ when Hash
66
+ h = if {}.respond_to?(:with_indifferent_access) # Better support for Rails.
67
+ {}.with_indifferent_access
68
+ else
69
+ Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
70
+ end
71
+
72
+ object.each { |k, v| h[k] = indifferentiate(v) }
73
+ h
74
+ when Array
75
+ object.map { |v| indifferentiate(v) }
76
+ else
77
+ object
78
+ end
79
+ end
80
+
53
81
  # Copy some of the Worker class' config methods here for convenience.
54
- [:mode, :mode=, :worker_count=, :sleep_period, :sleep_period=, :stop!].each do |meth|
82
+ [:mode, :mode=, :worker_count, :worker_count=, :wake_interval, :wake_interval=, :stop!, :wake!, :wake_all!].each do |meth|
55
83
  define_method(meth) { |*args| Worker.send(meth, *args) }
56
84
  end
57
85
  end
@@ -12,11 +12,19 @@ module Que
12
12
 
13
13
  # The only method that adapters really need to implement. Should lock a
14
14
  # PG::Connection (or something that acts like a PG::Connection) so that
15
- # no other threads are using it and yield it to the block.
15
+ # no other threads are using it and yield it to the block. Should also
16
+ # be re-entrant.
16
17
  def checkout(&block)
17
18
  raise NotImplementedError
18
19
  end
19
20
 
21
+ # Called after a job is queued in async mode, to prompts a worker to
22
+ # wake up after the current transaction commits. Not all adapters will
23
+ # implement this.
24
+ def wake_worker_after_commit
25
+ false
26
+ end
27
+
20
28
  def execute(*args)
21
29
  checkout { |conn| conn.async_exec(*args) }
22
30
  end
@@ -3,6 +3,8 @@ require 'monitor'
3
3
  module Que
4
4
  module Adapters
5
5
  class PG < Base
6
+ attr_reader :lock
7
+
6
8
  def initialize(pg)
7
9
  @pg = pg
8
10
  @lock = Monitor.new # Must be re-entrant.
@@ -9,6 +9,10 @@ module Que
9
9
  def checkout(&block)
10
10
  @db.synchronize(&block)
11
11
  end
12
+
13
+ def wake_worker_after_commit
14
+ @db.after_commit { Que.wake! }
15
+ end
12
16
  end
13
17
  end
14
18
  end
@@ -2,8 +2,11 @@ require 'multi_json'
2
2
 
3
3
  module Que
4
4
  class Job
5
+ attr_reader :attrs
6
+
5
7
  def initialize(attrs)
6
- @attrs = attrs
8
+ @attrs = attrs
9
+ @attrs[:args] = Que.indifferentiate MultiJson.load(@attrs[:args])
7
10
  end
8
11
 
9
12
  # Subclasses should define their own run methods, but keep an empty one
@@ -12,18 +15,16 @@ module Que
12
15
  end
13
16
 
14
17
  def _run
15
- start = Time.now
16
-
17
- run *@attrs[:args]
18
+ time = Time.now
19
+ run *attrs[:args]
18
20
  destroy unless @destroyed
19
-
20
- Que.log :info, "Worked job in #{((Time.now - start) * 1000).round(1)} ms: #{inspect}"
21
+ Que.log :info, "Worked job in #{((Time.now - time) * 1000).round(1)} ms: #{inspect}"
21
22
  end
22
23
 
23
24
  private
24
25
 
25
26
  def destroy
26
- Que.execute :destroy_job, [@attrs[:priority], @attrs[:run_at], @attrs[:job_id]]
27
+ Que.execute :destroy_job, attrs.values_at(:priority, :run_at, :job_id)
27
28
  @destroyed = true
28
29
  end
29
30
 
@@ -38,18 +39,20 @@ module Que
38
39
 
39
40
  attrs = {:job_class => to_s, :args => MultiJson.dump(args)}
40
41
 
41
- if t = run_at || @default_run_at && @default_run_at.call
42
- attrs[:run_at] = t
42
+ if time = run_at || @default_run_at && @default_run_at.call
43
+ attrs[:run_at] = time
43
44
  end
44
45
 
45
- if p = priority || @default_priority
46
- attrs[:priority] = p
46
+ if pty = priority || @default_priority
47
+ attrs[:priority] = pty
47
48
  end
48
49
 
49
- if Que.mode == :sync && !attrs[:run_at]
50
+ if Que.mode == :sync && !time
50
51
  run_job(attrs)
51
52
  else
52
- Que.execute *insert_sql(attrs)
53
+ values = Que.execute(:insert_job, attrs.values_at(:priority, :run_at, :job_class, :args)).first
54
+ Que.adapter.wake_worker_after_commit unless time
55
+ new(values)
53
56
  end
54
57
  end
55
58
 
@@ -65,7 +68,7 @@ module Que
65
68
  # deleting it, and removing the lock.
66
69
  Que.adapter.checkout do
67
70
  begin
68
- if row = Que.execute(:lock_job).first
71
+ if job = Que.execute(:lock_job).first
69
72
  # Edge case: It's possible for the lock_job query to have
70
73
  # grabbed a job that's already been worked, if it took its MVCC
71
74
  # snapshot while the job was processing, but didn't attempt the
@@ -76,21 +79,21 @@ module Que
76
79
  # Note that there is currently no spec for this behavior, since
77
80
  # I'm not sure how to reliably commit a transaction that deletes
78
81
  # the job in a separate thread between lock_job and check_job.
79
- return true if Que.execute(:check_job, [row['priority'], row['run_at'], row['job_id']]).none?
82
+ return true if Que.execute(:check_job, job.values_at(:priority, :run_at, :job_id)).none?
80
83
 
81
- run_job(row)
84
+ run_job(job)
82
85
  else
83
86
  Que.log :info, "No jobs available..."
84
87
  nil
85
88
  end
86
89
  rescue => error
87
90
  begin
88
- if row
91
+ if job
89
92
  # Borrowed the backoff formula and error data format from delayed_job.
90
- count = row['error_count'].to_i + 1
91
- run_at = count ** 4 + 3
93
+ count = job[:error_count].to_i + 1
94
+ delay = count ** 4 + 3
92
95
  message = "#{error.message}\n#{error.backtrace.join("\n")}"
93
- Que.execute :set_error, [count, run_at, message, row['priority'], row['run_at'], row['job_id']]
96
+ Que.execute :set_error, [count, delay, message] + job.values_at(:priority, :run_at, :job_id)
94
97
  end
95
98
  rescue
96
99
  # If we can't reach the database for some reason, too bad, but
@@ -110,57 +113,20 @@ module Que
110
113
  return !error.is_a?(PG::Error)
111
114
  ensure
112
115
  # Clear the advisory lock we took when locking the job. Important
113
- # to do this so that they don't pile up in the database.
114
- Que.execute "SELECT pg_advisory_unlock_all()" if row
116
+ # to do this so that they don't pile up in the database. Again, if
117
+ # we can't reach the database, don't crash the work loop.
118
+ begin
119
+ Que.execute "SELECT pg_advisory_unlock($1)", [job[:job_id]] if job
120
+ rescue
121
+ end
115
122
  end
116
123
  end
117
124
  end
118
125
 
119
126
  private
120
127
 
121
- # Column names are not escaped, so this method should not be called with untrusted hashes.
122
- def insert_sql(hash)
123
- number = 0
124
- columns = []
125
- placeholders = []
126
- values = []
127
-
128
- hash.each do |key, value|
129
- columns << key
130
- placeholders << "$#{number += 1}"
131
- values << value
132
- end
133
-
134
- ["INSERT INTO que_jobs (#{columns.join(', ')}) VALUES (#{placeholders.join(', ')})", values]
135
- end
136
-
137
128
  def run_job(attrs)
138
- attrs = indifferentiate(attrs)
139
- attrs[:args] = indifferentiate(MultiJson.load(attrs[:args]))
140
- klass = attrs[:job_class].split('::').inject(Object, &:const_get)
141
- klass.new(attrs).tap(&:_run)
142
- end
143
-
144
- def indifferentiate(input)
145
- case input
146
- when Hash
147
- h = indifferent_hash
148
- input.each { |k, v| h[k] = indifferentiate(v) }
149
- h
150
- when Array
151
- input.map { |v| indifferentiate(v) }
152
- else
153
- input
154
- end
155
- end
156
-
157
- def indifferent_hash
158
- # Tiny hack to better support Rails.
159
- if {}.respond_to?(:with_indifferent_access)
160
- {}.with_indifferent_access
161
- else
162
- Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
163
- end
129
+ attrs[:job_class].split('::').inject(Object, &:const_get).new(attrs).tap(&:_run)
164
130
  end
165
131
  end
166
132
  end
@@ -4,7 +4,6 @@ namespace :que do
4
4
  require 'logger'
5
5
 
6
6
  Que.logger = Logger.new(STDOUT)
7
- Que.mode = :async
8
7
  Que.worker_count = (ENV['WORKER_COUNT'] || 4).to_i
9
8
 
10
9
  # When changing how signals are caught, be sure to test the behavior with
@@ -1,87 +1,132 @@
1
1
  module Que
2
2
  SQL = {
3
- # Thanks to RhodiumToad in #postgresql for the job lock CTE. It was
4
- # modified only slightly from his design.
5
- :lock_job => (
6
- <<-SQL
7
- WITH RECURSIVE cte AS (
8
- SELECT (job).*, pg_try_advisory_lock((job).job_id) AS locked
3
+ # Thanks to RhodiumToad in #postgresql for help with the job lock CTE.
4
+ :lock_job => %{
5
+ WITH RECURSIVE job AS (
6
+ SELECT (j).*, pg_try_advisory_lock((j).job_id) AS locked
7
+ FROM (
8
+ SELECT j
9
+ FROM que_jobs AS j
10
+ WHERE run_at <= now()
11
+ ORDER BY priority, run_at, job_id
12
+ LIMIT 1
13
+ ) AS t1
14
+ UNION ALL (
15
+ SELECT (j).*, pg_try_advisory_lock((j).job_id) AS locked
9
16
  FROM (
10
- SELECT job
11
- FROM que_jobs AS job
12
- WHERE run_at <= now()
13
- ORDER BY priority, run_at, job_id
17
+ SELECT (
18
+ SELECT j
19
+ 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
+ ORDER BY priority, run_at, job_id
22
+ LIMIT 1
23
+ ) AS j
24
+ FROM job
25
+ WHERE NOT job.locked
14
26
  LIMIT 1
15
27
  ) AS t1
16
- UNION ALL (
17
- SELECT (job).*, pg_try_advisory_lock((job).job_id) AS locked
18
- FROM (
19
- SELECT (
20
- SELECT job
21
- FROM que_jobs AS job
22
- WHERE run_at <= now() AND (priority, run_at, job_id) > (cte.priority, cte.run_at, cte.job_id)
23
- ORDER BY priority, run_at, job_id
24
- LIMIT 1
25
- ) AS job
26
- FROM cte
27
- WHERE NOT cte.locked
28
- LIMIT 1
29
- ) AS t1
30
- )
31
28
  )
32
- SELECT priority, run_at, job_id, job_class, args, error_count
33
- FROM cte
34
- WHERE locked
35
- LIMIT 1
36
- SQL
37
- ).freeze,
29
+ )
30
+ SELECT priority, run_at, job_id, job_class, args, error_count
31
+ FROM job
32
+ WHERE locked
33
+ LIMIT 1
34
+ }.freeze,
38
35
 
39
- :check_job => (
40
- <<-SQL
41
- SELECT 1 AS one
42
- FROM que_jobs
43
- WHERE priority = $1::integer
44
- AND run_at = $2::timestamptz
45
- AND job_id = $3::bigint
46
- SQL
47
- ).freeze,
36
+ :check_job => %{
37
+ SELECT 1 AS one
38
+ FROM que_jobs
39
+ WHERE priority = $1::integer
40
+ AND run_at = $2::timestamptz
41
+ AND job_id = $3::bigint
42
+ }.freeze,
48
43
 
49
- :set_error => (
50
- <<-SQL
51
- UPDATE que_jobs
52
- SET error_count = $1::integer,
53
- run_at = now() + $2::integer * '1 second'::interval,
54
- last_error = $3::text
55
- WHERE priority = $4::integer
56
- AND run_at = $5::timestamptz
57
- AND job_id = $6::bigint
58
- SQL
59
- ).freeze,
44
+ :set_error => %{
45
+ UPDATE que_jobs
46
+ SET error_count = $1::integer,
47
+ run_at = now() + $2::integer * '1 second'::interval,
48
+ last_error = $3::text
49
+ WHERE priority = $4::integer
50
+ AND run_at = $5::timestamptz
51
+ AND job_id = $6::bigint
52
+ }.freeze,
60
53
 
61
- :destroy_job => (
62
- <<-SQL
63
- DELETE FROM que_jobs
64
- WHERE priority = $1::integer
65
- AND run_at = $2::timestamptz
66
- AND job_id = $3::bigint
67
- SQL
68
- ).freeze,
54
+ :insert_job => %{
55
+ INSERT INTO que_jobs
56
+ (priority, run_at, job_class, args)
57
+ VALUES
58
+ (coalesce($1, 1)::integer, coalesce($2, 'now')::timestamptz, $3::text, coalesce($4, '[]')::json)
59
+ RETURNING *
60
+ }.freeze,
69
61
 
70
- :create_table => (
71
- <<-SQL
72
- CREATE TABLE que_jobs
73
- (
74
- priority integer NOT NULL DEFAULT 1,
75
- run_at timestamptz NOT NULL DEFAULT now(),
76
- job_id bigserial NOT NULL,
77
- job_class text NOT NULL,
78
- args json NOT NULL DEFAULT '[]'::json,
79
- error_count integer NOT NULL DEFAULT 0,
80
- last_error text,
62
+ :destroy_job => %{
63
+ DELETE FROM que_jobs
64
+ WHERE priority = $1::integer
65
+ AND run_at = $2::timestamptz
66
+ AND job_id = $3::bigint
67
+ }.freeze,
81
68
 
82
- CONSTRAINT que_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
83
- )
84
- SQL
85
- ).freeze
69
+ :job_stats => %{
70
+ SELECT job_class,
71
+ count(*) AS count,
72
+ count(locks.job_id) AS count_working,
73
+ sum((error_count > 0)::int) AS count_errored,
74
+ max(error_count) AS highest_error_count,
75
+ min(run_at) AS oldest_run_at
76
+ FROM que_jobs
77
+ LEFT JOIN (
78
+ SELECT (classid::bigint << 32) + objid::bigint AS job_id
79
+ FROM pg_locks
80
+ WHERE locktype = 'advisory'
81
+ ) locks USING (job_id)
82
+ GROUP BY job_class
83
+ ORDER BY count(*) DESC
84
+ }.freeze,
85
+
86
+ :worker_states => %{
87
+ SELECT que_jobs.*,
88
+ pg.pid AS pg_backend_pid,
89
+ pg.state AS pg_state,
90
+ pg.state_change AS pg_state_changed_at,
91
+ pg.query AS pg_last_query,
92
+ pg.query_start AS pg_last_query_started_at,
93
+ pg.xact_start AS pg_transaction_started_at,
94
+ pg.waiting AS pg_waiting_on_lock
95
+ FROM que_jobs
96
+ JOIN (
97
+ SELECT (classid::bigint << 32) + objid::bigint AS job_id, pg_stat_activity.*
98
+ FROM pg_locks
99
+ JOIN pg_stat_activity USING (pid)
100
+ WHERE locktype = 'advisory'
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
+ }.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'
86
131
  }
87
132
  end