que 0.14.3 → 1.0.0.beta

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.
Files changed (102) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +108 -14
  4. data/LICENSE.txt +1 -1
  5. data/README.md +49 -45
  6. data/bin/command_line_interface.rb +239 -0
  7. data/bin/que +8 -82
  8. data/docs/README.md +2 -0
  9. data/docs/active_job.md +6 -0
  10. data/docs/advanced_setup.md +7 -64
  11. data/docs/command_line_interface.md +45 -0
  12. data/docs/error_handling.md +65 -18
  13. data/docs/inspecting_the_queue.md +30 -80
  14. data/docs/job_helper_methods.md +27 -0
  15. data/docs/logging.md +3 -22
  16. data/docs/managing_workers.md +6 -61
  17. data/docs/middleware.md +15 -0
  18. data/docs/migrating.md +4 -7
  19. data/docs/multiple_queues.md +8 -4
  20. data/docs/shutting_down_safely.md +1 -1
  21. data/docs/using_plain_connections.md +39 -15
  22. data/docs/using_sequel.md +5 -3
  23. data/docs/writing_reliable_jobs.md +15 -24
  24. data/lib/que.rb +98 -182
  25. data/lib/que/active_job/extensions.rb +97 -0
  26. data/lib/que/active_record/connection.rb +51 -0
  27. data/lib/que/active_record/model.rb +48 -0
  28. data/lib/que/connection.rb +179 -0
  29. data/lib/que/connection_pool.rb +78 -0
  30. data/lib/que/job.rb +107 -156
  31. data/lib/que/job_cache.rb +240 -0
  32. data/lib/que/job_methods.rb +168 -0
  33. data/lib/que/listener.rb +176 -0
  34. data/lib/que/locker.rb +466 -0
  35. data/lib/que/metajob.rb +47 -0
  36. data/lib/que/migrations.rb +24 -17
  37. data/lib/que/migrations/4/down.sql +48 -0
  38. data/lib/que/migrations/4/up.sql +265 -0
  39. data/lib/que/poller.rb +267 -0
  40. data/lib/que/rails/railtie.rb +14 -0
  41. data/lib/que/result_queue.rb +35 -0
  42. data/lib/que/sequel/model.rb +51 -0
  43. data/lib/que/utils/assertions.rb +62 -0
  44. data/lib/que/utils/constantization.rb +19 -0
  45. data/lib/que/utils/error_notification.rb +68 -0
  46. data/lib/que/utils/freeze.rb +20 -0
  47. data/lib/que/utils/introspection.rb +50 -0
  48. data/lib/que/utils/json_serialization.rb +21 -0
  49. data/lib/que/utils/logging.rb +78 -0
  50. data/lib/que/utils/middleware.rb +33 -0
  51. data/lib/que/utils/queue_management.rb +18 -0
  52. data/lib/que/utils/transactions.rb +34 -0
  53. data/lib/que/version.rb +1 -1
  54. data/lib/que/worker.rb +128 -167
  55. data/que.gemspec +13 -2
  56. metadata +37 -80
  57. data/.rspec +0 -2
  58. data/.travis.yml +0 -64
  59. data/Gemfile +0 -24
  60. data/docs/customizing_que.md +0 -200
  61. data/lib/generators/que/install_generator.rb +0 -24
  62. data/lib/generators/que/templates/add_que.rb +0 -13
  63. data/lib/que/adapters/active_record.rb +0 -40
  64. data/lib/que/adapters/base.rb +0 -133
  65. data/lib/que/adapters/connection_pool.rb +0 -16
  66. data/lib/que/adapters/pg.rb +0 -21
  67. data/lib/que/adapters/pond.rb +0 -16
  68. data/lib/que/adapters/sequel.rb +0 -20
  69. data/lib/que/railtie.rb +0 -16
  70. data/lib/que/rake_tasks.rb +0 -59
  71. data/lib/que/sql.rb +0 -170
  72. data/spec/adapters/active_record_spec.rb +0 -175
  73. data/spec/adapters/connection_pool_spec.rb +0 -22
  74. data/spec/adapters/pg_spec.rb +0 -41
  75. data/spec/adapters/pond_spec.rb +0 -22
  76. data/spec/adapters/sequel_spec.rb +0 -57
  77. data/spec/gemfiles/Gemfile.current +0 -19
  78. data/spec/gemfiles/Gemfile.old +0 -19
  79. data/spec/gemfiles/Gemfile.older +0 -19
  80. data/spec/gemfiles/Gemfile.oldest +0 -19
  81. data/spec/spec_helper.rb +0 -129
  82. data/spec/support/helpers.rb +0 -25
  83. data/spec/support/jobs.rb +0 -35
  84. data/spec/support/shared_examples/adapter.rb +0 -42
  85. data/spec/support/shared_examples/multi_threaded_adapter.rb +0 -46
  86. data/spec/unit/configuration_spec.rb +0 -31
  87. data/spec/unit/connection_spec.rb +0 -14
  88. data/spec/unit/customization_spec.rb +0 -251
  89. data/spec/unit/enqueue_spec.rb +0 -245
  90. data/spec/unit/helper_spec.rb +0 -12
  91. data/spec/unit/logging_spec.rb +0 -101
  92. data/spec/unit/migrations_spec.rb +0 -84
  93. data/spec/unit/pool_spec.rb +0 -365
  94. data/spec/unit/run_spec.rb +0 -14
  95. data/spec/unit/states_spec.rb +0 -50
  96. data/spec/unit/stats_spec.rb +0 -46
  97. data/spec/unit/transaction_spec.rb +0 -36
  98. data/spec/unit/work_spec.rb +0 -596
  99. data/spec/unit/worker_spec.rb +0 -167
  100. data/tasks/benchmark.rb +0 -3
  101. data/tasks/rspec.rb +0 -14
  102. data/tasks/safe_shutdown.rb +0 -67
