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
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Que
4
+ SQL[:finish_job] =
5
+ %{
6
+ UPDATE public.que_jobs
7
+ SET finished_at = now()
8
+ WHERE id = $1::bigint
9
+ }
10
+
11
+ SQL[:expire_job] =
12
+ %{
13
+ UPDATE public.que_jobs
14
+ SET error_count = error_count + 1,
15
+ expired_at = now()
16
+ WHERE id = $1::bigint
17
+ }
18
+
19
+ SQL[:destroy_job] =
20
+ %{
21
+ DELETE FROM public.que_jobs
22
+ WHERE id = $1::bigint
23
+ }
24
+
25
+ SQL[:set_error] =
26
+ %{
27
+ UPDATE public.que_jobs
28
+ SET error_count = error_count + 1,
29
+ run_at = now() + $1::float * '1 second'::interval,
30
+ last_error_message = left($2::text, 500),
31
+ last_error_backtrace = left($3::text, 10000)
32
+ WHERE id = $4::bigint
33
+ }
34
+
35
+ module JobMethods
36
+ # Note that we delegate almost all methods to the result of the que_target
37
+ # method, which could be one of a few things, depending on the circumstance.
38
+
39
+ # Run the job with error handling and cleanup logic. Optionally support
40
+ # overriding the args, because it's necessary when jobs are invoked from
41
+ # ActiveJob.
42
+ def _run(args: nil, reraise_errors: false)
43
+ if args.nil? && que_target
44
+ args = que_target.que_attrs.fetch(:args)
45
+ end
46
+
47
+ run(*args)
48
+ default_resolve_action if que_target && !que_target.que_resolved
49
+ rescue => error
50
+ raise error unless que_target
51
+
52
+ que_target.que_error = error
53
+
54
+ run_error_notifier =
55
+ begin
56
+ handle_error(error)
57
+ rescue => error_2
58
+ Que.notify_error(error_2, que_target.que_attrs)
59
+ true
60
+ end
61
+
62
+ Que.notify_error(error, que_target.que_attrs) if run_error_notifier
63
+ retry_in_default_interval unless que_target.que_resolved
64
+
65
+ raise error if reraise_errors
66
+ end
67
+
68
+ private
69
+
70
+ # This method defines the object on which the various job helper methods are
71
+ # acting. When using Que in the default configuration this will just be
72
+ # self, but when using the Que adapter for ActiveJob it'll be the actual
73
+ # underlying job object. When running an ActiveJob::Base subclass that
74
+ # includes this module through a separate adapter this will be nil - hence,
75
+ # the defensive coding in every method that no-ops if que_target is falsy.
76
+ def que_target
77
+ raise NotImplementedError
78
+ end
79
+
80
+ def resolve_que_setting(*args)
81
+ return unless que_target
82
+
83
+ que_target.class.resolve_que_setting(*args)
84
+ end
85
+
86
+ def default_resolve_action
87
+ return unless que_target
88
+
89
+ destroy
90
+ end
91
+
92
+ def expire
93
+ return unless que_target
94
+
95
+ if id = que_target.que_attrs[:id]
96
+ Que.execute :expire_job, [id]
97
+ end
98
+
99
+ que_target.que_resolved = true
100
+ end
101
+
102
+ def finish
103
+ return unless que_target
104
+
105
+ if id = que_target.que_attrs[:id]
106
+ Que.execute :finish_job, [id]
107
+ end
108
+
109
+ que_target.que_resolved = true
110
+ end
111
+
112
+ def error_count
113
+ return 0 unless que_target
114
+
115
+ count = que_target.que_attrs.fetch(:error_count)
116
+ que_target.que_error ? count + 1 : count
117
+ end
118
+
119
+ # To be overridden in subclasses.
120
+ def handle_error(error)
121
+ return unless que_target
122
+
123
+ max = resolve_que_setting(:maximum_retry_count)
124
+
125
+ if max && error_count > max
126
+ expire
127
+ else
128
+ retry_in_default_interval
129
+ end
130
+ end
131
+
132
+ def retry_in_default_interval
133
+ return unless que_target
134
+
135
+ retry_in(resolve_que_setting(:retry_interval, error_count))
136
+ end
137
+
138
+ # Explicitly check for the job id in these helpers, because it won't exist
139
+ # if we're running synchronously.
140
+ def retry_in(period)
141
+ return unless que_target
142
+
143
+ if id = que_target.que_attrs[:id]
144
+ values = [period]
145
+
146
+ if e = que_target.que_error
147
+ values << "#{e.class}: #{e.message}".slice(0, 500) << e.backtrace.join("\n").slice(0, 10000)
148
+ else
149
+ values << nil << nil
150
+ end
151
+
152
+ Que.execute :set_error, values << id
153
+ end
154
+
155
+ que_target.que_resolved = true
156
+ end
157
+
158
+ def destroy
159
+ return unless que_target
160
+
161
+ if id = que_target.que_attrs[:id]
162
+ Que.execute :destroy_job, [id]
163
+ end
164
+
165
+ que_target.que_resolved = true
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Que
4
+ class Listener
5
+ MESSAGE_FORMATS = {}
6
+
7
+ attr_reader :connection, :channel
8
+
9
+ def initialize(connection:, channel: nil)
10
+ @connection = connection
11
+ @channel = channel || "que_listener_#{connection.backend_pid}"
12
+
13
+ Que.internal_log :listener_instantiate, self do
14
+ {
15
+ backend_pid: connection.backend_pid,
16
+ }
17
+ end
18
+ end
19
+
20
+ def listen
21
+ connection.execute "LISTEN #{channel}"
22
+ end
23
+
24
+ def wait_for_grouped_messages(timeout)
25
+ messages = wait_for_messages(timeout)
26
+
27
+ output = {}
28
+
29
+ messages.each do |message|
30
+ message_type = message.delete(:message_type)
31
+
32
+ (output[message_type.to_sym] ||= []) << message.freeze
33
+ end
34
+
35
+ output
36
+ end
37
+
38
+ def wait_for_messages(timeout)
39
+ # Make sure we never pass nil to this method, so we don't hang the thread.
40
+ Que.assert(Numeric, timeout)
41
+
42
+ Que.internal_log :listener_waiting, self do
43
+ {
44
+ backend_pid: connection.backend_pid,
45
+ channel: channel,
46
+ timeout: timeout,
47
+ }
48
+ end
49
+
50
+ accumulated_messages = []
51
+
52
+ # Notifications often come in batches (especially when a transaction that
53
+ # inserted many jobs commits), so we want to loop and pick up all the
54
+ # received notifications before continuing.
55
+ loop do
56
+ notification_received =
57
+ connection.wait_for_notify(timeout) do |channel, pid, payload|
58
+ # We've received a notification, so zero out the timeout before we
59
+ # loop again to check for another message. This ensures that we
60
+ # don't wait an additional `timeout` seconds after processing the
61
+ # final message before this method returns.
62
+ timeout = 0
63
+
64
+ Que.internal_log(:listener_received_notification, self) do
65
+ {
66
+ channel: channel,
67
+ backend_pid: connection.backend_pid,
68
+ source_pid: pid,
69
+ payload: payload,
70
+ }
71
+ end
72
+
73
+ # Be very defensive about the message we receive - it may not be
74
+ # valid JSON or have the structure we expect.
75
+ next unless message = parse_payload(payload)
76
+
77
+ case message
78
+ when Array then accumulated_messages.concat(message)
79
+ when Hash then accumulated_messages << message
80
+ else raise Error, "Unexpected parse_payload output: #{message.class}"
81
+ end
82
+ end
83
+
84
+ break unless notification_received
85
+ end
86
+
87
+ return accumulated_messages if accumulated_messages.empty?
88
+
89
+ Que.internal_log(:listener_received_messages, self) do
90
+ {
91
+ backend_pid: connection.backend_pid,
92
+ channel: channel,
93
+ messages: accumulated_messages,
94
+ }
95
+ end
96
+
97
+ accumulated_messages.keep_if do |message|
98
+ next unless message.is_a?(Hash)
99
+ next unless type = message[:message_type]
100
+ next unless type.is_a?(String)
101
+ next unless format = MESSAGE_FORMATS[type.to_sym]
102
+
103
+ if message_matches_format?(message, format)
104
+ true
105
+ else
106
+ error_message = [
107
+ "Message of type '#{type}' doesn't match format!",
108
+ # Massage message and format a bit to make these errors more readable.
109
+ "Message: #{Hash[message.reject{|k,v| k == :message_type}.sort_by{|k,v| k}].inspect}",
110
+ "Format: #{Hash[format.sort_by{|k,v| k}].inspect}",
111
+ ].join("\n")
112
+
113
+ Que.notify_error_async(Error.new(error_message))
114
+ false
115
+ end
116
+ end
117
+
118
+ Que.internal_log(:listener_filtered_messages, self) do
119
+ {
120
+ backend_pid: connection.backend_pid,
121
+ channel: channel,
122
+ messages: accumulated_messages,
123
+ }
124
+ end
125
+
126
+ accumulated_messages
127
+ end
128
+
129
+ def unlisten
130
+ # Be sure to drain all notifications so that any code that uses this
131
+ # connection later doesn't receive any nasty surprises.
132
+ connection.execute "UNLISTEN *"
133
+ connection.drain_notifications
134
+
135
+ Que.internal_log :listener_unlisten, self do
136
+ {
137
+ backend_pid: connection.backend_pid,
138
+ channel: channel,
139
+ }
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def parse_payload(payload)
146
+ Que.deserialize_json(payload)
147
+ rescue JSON::ParserError => e
148
+ Que.notify_error_async(e)
149
+ nil
150
+ end
151
+
152
+ def message_matches_format?(message, format)
153
+ message_has_all_keys?(message, format) &&
154
+ message_has_no_excess_keys?(message, format) &&
155
+ message_keys_all_valid?(message, format)
156
+ end
157
+
158
+ def message_has_all_keys?(message, format)
159
+ format.all? { |k,v| message.has_key?(k) }
160
+ end
161
+
162
+ def message_has_no_excess_keys?(message, format)
163
+ message.all? { |k,v| format.has_key?(k) || k == :message_type }
164
+ end
165
+
166
+ def message_keys_all_valid?(message, format)
167
+ message.all? do |key, value|
168
+ if type = format[key]
169
+ Que.assert?(type, value)
170
+ else
171
+ true
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,466 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Locker class encapsulates a thread that is listening/polling for new
4
+ # jobs in the DB, locking them, passing their primary keys to workers, then
5
+ # cleaning up by unlocking them once the workers are done.
6
+
7
+ require 'set'
8
+
9
+ module Que
10
+ Listener::MESSAGE_FORMATS[:job_available] =
11
+ {
12
+ queue: String,
13
+ id: Integer,
14
+ run_at: TIME_REGEX,
15
+ priority: Integer,
16
+ }
17
+
18
+ SQL[:clean_lockers] =
19
+ %{
20
+ DELETE FROM public.que_lockers
21
+ WHERE pid = pg_backend_pid()
22
+ OR pid NOT IN (SELECT pid FROM pg_stat_activity)
23
+ }
24
+
25
+ SQL[:register_locker] =
26
+ %{
27
+ INSERT INTO public.que_lockers
28
+ (
29
+ pid,
30
+ worker_count,
31
+ worker_priorities,
32
+ ruby_pid,
33
+ ruby_hostname,
34
+ listening,
35
+ queues
36
+ )
37
+ VALUES
38
+ (
39
+ pg_backend_pid(),
40
+ $1::integer,
41
+ $2::integer[],
42
+ $3::integer,
43
+ $4::text,
44
+ $5::boolean,
45
+ $6::text[]
46
+ )
47
+ }
48
+
49
+ class Locker
50
+ attr_reader :thread, :workers, :job_cache, :locks
51
+
52
+ MESSAGE_RESOLVERS = {}
53
+ RESULT_RESOLVERS = {}
54
+
55
+ MESSAGE_RESOLVERS[:job_available] =
56
+ -> (messages) {
57
+ metajobs = messages.map { |key| Metajob.new(key) }
58
+ push_jobs(lock_jobs(job_cache.accept?(metajobs)))
59
+ }
60
+
61
+ RESULT_RESOLVERS[:job_finished] =
62
+ -> (messages) { finish_jobs(messages.map{|m| m.fetch(:metajob)}) }
63
+
64
+ DEFAULT_POLL_INTERVAL = 5.0
65
+ DEFAULT_WAIT_PERIOD = 50
66
+ DEFAULT_MINIMUM_QUEUE_SIZE = 2
67
+ DEFAULT_MAXIMUM_QUEUE_SIZE = 8
68
+ DEFAULT_WORKER_COUNT = 6
69
+ DEFAULT_WORKER_PRIORITIES = [10, 30, 50].freeze
70
+
71
+ def initialize(
72
+ queues: [Que.default_queue],
73
+ connection: nil,
74
+ listen: true,
75
+ poll: true,
76
+ poll_interval: DEFAULT_POLL_INTERVAL,
77
+ wait_period: DEFAULT_WAIT_PERIOD,
78
+ maximum_queue_size: DEFAULT_MAXIMUM_QUEUE_SIZE,
79
+ minimum_queue_size: DEFAULT_MINIMUM_QUEUE_SIZE,
80
+ worker_count: DEFAULT_WORKER_COUNT,
81
+ worker_priorities: DEFAULT_WORKER_PRIORITIES,
82
+ on_worker_start: nil
83
+ )
84
+
85
+ # Sanity-check all our arguments, since some users may instantiate Locker
86
+ # directly.
87
+ Que.assert [TrueClass, FalseClass], listen
88
+ Que.assert [TrueClass, FalseClass], poll
89
+
90
+ Que.assert Numeric, poll_interval
91
+ Que.assert Numeric, wait_period
92
+ Que.assert Integer, worker_count
93
+
94
+ Que.assert Array, worker_priorities
95
+ worker_priorities.each { |p| Que.assert(Integer, p) }
96
+
97
+ all_worker_priorities = worker_priorities.values_at(0...worker_count)
98
+
99
+ # We use a JobCache to track jobs and pass them to workers, and a
100
+ # ResultQueue to receive messages from workers.
101
+ @job_cache = JobCache.new(
102
+ maximum_size: maximum_queue_size,
103
+ minimum_size: minimum_queue_size,
104
+ priorities: all_worker_priorities.uniq,
105
+ )
106
+
107
+ @result_queue = ResultQueue.new
108
+
109
+ Que.internal_log :locker_instantiate, self do
110
+ {
111
+ queues: queues,
112
+ listen: listen,
113
+ poll: poll,
114
+ poll_interval: poll_interval,
115
+ wait_period: wait_period,
116
+ maximum_queue_size: maximum_queue_size,
117
+ minimum_queue_size: minimum_queue_size,
118
+ worker_count: worker_count,
119
+ worker_priorities: worker_priorities,
120
+ }
121
+ end
122
+
123
+ # Local cache of which advisory locks are held by this connection.
124
+ @locks = Set.new
125
+
126
+ @queue_names = queues.is_a?(Hash) ? queues.keys : queues
127
+ @wait_period = wait_period.to_f / 1000 # Milliseconds to seconds.
128
+
129
+ # If the worker_count exceeds the array of priorities it'll result in
130
+ # extra workers that will work jobs of any priority. For example, the
131
+ # default worker_count of 6 and the default worker priorities of [10, 30,
132
+ # 50] will result in three workers that only work jobs that meet those
133
+ # priorities, and three workers that will work any job.
134
+ @workers =
135
+ all_worker_priorities.map do |priority|
136
+ Worker.new(
137
+ priority: priority,
138
+ job_cache: @job_cache,
139
+ result_queue: @result_queue,
140
+ start_callback: on_worker_start,
141
+ )
142
+ end
143
+
144
+ # To prevent race conditions, let every worker get into a ready state
145
+ # before starting up the locker thread.
146
+ loop do
147
+ break if job_cache.waiting_count == workers.count
148
+ sleep 0.001
149
+ end
150
+
151
+ pool =
152
+ if connection
153
+ # Wrap the given connection in a dummy connection pool.
154
+ ConnectionPool.new { |&block| block.call(connection) }
155
+ else
156
+ Que.pool
157
+ end
158
+
159
+ @thread =
160
+ Thread.new do
161
+ # An error causing this thread to exit is a bug in Que, which we want
162
+ # to know about ASAP, so propagate the error if it happens.
163
+ Thread.current.abort_on_exception = true
164
+
165
+ # Give this thread priority, so it can promptly respond to NOTIFYs.
166
+ Thread.current.priority = 1
167
+
168
+ pool.checkout do |connection|
169
+ original_application_name =
170
+ connection.
171
+ execute("SHOW application_name").
172
+ first.
173
+ fetch(:application_name)
174
+
175
+ begin
176
+ @connection = connection
177
+
178
+ connection.execute(
179
+ "SELECT set_config('application_name', $1, false)",
180
+ ["Que Locker: #{connection.backend_pid}"]
181
+ )
182
+
183
+ Poller.setup(connection)
184
+
185
+ if listen
186
+ @listener = Listener.new(connection: connection)
187
+ end
188
+
189
+ if poll
190
+ @pollers =
191
+ queues.map do |queue, interval|
192
+ Poller.new(
193
+ connection: connection,
194
+ queue: queue,
195
+ poll_interval: interval || poll_interval,
196
+ )
197
+ end
198
+ end
199
+
200
+ work_loop
201
+ ensure
202
+ connection.execute(
203
+ "SELECT set_config('application_name', $1, false)",
204
+ [original_application_name]
205
+ )
206
+
207
+ Poller.cleanup(connection)
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ def stop!
214
+ stop; wait_for_stop
215
+ end
216
+
217
+ def stop
218
+ @job_cache.stop
219
+ @stop = true
220
+ end
221
+
222
+ def stopping?
223
+ @stop
224
+ end
225
+
226
+ def wait_for_stop
227
+ @thread.join
228
+ end
229
+
230
+ private
231
+
232
+ attr_reader :connection, :pollers
233
+
234
+ def work_loop
235
+ Que.log(
236
+ level: :debug,
237
+ event: :locker_start,
238
+ queues: @queue_names,
239
+ )
240
+
241
+ Que.internal_log :locker_start, self do
242
+ {
243
+ backend_pid: connection.backend_pid,
244
+ worker_priorities: workers.map(&:priority),
245
+ pollers: pollers && pollers.map { |p| [p.queue, p.poll_interval] }
246
+ }
247
+ end
248
+
249
+ begin
250
+ @listener.listen if @listener
251
+
252
+ # A previous locker that didn't exit cleanly may have left behind
253
+ # a bad locker record, so clean up before registering.
254
+ connection.execute :clean_lockers
255
+ connection.execute :register_locker, [
256
+ @workers.count,
257
+ "{#{@workers.map(&:priority).map{|p| p || 'NULL'}.join(',')}}",
258
+ Process.pid,
259
+ CURRENT_HOSTNAME,
260
+ !!@listener,
261
+ "{\"#{@queue_names.join('","')}\"}",
262
+ ]
263
+
264
+ {} while cycle
265
+
266
+ Que.log(
267
+ level: :debug,
268
+ event: :locker_stop,
269
+ )
270
+
271
+ unlock_jobs(@job_cache.clear)
272
+
273
+ @workers.each(&:wait_until_stopped)
274
+
275
+ handle_results
276
+ ensure
277
+ connection.execute :clean_lockers
278
+
279
+ @listener.unlisten if @listener
280
+ end
281
+ end
282
+
283
+ def cycle
284
+ # Poll at the start of a cycle, so that when the worker starts up we can
285
+ # load up the queue with jobs immediately.
286
+ poll
287
+
288
+ # If we got the stop call while we were polling, break before going to
289
+ # sleep.
290
+ return if @stop
291
+
292
+ # The main sleeping part of the cycle. If this is a listening locker, this
293
+ # is where we wait for notifications.
294
+ wait
295
+
296
+ # Manage any job output we got while we were sleeping.
297
+ handle_results
298
+
299
+ # If we haven't gotten the stop signal, cycle again.
300
+ !@stop
301
+ end
302
+
303
+ def poll
304
+ # Only poll when there are pollers to use (that is, when polling is
305
+ # enabled) and when the local queue has dropped below the configured
306
+ # minimum size.
307
+ return unless pollers && job_cache.jobs_needed?
308
+
309
+ pollers.each do |poller|
310
+ priorities = job_cache.available_priorities
311
+ break if priorities.empty?
312
+
313
+ Que.internal_log(:locker_polling, self) { {priorities: priorities, held_locks: @locks.to_a, queue: poller.queue} }
314
+
315
+ if metajobs = poller.poll(priorities: priorities, held_locks: @locks)
316
+ metajobs.each do |metajob|
317
+ mark_id_as_locked(metajob.id)
318
+ end
319
+
320
+ push_jobs(metajobs)
321
+ end
322
+ end
323
+ end
324
+
325
+ def wait
326
+ if @listener
327
+ @listener.wait_for_grouped_messages(@wait_period).each do |type, messages|
328
+ if resolver = MESSAGE_RESOLVERS[type]
329
+ instance_exec messages, &resolver
330
+ else
331
+ raise Error, "Unexpected message type: #{type.inspect}"
332
+ end
333
+ end
334
+ else
335
+ sleep(@wait_period)
336
+ end
337
+ end
338
+
339
+ def handle_results
340
+ messages_by_type =
341
+ @result_queue.clear.group_by{|r| r.fetch(:message_type)}
342
+
343
+ messages_by_type.each do |type, messages|
344
+ if resolver = RESULT_RESOLVERS[type]
345
+ instance_exec messages, &resolver
346
+ else
347
+ raise Error, "Unexpected result type: #{type.inspect}"
348
+ end
349
+ end
350
+ end
351
+
352
+ def lock_jobs(metajobs)
353
+ metajobs.reject! { |m| @locks.include?(m.id) }
354
+ return metajobs if metajobs.empty?
355
+
356
+ ids = metajobs.map{|m| m.id.to_i}
357
+
358
+ Que.internal_log :locker_locking, self do
359
+ {
360
+ backend_pid: connection.backend_pid,
361
+ ids: ids,
362
+ }
363
+ end
364
+
365
+ jobs =
366
+ connection.execute \
367
+ <<-SQL
368
+ WITH jobs AS (
369
+ SELECT * FROM que_jobs WHERE id IN (#{ids.join(', ')})
370
+ )
371
+ SELECT * FROM jobs WHERE pg_try_advisory_lock(id)
372
+ SQL
373
+
374
+ jobs_by_id = {}
375
+
376
+ jobs.each do |job|
377
+ id = job.fetch(:id)
378
+ mark_id_as_locked(id)
379
+ jobs_by_id[id] = job
380
+ end
381
+
382
+ metajobs.keep_if do |metajob|
383
+ if job = jobs_by_id[metajob.id]
384
+ metajob.set_job(job)
385
+ true
386
+ else
387
+ false
388
+ end
389
+ end
390
+ end
391
+
392
+ def push_jobs(metajobs)
393
+ return if metajobs.empty?
394
+
395
+ # First check that the jobs are all still visible/available in the DB.
396
+ ids = metajobs.map(&:id)
397
+
398
+ verified_ids =
399
+ connection.execute(
400
+ <<-SQL
401
+ SELECT id
402
+ FROM public.que_jobs
403
+ WHERE finished_at IS NULL
404
+ AND expired_at IS NULL
405
+ AND id IN (#{ids.join(', ')})
406
+ SQL
407
+ ).map{|h| h[:id]}.to_set
408
+
409
+ good, bad = metajobs.partition{|mj| verified_ids.include?(mj.id)}
410
+
411
+ displaced = @job_cache.push(*good) || []
412
+
413
+ # Unlock any low-importance jobs the new ones may displace.
414
+ if bad.any? || displaced.any?
415
+ unlock_jobs(bad + displaced)
416
+ end
417
+ end
418
+
419
+ def finish_jobs(metajobs)
420
+ unlock_jobs(metajobs)
421
+ end
422
+
423
+ def unlock_jobs(metajobs)
424
+ return if metajobs.empty?
425
+
426
+ # Unclear how untrusted input would get passed to this method, but since
427
+ # we need string interpolation here, make sure we only have integers.
428
+ ids = metajobs.map { |job| job.id.to_i }
429
+
430
+ Que.internal_log :locker_unlocking, self do
431
+ {
432
+ backend_pid: connection.backend_pid,
433
+ ids: ids,
434
+ }
435
+ end
436
+
437
+ values = ids.join('), (')
438
+
439
+ results =
440
+ connection.execute \
441
+ "SELECT pg_advisory_unlock(v.i) FROM (VALUES (#{values})) v (i)"
442
+
443
+ results.each do |result|
444
+ Que.assert(result.fetch(:pg_advisory_unlock)) do
445
+ [
446
+ "Tried to unlock a job we hadn't locked!",
447
+ results.inspect,
448
+ ids.inspect,
449
+ ].join(' ')
450
+ end
451
+ end
452
+
453
+ ids.each do |id|
454
+ Que.assert(@locks.delete?(id)) do
455
+ "Tried to remove a local lock that didn't exist!: #{id}"
456
+ end
457
+ end
458
+ end
459
+
460
+ def mark_id_as_locked(id)
461
+ Que.assert(@locks.add?(id)) do
462
+ "Tried to lock a job that was already locked: #{id}"
463
+ end
464
+ end
465
+ end
466
+ end