que 0.14.3 → 1.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
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