@@ -1,11 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # The class that jobs should generally inherit from.
4
+
3
5
  module Que
4
6
  class Job
5
- attr_reader :attrs, :_error
7
+ include JobMethods
8
+
9
+ MAXIMUM_TAGS_COUNT = 5
10
+ MAXIMUM_TAG_LENGTH = 100
11
+
12
+ SQL[:insert_job] =
13
+ %{
14
+ INSERT INTO public.que_jobs
15
+ (queue, priority, run_at, job_class, args, data)
16
+ VALUES
17
+ (
18
+ coalesce($1, 'default')::text,
19
+ coalesce($2, 100)::smallint,
20
+ coalesce($3, now())::timestamptz,
21
+ $4::text,
22
+ coalesce($5, '[]')::jsonb,
23
+ coalesce($6, '{}')::jsonb
24
+ )
25
+ RETURNING *
26
+ }
27
+
28
+ attr_reader :que_attrs
29
+ attr_accessor :que_error, :que_resolved
6
30
 
7
31
  def initialize(attrs)
8
- @attrs = attrs
32
+ @que_attrs = attrs
33
+ Que.internal_log(:job_instantiate, self) { attrs }
9
34
  end
10
35
 
11
36
  # Subclasses should define their own run methods, but keep an empty one
@@ -13,185 +38,111 @@ module Que
13
38
  def run(*args)
14
39
  end
15
40
 
16
- def _run
17
- run(*attrs[:args])
18
- destroy unless @destroyed
19
- rescue => error
20
- @_error = error
21
- run_error_notifier = handle_error(error)
22
- destroy unless @retried || @destroyed
23
-
24
- if run_error_notifier && Que.error_notifier
25
- # Protect the work loop from a failure of the error notifier.
26
- Que.error_notifier.call(error, @attrs) rescue nil
27
- end
28
- end
29
-
30
41
  private
31
42
 
