que 1.0.0.beta2 → 1.0.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.
@@ -17,7 +17,7 @@ module Que
17
17
 
18
18
  # Use Rails' executor (if present) to make sure that the connection
19
19
  # we're using isn't taken from us while the block runs. See
20
- # https://github.com/chanks/que/issues/166#issuecomment-274218910
20
+ # https://github.com/que-rb/que/issues/166#issuecomment-274218910
21
21
  def wrap_in_rails_executor(&block)
22
22
  if defined?(::Rails.application.executor)
23
23
  ::Rails.application.executor.wrap(&block)
@@ -3,7 +3,7 @@
3
3
  module Que
4
4
  module ActiveRecord
5
5
  class Model < ::ActiveRecord::Base
6
- self.table_name = :que_jobs
6
+ self.table_name = 'public.que_jobs'
7
7
 
8
8
  t = arel_table
9
9
 
@@ -119,6 +119,10 @@ module Que
119
119
  loop { break if next_notification.nil? }
120
120
  end
121
121
 
122
+ def server_version
123
+ wrapped_connection.server_version
124
+ end
125
+
122
126
  def in_transaction?
123
127
  wrapped_connection.transaction_status != ::PG::PQTRANS_IDLE
124
128
  end
@@ -152,7 +156,13 @@ module Que
152
156
  },
153
157
 
154
158
  # Timestamp with time zone
155
- 1184 => Time.method(:parse),
159
+ 1184 => -> (value) {
160
+ case value
161
+ when Time then value
162
+ when String then Time.parse(value)
163
+ else raise "Unexpected time class: #{value.class} (#{value.inspect})"
164
+ end
165
+ }
156
166
  }
157
167
 
158
168
  # JSON, JSONB
@@ -59,10 +59,8 @@ module Que
59
59
 
60
60
  # Relying on the hash's contents being sorted, here.
61
61
  priority_queues.reverse_each do |_, pq|
62
- pq.waiting_count.times do
63
- job = _shift_job(pq.priority)
64
- break if job.nil? # False would mean we're stopping.
65
- pq.push(job)
62
+ pq.populate do
63
+ _shift_job(pq.priority)
66
64
  end
67
65
  end
68
66
 
@@ -75,7 +73,7 @@ module Que
75
73
 
76
74
  def shift(priority = nil)
77
75
  queue = priority_queues.fetch(priority) { raise Error, "not a permitted priority! #{priority}" }
78
- queue.pop
76
+ queue.pop || shift_job(priority)
79
77
  end
80
78
 
81
79
  def shift_job(priority = nil)
@@ -107,10 +105,6 @@ module Que
107
105
  end
108
106
  end
109
107
 
110
- def jobs_needed?
111
- minimum_size > size
112
- end
113
-
114
108
  def waiting_count
115
109
  count = 0
116
110
  priority_queues.each_value do |pq|
@@ -162,6 +156,10 @@ module Que
162
156
  sync { _stopping? }
163
157
  end
164
158
 
159
+ def job_available?(priority)
160
+ (job = @array.first) && job.priority_sufficient?(priority)
161
+ end
162
+
165
163
  private
166
164
 
167
165
  def _buffer_space
@@ -214,15 +212,14 @@ module Que
214
212
  def pop
215
213
  sync do
216
214
  loop do
217
- return false if @stopping
218
-
219
- if item = @items.pop
215
+ if @stopping
216
+ return false
217
+ elsif item = @items.pop
220
218
  return item
219
+ elsif job_buffer.job_available?(priority)
220
+ return false
221
221
  end
222
222
 
223
- job = job_buffer.shift_job(priority)
224
- return job unless job.nil? # False means we're stopping.
225
-
226
223
  @waiting += 1
227
224
  @cv.wait(mutex)
228
225
  @waiting -= 1
@@ -230,18 +227,20 @@ module Que
230
227
  end
231
228
  end
232
229
 
233
- def push(item)
230
+ def stop
234
231
  sync do
235
- Que.assert(waiting_count > 0)
236
- @items << item
237
- @cv.signal
232
+ @stopping = true
233
+ @cv.broadcast
238
234
  end
239
235
  end
240
236
 
241
- def stop
237
+ def populate
242
238
  sync do
