que 0.3.0 → 0.4.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.
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