32
- def error_count
33
- @attrs[:error_count]
34
- end
35
-
36
- def error_message
37
- self.class.send(:error_message, @_error)
38
- end
39
-
40
- def handle_error(error)
41
- error_count = @attrs[:error_count] += 1
42
- retry_interval = self.class.retry_interval || Job.retry_interval
43
- wait = retry_interval.respond_to?(:call) ? retry_interval.call(error_count) : retry_interval
44
- retry_in(wait)
45
- end
46
-
47
- def retry_in(period)
48
- Que.execute :set_error, [period, error_message] + @attrs.values_at(:queue, :priority, :run_at, :job_id)
49
- @retried = true
43
+ # Have the job helper methods act on this object.
44
+ def que_target
45
+ self
50
46
  end
51
47
 
52
- def destroy
53
- Que.execute :destroy_job, attrs.values_at(:queue, :priority, :run_at, :job_id)
54
- @destroyed = true
55
- end
56
-
57
- @retry_interval = proc { |count| count ** 4 + 3 }
58
-
59
48
  class << self
60
- attr_reader :retry_interval
61
-
62
- def enqueue(*args)
63
- if args.last.is_a?(Hash)
64
- options = args.pop
65
- queue = options.delete(:queue) || '' if options.key?(:queue)
66
- job_class = options.delete(:job_class)
67
- run_at = options.delete(:run_at)
68
- priority = options.delete(:priority)
69
- args << options if options.any?
70
- end
71
-
72
- attrs = {:job_class => job_class || to_s, :args => args}
73
-
74
- warn "@default_run_at in #{to_s} has been deprecated and will be removed in Que version 1.0.0. Please use @run_at instead." if @default_run_at
75
-
76
- if t = run_at || @run_at && @run_at.call || @default_run_at && @default_run_at.call
77
- attrs[:run_at] = t
78
- end
79
-
80
- warn "@default_priority in #{to_s} has been deprecated and will be removed in Que version 1.0.0. Please use @priority instead." if @default_priority
81
-
82
- if p = priority || @priority || @default_priority
83
- attrs[:priority] = p
84
- end
49
+ # Job class configuration options.
50
+ attr_accessor \
51
+ :run_synchronously,
52
+ :retry_interval,
53
+ :maximum_retry_count,
54
+ :queue,
55
+ :priority,
56
+ :run_at
57
+
58
+ def enqueue(
59
+ *args,
60
+ queue: nil,
61
+ priority: nil,
62
+ run_at: nil,
63
+ job_class: nil,
64
+ tags: nil,
65
+ **arg_opts
66
+ )
67
+
68
+ args << arg_opts if arg_opts.any?
69
+
70
+ if tags
71
+ if tags.length > MAXIMUM_TAGS_COUNT
72
+ raise Que::Error, "Can't enqueue a job with more than #{MAXIMUM_TAGS_COUNT} tags! (passed #{tags.length})"
73
+ end
85
74
 
86
- if q = queue || @queue
87
- attrs[:queue] = q
75
+ tags.each do |tag|
76
+ if tag.length > MAXIMUM_TAG_LENGTH
77
+ raise Que::Error, "Can't enqueue a job with a tag longer than 100 characters! (\"#{tag}\")"
78
+ end
79
+ end
88
80
  end
89
81
 
90
- if Que.mode == :sync && !t
91
- run(*attrs[:args])
82
+ attrs = {
83
+ queue: queue || resolve_que_setting(:queue) || Que.default_queue,
84
+ priority: priority || resolve_que_setting(:priority),
85
+ run_at: run_at || resolve_que_setting(:run_at),
86
+ args: Que.serialize_json(args),
87
+ data: tags ? Que.serialize_json(tags: tags) : "{}",
88
+ job_class: \
89
+ job_class || name ||
90
+ raise(Error, "Can't enqueue an anonymous subclass of Que::Job"),
91
+ }
92
+
93
+ if attrs[:run_at].nil? && resolve_que_setting(:run_synchronously)
94
+ attrs[:args] = Que.deserialize_json(attrs[:args])
95
+ attrs[:data] = Que.deserialize_json(attrs[:data])
96
+ _run_attrs(attrs)
92
97
  else