243
- @stopping = true
244
- @cv.broadcast
239
+ waiting_count.times do
240
+ job = yield
241
+ break if job.nil? # False would mean we're stopping.
242
+ _push(job)
243
+ end
245
244
  end
246
245
  end
247
246
 
@@ -254,6 +253,12 @@ module Que
254
253
  def sync(&block)
255
254
  mutex.synchronize(&block)
256
255
  end
256
+
257
+ def _push(item)
258
+ Que.assert(waiting_count > 0)
259
+ @items << item
260
+ @cv.signal
261
+ end
257
262
  end
258
263
  end
259
264
  end
@@ -65,6 +65,10 @@ module Que
65
65
  raise error if reraise_errors
66
66
  end
67
67
 
68
+ def log_level(elapsed)
69
+ :debug
70
+ end
71
+
68
72
  private
69
73
 
70
74
  # This method defines the object on which the various job helper methods are
data/lib/que/locker.rb CHANGED
@@ -47,8 +47,7 @@ module Que
47
47
  DEFAULT_WAIT_PERIOD = 50
48
48
  DEFAULT_MINIMUM_BUFFER_SIZE = 2
49
49
  DEFAULT_MAXIMUM_BUFFER_SIZE = 8
50
- DEFAULT_WORKER_COUNT = 6
51
- DEFAULT_WORKER_PRIORITIES = [10, 30, 50].freeze
50
+ DEFAULT_WORKER_PRIORITIES = [10, 30, 50, nil, nil, nil].freeze
52
51
 
53
52
  def initialize(
54
53
  queues: [Que.default_queue],
@@ -59,7 +58,6 @@ module Que
59
58
  wait_period: DEFAULT_WAIT_PERIOD,
60
59
  maximum_buffer_size: DEFAULT_MAXIMUM_BUFFER_SIZE,
61
60
  minimum_buffer_size: DEFAULT_MINIMUM_BUFFER_SIZE,
62
- worker_count: DEFAULT_WORKER_COUNT,
63
61
  worker_priorities: DEFAULT_WORKER_PRIORITIES,
64
62
  on_worker_start: nil
65
63
  )
@@ -71,19 +69,16 @@ module Que
71
69
 
72
70
  Que.assert Numeric, poll_interval
73
71
  Que.assert Numeric, wait_period
74
- Que.assert Integer, worker_count
75
72
 
76
73
  Que.assert Array, worker_priorities
77
- worker_priorities.each { |p| Que.assert(Integer, p) }
78
-
79
- all_worker_priorities = worker_priorities.values_at(0...worker_count)
74
+ worker_priorities.each { |p| Que.assert([Integer, NilClass], p) }
80
75
 
81
76
  # We use a JobBuffer to track jobs and pass them to workers, and a
82
77
  # ResultQueue to receive messages from workers.
83
78
  @job_buffer = JobBuffer.new(
84
79
  maximum_size: maximum_buffer_size,
85
80
  minimum_size: minimum_buffer_size,
86
- priorities: all_worker_priorities.uniq,
81
+ priorities: worker_priorities.uniq,
87
82
  )
88
83
 
89
84
  @result_queue = ResultQueue.new
@@ -99,7 +94,6 @@ module Que
99
94
  wait_period: wait_period,
100
95
  maximum_buffer_size: maximum_buffer_size,
101
96
  minimum_buffer_size: minimum_buffer_size,
102
- worker_count: worker_count,
103
97
  worker_priorities: worker_priorities,
104
98
  }
105
99
  end
@@ -110,13 +104,8 @@ module Que
110
104
  @queue_names = queues.is_a?(Hash) ? queues.keys : queues
111
105
  @wait_period = wait_period.to_f / 1000 # Milliseconds to seconds.
112
106
 
113
- # If the worker_count exceeds the array of priorities it'll result in
114
- # extra workers that will work jobs of any priority. For example, the
115
- # default worker_count of 6 and the default worker priorities of [10, 30,
116
- # 50] will result in three workers that only work jobs that meet those
117
- # priorities, and three workers that will work any job.
118
107
  @workers =