93
- values = Que.execute(:insert_job, attrs.values_at(:queue, :priority, :run_at, :job_class, :args)).first
94
- Que.adapter.wake_worker_after_commit unless t
98
+ values =
99
+ Que.execute(
100
+ :insert_job,
101
+ attrs.values_at(:queue, :priority, :run_at, :job_class, :args, :data),
102
+ ).first
103
+
95
104
  new(values)
96
105
  end
97
106
  end
98
107
 
99
- def queue(*args)
100
- warn "#{to_s}.queue(*args) is deprecated and will be removed in Que version 1.0.0. Please use #{to_s}.enqueue(*args) instead."
101
- enqueue(*args)
102
- end
103
-
104
108
  def run(*args)
109
+ # Make sure things behave the same as they would have with a round-trip
110
+ # to the DB.
111
+ args = Que.deserialize_json(Que.serialize_json(args))
112
+
105
113
  # Should not fail if there's no DB connection.
106
- new(:args => args).tap { |job| job.run(*args) }
114
+ _run_attrs(args: args)
107
115
  end
108
116
 
109
- def work(queue = '')
110
- # Since we're taking session-level advisory locks, we have to hold the
111
- # same connection throughout the process of getting a job, working it,
112
- # deleting it, and removing the lock.
113
- return_value =
114
- Que.adapter.checkout do
115
- begin
116
- if job = Que.execute(:lock_job, [queue]).first
117
- # Edge case: It's possible for the lock_job query to have
118
- # grabbed a job that's already been worked, if it took its MVCC
119
- # snapshot while the job was processing, but didn't attempt the
120
- # advisory lock until it was finished. Since we have the lock, a
121
- # previous worker would have deleted it by now, so we just
122
- # double check that it still exists before working it.
123
-
124
- # Note that there is currently no spec for this behavior, since
125
- # I'm not sure how to reliably commit a transaction that deletes
126
- # the job in a separate thread between lock_job and check_job.
127
- if Que.execute(:check_job, job.values_at(:queue, :priority, :run_at, :job_id)).none?
128
- {:event => :job_race_condition}
129
- else
130
- klass = class_for(job[:job_class])
131
- instance = klass.new(job)
132
- instance._run
133
- if e = instance._error
134
- {:event => :job_errored, :job => job, :error => e}
135
- else
136
- {:event => :job_worked, :job => job}
137
- end
138
- end
139
- else
140
- {:event => :job_unavailable}
141
- end
142
- rescue => error
143
- begin
144
- if job
145
- count = job[:error_count].to_i + 1
146
- interval = klass && klass.respond_to?(:retry_interval) && klass.retry_interval || retry_interval
147
- delay = interval.respond_to?(:call) ? interval.call(count) : interval
148
- message = error_message(error)
149
- Que.execute :set_error, [delay, message] + job.values_at(:queue, :priority, :run_at, :job_id)
150
- end
151
- rescue
152
- # If we can't reach the database for some reason, too bad, but
153
- # don't let it crash the work loop.
154
- end
155
-
156
- if Que.error_notifier
157
- # Similarly, protect the work loop from a failure of the error notifier.
158
- Que.error_notifier.call(error, job) rescue nil
159
- end
160
-
161
- return {:event => :job_errored, :error => error, :job => job}
162
- ensure
163
- # Clear the advisory lock we took when locking the job. Important
164
- # to do this so that they don't pile up in the database. Again, if
165
- # we can't reach the database, don't crash the work loop.
166
- begin
167
- Que.execute "SELECT pg_advisory_unlock($1)", [job[:job_id]] if job
168
- rescue
169
- end
170
- end
171
- end
172
-
173
- Que.adapter.cleanup!
117
+ def resolve_que_setting(setting, *args)
118
+ value = send(setting) if respond_to?(setting)
174
119
 