119
- all_worker_priorities.map do |priority|
108
+ worker_priorities.map do |priority|
120
109
  Worker.new(
121
110
  priority: priority,
122
111
  job_buffer: @job_buffer,
@@ -138,13 +127,20 @@ module Que
138
127
  if connection_url
139
128
  uri = URI.parse(connection_url)
140
129
 
141
- {
142
- host: uri.host,
143
- user: uri.user,
144
- password: uri.password,
145
- port: uri.port || 5432,
146
- dbname: uri.path[1..-1],
147
- }.merge(Hash[uri.query.split("&").map{|s| s.split('=')}.map{|a,b| [a.to_sym, b]}])
130
+ opts =
131
+ {
132
+ host: uri.host,
133
+ user: uri.user,
134
+ password: uri.password,
135
+ port: uri.port || 5432,
136
+ dbname: uri.path[1..-1],
137
+ }
138
+
139
+ if uri.query
140
+ opts.merge!(Hash[uri.query.split("&").map{|s| s.split('=')}.map{|a,b| [a.to_sym, b]}])
141
+ end
142
+
143
+ opts
148
144
  else
149
145
  Que.pool.checkout do |conn|
150
146
  c = conn.wrapped_connection
@@ -305,12 +301,15 @@ module Que
305
301
 
306
302
  def poll
307
303
  # Only poll when there are pollers to use (that is, when polling is
308
- # enabled) and when the local queue has dropped below the configured
309
- # minimum size.
310
- return unless pollers && job_buffer.jobs_needed?
304
+ # enabled).
305
+ return unless pollers
311
306
 
312
307
  # Figure out what job priorities we have to fill.
313
308
  priorities = job_buffer.available_priorities
309
+
310
+ # Only poll when there are workers ready for jobs.
311
+ return if priorities.empty?
312
+
314
313
  all_metajobs = []
315
314
 
316
315
  pollers.each do |poller|
@@ -394,10 +393,12 @@ module Que
394
393
  }
395
394
  end
396
395
 
396
+ materalize_cte = connection.server_version >= 12_00_00
397
+
397
398
  jobs =
398
399
  connection.execute \
399
400
  <<-SQL