175
- return_value
120
+ if !value.nil?
121
+ value.respond_to?(:call) ? value.call(*args) : value
122
+ else
123
+ c = superclass
124
+ if c.respond_to?(:resolve_que_setting)
125
+ c.resolve_que_setting(setting, *args)
126
+ end
127
+ end
176
128
  end
177
129
 
178
130
  private
179
131
 
180
- def error_message(error)
181
- message = error.class.to_s
132
+ def _run_attrs(attrs)
133
+ attrs[:error_count] = 0
134
+ Que.recursively_freeze(attrs)
182
135
 
183
- unless error.message.nil? || error.message.strip.empty?
184
- message << ": #{error.message}"
136
+ new(attrs).tap do |job|
137
+ Que.run_middleware(job) do
138
+ job._run(reraise_errors: true)
139
+ end
185
140
  end
186
-
187
- message = message.slice(0, 500)
188
-
189
- ([message] + error.backtrace).join("\n")
190
- end
191
-
192
- def class_for(string)
193
- Que.constantize(string)
194
141
  end
195
142
  end
143
+
144
+ # Set up some defaults.
145
+ self.retry_interval = proc { |count| count ** 4 + 3 }
146
+ self.maximum_retry_count = 15
196
147
  end
197
148
  end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A sized thread-safe queue that holds ordered job sort_keys. Supports blocking