400
- WITH jobs AS (SELECT * FROM que_jobs WHERE id IN (#{ids.join(', ')}))
401
+ WITH jobs AS #{materalize_cte ? 'MATERIALIZED' : ''} (SELECT * FROM que_jobs WHERE id IN (#{ids.join(', ')}))
401
402
  SELECT * FROM jobs WHERE pg_try_advisory_lock(id)
402
403
  SQL
403
404
 
@@ -5,8 +5,6 @@ module Que
5
5
  class Railtie < ::Rails::Railtie
6
6
  config.que = Que
7
7
 
8
- Que.run_asynchronously = true if ::Rails.env.test?
9
-
10
8
  Que.logger = proc { ::Rails.logger }
11
9
  Que.connection = ::ActiveRecord if defined? ::ActiveRecord
12
10
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ::Sequel.extension :pg_json_ops
4
+
3
5
  module Que
4
6
  module Sequel
5
7
  QUALIFIED_TABLE = ::Sequel.qualify(:public, :que_jobs)
@@ -7,10 +9,10 @@ module Que
7
9
  class Model < ::Sequel::Model(QUALIFIED_TABLE)
8
10
  dataset_module do
9
11
  conditions = {
10
- errored: ::Sequel.qualify(QUALIFIED_TABLE, :error_count) > 0,
11
- expired: ::Sequel.~(::Sequel.qualify(QUALIFIED_TABLE, :expired_at) => nil),
12
- finished: ::Sequel.~(::Sequel.qualify(QUALIFIED_TABLE, :finished_at) => nil),
13
- scheduled: ::Sequel.qualify(QUALIFIED_TABLE, :run_at) > ::Sequel::CURRENT_TIMESTAMP,
12
+ errored: QUALIFIED_TABLE[:error_count] > 0,
13
+ expired: QUALIFIED_TABLE[:expired_at] !~ nil,
14
+ finished: QUALIFIED_TABLE[:finished_at] !~ nil,
15
+ scheduled: QUALIFIED_TABLE[:run_at] > ::Sequel::CURRENT_TIMESTAMP,
14
16
  }
15
17
 
16
18
  conditions.each do |name, condition|
@@ -18,32 +20,28 @@ module Que
18
20
  subset :"not_#{name}", ~condition
19
21
  end
20
22
 
21
- subset :ready, conditions.values.map(&:~).inject{|a, b| a & b}
22
- subset :not_ready, conditions.values. inject{|a, b| a | b}
23
+ subset :ready, conditions.values.map(&:~).inject(:&)
24
+ subset :not_ready, conditions.values. inject(:|)
23
25
 
24
26
  def by_job_class(job_class)
25
27
  job_class = job_class.name if job_class.is_a?(Class)
26
28
  where(
27
- ::Sequel.|(
28
- {::Sequel.qualify(QUALIFIED_TABLE, :job_class) => job_class},
29
- {
30
- ::Sequel.qualify(QUALIFIED_TABLE, :job_class) => "ActiveJob::QueueAdapters::QueAdapter::JobWrapper",
31
- ::Sequel.lit("public.que_jobs.args->0->>'job_class'") => job_class,
32
- }
33
- )
29
+ (QUALIFIED_TABLE[:job_class] =~ job_class) |
30
+ (QUALIFIED_TABLE[:job_class] =~ "ActiveJob::QueueAdapters::QueAdapter::JobWrapper") &
31
+ (QUALIFIED_TABLE[:args].pg_jsonb[0].get_text("job_class") =~ job_class)
34
32
  )
35
33
  end
36
34
 
37
35
  def by_queue(queue)
38
- where(::Sequel.qualify(QUALIFIED_TABLE, :queue) => queue)
36
+ where(QUALIFIED_TABLE[:queue] => queue)
39
37
  end
40
38
 
41
39
  def by_tag(tag)
42
- where(::Sequel.lit("public.que_jobs.data @> ?", JSON.dump(tags: [tag])))
40
+ where(QUALIFIED_TABLE[:data].pg_jsonb.contains(JSON.dump(tags: [tag])))
43
41
  end
44
42
 
45
43
  def by_args(*args)
46
- where(::Sequel.lit("public.que_jobs.args @> ?", JSON.dump(args)))
44
+ where(QUALIFIED_TABLE[:args].pg_jsonb.contains(JSON.dump(args)))
47
45
  end
48
46
  end
49
47
  end
data/lib/que/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Que
4
- VERSION = '1.0.0.beta2'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/que/worker.rb CHANGED
@@ -3,10 +3,14 @@
3
3
  # Workers wrap threads which continuously pull job pks from JobBuffer objects,
4
4
  # fetch and work those jobs, and export relevant data to ResultQueues.
5
5
 
6
+ require 'set'
7
+
6
8
  module Que
7
9
  class Worker
8
10
  attr_reader :thread, :priority
9
11
 
12
+ VALID_LOG_LEVELS = [:debug, :info, :warn, :error, :fatal, :unknown].to_set.freeze
13
+
10
14
  SQL[:check_job] =
11
15
  %{
12
16
  SELECT 1 AS one
@@ -50,10 +54,17 @@ module Que
50
54
  private
51
55
 
52
56
  def work_loop
53
- # Blocks until a job of the appropriate priority is available. If the
54
- # queue is shutting down this will return nil, which breaks the loop and
57
+ # Blocks until a job of the appropriate priority is available.
58
+ # `fetch_next_metajob` normally returns a job to be processed.
59
+ # If the queue is shutting down it will return false, which breaks the loop and
55
60
  # lets the thread finish.
56
- while metajob = fetch_next_metajob
61
+ while (metajob = fetch_next_metajob) != false
62
+ # If metajob is nil instead of false, we've hit a rare race condition where
63
+ # there was a job in the buffer when the worker code checked, but the job was
64
+ # picked up by the time we got around to shifting it off the buffer.
65
+ # Letting this case go unhandled leads to worker threads exiting pre-maturely, so
66
+ # we check explicitly and continue the loop.
67
+ next if metajob.nil?
57
68
  id = metajob.id
58
69
 
59
70
  Que.internal_log(:worker_received_job, self) { {id: id} }
@@ -91,20 +102,31 @@ module Que
91
102
 
92
103
  Que.run_job_middleware(instance) { instance.tap(&:_run) }
93
104
 
94
- log_message = {
95
- level: :debug,
96
- job_id: metajob.id,
97
- elapsed: (Time.now - start),
98
- }
105
+ elapsed = Time.now - start
99
106
 
100
- if error = instance.que_error
101
- log_message[:event] = :job_errored
102
- log_message[:error] = "#{error.class}: #{error.message}".slice(0, 500)
103
- else
104
- log_message[:event] = :job_worked
105
- end
107
+ log_level =
108
+ if instance.que_error
109
+ :error
110
+ else
111
+ instance.log_level(elapsed)
112
+ end
113
+
114
+ if VALID_LOG_LEVELS.include?(log_level)
115
+ log_message = {
116
+ level: log_level,
117
+ job_id: metajob.id,
118
+ elapsed: elapsed,
119
+ }
106
120
 
107
- Que.log(log_message)
121
+ if error = instance.que_error
122
+ log_message[:event] = :job_errored
123
+ log_message[:error] = "#{error.class}: #{error.message}".slice(0, 500)
124
+ else
125
+ log_message[:event] = :job_worked
126
+ end
127
+
128
+ Que.log(log_message)
129
+ end
108
130
 
109
131
  instance
110
132
  rescue => error
data/que.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ['christopher.m.hanks@gmail.com']
11
11
  spec.description = %q{A job queue that uses PostgreSQL's advisory locks for speed and reliability.}
12
12
  spec.summary = %q{A PostgreSQL-based Job Queue}
13
- spec.homepage = 'https://github.com/chanks/que'
13
+ spec.homepage = 'https://github.com/que-rb/que'
14
14
  spec.license = 'MIT'
15
15
 
16
16
  files_to_exclude = [
@@ -29,5 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
30
30
  spec.require_paths = ['lib']
31
31
 
32
- spec.add_development_dependency 'bundler', '~> 1.3'
32
+ spec.add_development_dependency 'bundler'
33
33
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: que
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Hanks
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-13 00:00:00.000000000 Z
11
+ date: 2022-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.3'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.3'
26
+ version: '0'
27
27
  description: A job queue that uses PostgreSQL's advisory locks for speed and reliability.
28
28
  email:
29
29
  - christopher.m.hanks@gmail.com
@@ -32,8 +32,8 @@ executables:
32
32
  extensions: []
33
33
  extra_rdoc_files: []
34
34
  files:
35
+ - ".github/workflows/tests.yml"
35
36
  - ".gitignore"
36
- - CHANGELOG.1.0.beta.md
37
37
  - CHANGELOG.md
38
38
  - LICENSE.txt
39
39
  - README.md
@@ -41,21 +41,6 @@ files:
41
41
  - bin/command_line_interface.rb
42
42
  - bin/que
43
43
  - docs/README.md
44
- - docs/active_job.md
45
- - docs/advanced_setup.md
46
- - docs/command_line_interface.md
47
- - docs/error_handling.md
48
- - docs/inspecting_the_queue.md
49
- - docs/job_helper_methods.md
50
- - docs/logging.md
51
- - docs/managing_workers.md
52
- - docs/middleware.md
53
- - docs/migrating.md
54
- - docs/multiple_queues.md
55
- - docs/shutting_down_safely.md
56
- - docs/using_plain_connections.md
57
- - docs/using_sequel.md
58
- - docs/writing_reliable_jobs.md
59
44
  - lib/que.rb
60
45
  - lib/que/active_job/extensions.rb
61
46
  - lib/que/active_record/connection.rb
@@ -94,11 +79,11 @@ files:
94
79
  - lib/que/version.rb
95
80
  - lib/que/worker.rb
96
81
  - que.gemspec
97
- homepage: https://github.com/chanks/que
82
+ homepage: https://github.com/que-rb/que
98
83
  licenses:
99
84
  - MIT
100
85
  metadata: {}
101
- post_install_message:
86
+ post_install_message:
102
87
  rdoc_options: []
103
88
  require_paths:
104
89
  - lib
@@ -109,13 +94,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
109
94
  version: '0'
110
95
  required_rubygems_version: !ruby/object:Gem::Requirement
111
96
  requirements:
112
- - - ">"
97
+ - - ">="
113
98
  - !ruby/object:Gem::Version
114
- version: 1.3.1
99
+ version: '0'
115
100
  requirements: []
116
- rubyforge_project:
117
- rubygems_version: 2.7.3
118
- signing_key:
101
+ rubygems_version: 3.1.4
102
+ signing_key:
119
103
  specification_version: 4
120
104
  summary: A PostgreSQL-based Job Queue
121
105
  test_files: []