4
+ # while waiting for a job to become available, only returning jobs over a
5
+ # minimum priority, and stopping gracefully.
6
+
7
+ module Que
8
+ class JobCache
9
+ attr_reader :maximum_size, :minimum_size, :priority_queues
10
+
11
+ def initialize(
12
+ maximum_size:,
13
+ minimum_size:,
14
+ priorities:
15
+ )
16
+ @maximum_size = Que.assert(Integer, maximum_size)
17
+ Que.assert(maximum_size >= 0) { "maximum_size for a JobCache must be at least zero!" }
18
+
19
+ @minimum_size = Que.assert(Integer, minimum_size)
20
+ Que.assert(minimum_size >= 0) { "minimum_size for a JobCache must be at least zero!" }
21
+
22
+ Que.assert(minimum_size <= maximum_size) do
23
+ "minimum queue size (#{minimum_size}) is " \
24
+ "greater than the maximum queue size (#{maximum_size})!"
25
+ end
26
+
27
+ @stop = false
28
+ @array = []
29
+ @monitor = Monitor.new # TODO: Make this a mutex?
30
+
31
+ # Make sure that priority = nil sorts highest.
32
+ @priority_queues = Hash[
33
+ priorities.sort_by{|p| p || Float::INFINITY}.map do |p|
34
+ [p, PriorityQueue.new(priority: p, job_cache: self)]
35
+ end
36
+ ].freeze
37
+ end
38
+
39
+ def push(*metajobs)
40
+ Que.internal_log(:job_cache_push, self) do
41
+ {
42
+ maximum_size: maximum_size,
43
+ ids: metajobs.map(&:id),
44
+ current_queue: @array,
45
+ }
46
+ end
47
+
48
+ sync do
49
+ return metajobs if stopping?
50
+
51
+ @array.push(*metajobs).sort!
52
+
53
+ # Relying on the hash's contents being sorted, here.
54
+ priority_queues.reverse_each do |_, pq|
55
+ pq.waiting_count.times do
56
+ job = shift_job(pq.priority)
57
+ break if job.nil?
58
+ pq.push(job)
59
+ end
60
+ end
61
+
62
+ # If we passed the maximum queue size, drop the lowest sort keys and
63
+ # return their ids to be unlocked.
64
+ overage = -cache_space
65
+ pop(overage) if overage > 0
66
+ end
67
+ end
68
+
69
+ def shift(priority = nil)
70
+ queue = priority_queues.fetch(priority) { raise Error, "not a permitted priority! #{priority}" }
71
+ queue.pop
72
+ end
73
+
74
+ def shift_job(priority = nil)
75
+ sync do
76
+ if stopping?
77
+ false
78
+ elsif (job = @array.first) && job.priority_sufficient?(priority)
79
+ @array.shift
80
+ end
81
+ end
82
+ end
83
+
84
+ def accept?(metajobs)
85
+ return [] if stopping?
86
+
87
+ metajobs.sort!
88
+
89
+ sync do
90
+ start_index = cache_space
91
+ final_index = metajobs.length - 1
92
+
93
+ return metajobs if start_index > final_index
94
+ index_to_lose = @array.length - 1
95
+
96
+ start_index.upto(final_index) do |index|
97
+ if index_to_lose >= 0 && (metajobs[index] <=> @array[index_to_lose]) < 0
98
+ return metajobs if index == final_index
99
+ index_to_lose -= 1
100
+ else
101
+ return metajobs.slice(0...index)
102
+ end
103
+ end
104
+
105
+ []
106
+ end
107
+ end
108
+
109
+ def jobs_needed?
110
+ minimum_size > size
111
+ end
112
+
113
+ def waiting_count
114
+ count = 0
115
+ priority_queues.each_value do |pq|
116
+ count += pq.waiting_count
117
+ end
118
+ count
119
+ end
120
+
121
+ def available_priorities
122
+ hash = {}
123
+ lowest_priority = true
124
+
125
+ priority_queues.reverse_each do |priority, pq|
126
+ count = pq.waiting_count
127
+
128
+ if lowest_priority
129
+ count += cache_space
130
+ lowest_priority = false
131
+ end
132
+
133
+ hash[priority || MAXIMUM_PRIORITY] = count if count > 0
134
+ end
135
+
136
+ hash
137
+ end
138
+
139
+ def cache_space
140
+ sync do
141
+ maximum_size - size
142
+ end
143
+ end
144
+
145
+ def size
146
+ sync { @array.size }
147
+ end
148
+
149
+ def to_a
150
+ sync { @array.dup }
151
+ end
152
+
153
+ def stop
154
+ sync { @stop = true }
155
+ priority_queues.each_value(&:stop)
156
+ end
157
+
158
+ def clear
159
+ sync { pop(size) }
160
+ end
161
+
162
+ def stopping?
163
+ sync { !!@stop }
164
+ end
165
+
166
+ private
167
+
168
+ def pop(count)
169
+ @array.pop(count)
170
+ end
171
+
172
+ def sync
173
+ @monitor.synchronize { yield }
174
+ end
175
+
176
+ # A queue object dedicated to a specific worker priority. It's basically a
177
+ # Queue object from the standard library, but it's able to reach into the
178
+ # JobCache's cache in order to satisfy a pop.
179
+ class PriorityQueue
180
+ attr_reader :job_cache, :priority
181
+
182
+ def initialize(
183
+ job_cache:,
184
+ priority:
185
+ )
186
+ @job_cache = job_cache
187
+ @priority = priority
188
+ @waiting = 0
189
+ @stopping = false
190
+ @items = [] # Items pending distribution to waiting threads.
191
+ @monitor = Monitor.new
192
+ @cv = Monitor::ConditionVariable.new(@monitor)
193
+ end
194
+
195
+ def pop
196
+ sync do
197
+ loop do
198
+ return false if @stopping
199
+
200
+ if item = @items.pop
201
+ return item
202
+ end
203
+
204
+ job = job_cache.shift_job(priority)
205
+ return job unless job.nil? # False means we're stopping.
206
+
207
+ @waiting += 1
208
+ @cv.wait
209
+ @waiting -= 1
210
+ end
211
+ end
212
+ end
213
+
214
+ def push(item)
215
+ sync do
216
+ Que.assert(waiting_count > 0)
217
+ @items << item
218
+ @cv.signal
219
+ end
220
+ end
221
+
222
+ def stop
223
+ sync do
224
+ @stopping = true
225
+ @cv.broadcast
226
+ end
227
+ end
228
+
229
+ def waiting_count
230
+ @waiting
231
+ end
232
+
233
+ private
234
+
235
+ def sync
236
+ @monitor.synchronize { yield }
237
+ end
238
+ end
239
+ end
240
+ end