exekutor 0.1.0 → 0.1.2

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 (51) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -3
  3. data/exe/exekutor +2 -2
  4. data/lib/active_job/queue_adapters/exekutor_adapter.rb +2 -1
  5. data/lib/exekutor/asynchronous.rb +143 -75
  6. data/lib/exekutor/cleanup.rb +27 -28
  7. data/lib/exekutor/configuration.rb +102 -48
  8. data/lib/exekutor/hook.rb +15 -11
  9. data/lib/exekutor/info/worker.rb +3 -3
  10. data/lib/exekutor/internal/base_record.rb +2 -1
  11. data/lib/exekutor/internal/callbacks.rb +55 -35
  12. data/lib/exekutor/internal/cli/app.rb +33 -23
  13. data/lib/exekutor/internal/cli/application_loader.rb +17 -6
  14. data/lib/exekutor/internal/cli/cleanup.rb +54 -40
  15. data/lib/exekutor/internal/cli/daemon.rb +9 -11
  16. data/lib/exekutor/internal/cli/default_option_value.rb +3 -1
  17. data/lib/exekutor/internal/cli/info.rb +117 -84
  18. data/lib/exekutor/internal/cli/manager.rb +234 -123
  19. data/lib/exekutor/internal/configuration_builder.rb +49 -30
  20. data/lib/exekutor/internal/database_connection.rb +6 -0
  21. data/lib/exekutor/internal/executable.rb +12 -7
  22. data/lib/exekutor/internal/executor.rb +50 -21
  23. data/lib/exekutor/internal/hooks.rb +11 -8
  24. data/lib/exekutor/internal/listener.rb +85 -43
  25. data/lib/exekutor/internal/logger.rb +29 -10
  26. data/lib/exekutor/internal/provider.rb +96 -77
  27. data/lib/exekutor/internal/reserver.rb +66 -19
  28. data/lib/exekutor/internal/status_server.rb +87 -54
  29. data/lib/exekutor/job.rb +1 -1
  30. data/lib/exekutor/job_error.rb +1 -1
  31. data/lib/exekutor/job_options.rb +22 -13
  32. data/lib/exekutor/plugins/appsignal.rb +7 -5
  33. data/lib/exekutor/plugins.rb +8 -4
  34. data/lib/exekutor/queue.rb +69 -30
  35. data/lib/exekutor/version.rb +1 -1
  36. data/lib/exekutor/worker.rb +89 -48
  37. data/lib/exekutor.rb +2 -2
  38. data/lib/generators/exekutor/configuration_generator.rb +11 -6
  39. data/lib/generators/exekutor/install_generator.rb +24 -15
  40. data/lib/generators/exekutor/templates/install/functions/exekutor_broadcast_job_enqueued.sql +10 -0
  41. data/lib/generators/exekutor/templates/install/functions/exekutor_requeue_orphaned_jobs.sql +11 -0
  42. data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +23 -22
  43. data/lib/generators/exekutor/templates/install/triggers/exekutor_broadcast_job_enqueued.sql +7 -0
  44. data/lib/generators/exekutor/templates/install/triggers/exekutor_requeue_orphaned_jobs.sql +5 -0
  45. data.tar.gz.sig +0 -0
  46. metadata +67 -23
  47. metadata.gz.sig +0 -0
  48. data/lib/generators/exekutor/templates/install/functions/job_notifier.sql +0 -7
  49. data/lib/generators/exekutor/templates/install/functions/requeue_orphaned_jobs.sql +0 -7
  50. data/lib/generators/exekutor/templates/install/triggers/notify_workers.sql +0 -6
  51. data/lib/generators/exekutor/templates/install/triggers/requeue_orphaned_jobs.sql +0 -5
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rainbow"
2
4
 
5
+ # The Exekutor namespace
3
6
  module Exekutor
4
7
  # @private
5
8
  module Internal
@@ -9,7 +12,7 @@ module Exekutor
9
12
 
10
13
  included do
11
14
  # The log tags to use when writing to the log
12
- mattr_accessor :log_tags, default: [self.name.demodulize]
15
+ mattr_accessor :log_tags, default: [name.demodulize]
13
16
  end
14
17
 
15
18
  protected
@@ -35,25 +38,24 @@ module Exekutor
35
38
  # Prints a message to STDOUT, unless {Exekutor::Configuration#quiet?} is true
36
39
  # @private
37
40
  def self.say(*args)
38
- puts(*args) unless config.quiet?
41
+ puts(*args) unless config.quiet? # rubocop:disable Rails/Output
39
42
  end
40
43
 
41
44
  # Prints the error in the log and to STDERR (unless {Exekutor::Configuration#quiet?} is true)
42
- # @param err [Exception] the error to print
45
+ # @param error [Exception] the error to print
43
46
  # @param message [String] the message to print above the error
44
47
  # @return [void]
45
- def self.print_error(err, message = nil)
46
- @backtrace_cleaner ||= ActiveSupport::BacktraceCleaner.new
47
- error = "#{err.class} – #{err.message}\nat #{@backtrace_cleaner.clean(err.backtrace).join("\n ")}"
48
-
48
+ def self.print_error(error, message = nil)
49
+ error = strferr(error)
49
50
  unless config.quiet?
50
- $stderr.puts Rainbow(message).bright.red if message
51
- $stderr.puts Rainbow(error).red
51
+ warn Rainbow(message).bright.red if message
52
+ warn Rainbow(error).red
52
53
  end
53
- unless ActiveSupport::Logger.logger_outputs_to?(logger, $stdout)
54
+ if config.quiet? || !ActiveSupport::Logger.logger_outputs_to?(logger, $stdout)
54
55
  logger.error message if message
55
56
  logger.error error
56
57
  end
58
+ nil
57
59
  end
58
60
 
59
61
  # Gets the logger
@@ -71,4 +73,21 @@ module Exekutor
71
73
  ActiveSupport::TaggedLogging.new logger
72
74
  end
73
75
  end
76
+
77
+ # @return [ActiveSupport::BacktraceCleaner] A cleaner for error backtraces
78
+ def self.backtrace_cleaner
79
+ @backtrace_cleaner ||= ActiveSupport::BacktraceCleaner.new
80
+ end
81
+
82
+ # @return [String] The given error class, message, and cleaned backtrace as a string
83
+ def self.strferr(err)
84
+ raise ArgumentError, "err must not be nil" if err.nil?
85
+ return err if err.is_a? String
86
+
87
+ "#{err.class} – #{err.message}\nat #{
88
+ err.backtrace ? backtrace_cleaner.clean(err.backtrace).join("\n ") : "unknown location"
89
+ }"
90
+ end
91
+
92
+ private_class_method :backtrace_cleaner, :strferr
74
93
  end
@@ -8,31 +8,33 @@ module Exekutor
8
8
  module Internal
9
9
  # Reserves jobs and provides them to an executor
10
10
  class Provider
11
- include Executable, Callbacks, Logger
11
+ include Logger
12
+ include Callbacks
13
+ include Executable
12
14
 
13
15
  define_callbacks :on_queue_empty, freeze: true
14
16
 
15
17
  # Represents an unknown value
16
18
  UNKNOWN = Object.new.freeze
17
- private_constant "UNKNOWN"
19
+ private_constant :UNKNOWN
18
20
 
19
21
  MAX_WAIT_TIMEOUT = 300
20
- private_constant "MAX_WAIT_TIMEOUT"
22
+ private_constant :MAX_WAIT_TIMEOUT
21
23
 
22
24
  # Creates a new provider
23
25
  # @param reserver [Reserver] the job reserver
24
26
  # @param executor [Executor] the job executor
25
27
  # @param pool [ThreadPoolExecutor] the thread pool to use
26
- # @param polling_interval [Integer] the polling interval
28
+ # @param polling_interval [ActiveSupport::Duration] the polling interval
27
29
  # @param interval_jitter [Float] the polling interval jitter
28
- def initialize(reserver:, executor:, pool:, polling_interval: 60,
30
+ def initialize(reserver:, executor:, pool:, polling_interval: 60.seconds,
29
31
  interval_jitter: polling_interval.to_i > 1 ? polling_interval * 0.1 : 0)
30
32
  super()
31
33
  @reserver = reserver
32
34
  @executor = executor
33
35
  @pool = pool
34
36
 
35
- @polling_interval = polling_interval.freeze
37
+ @polling_interval = polling_interval.to_i.freeze
36
38
  @interval_jitter = interval_jitter.to_f.freeze
37
39
 
38
40
  @event = Concurrent::Event.new
@@ -48,14 +50,14 @@ module Exekutor
48
50
 
49
51
  # Always poll at startup to fill up threads, use small jitter so workers started at the same time dont hit
50
52
  # the db at the same time
51
- @next_poll_at.set (1 + 2 * Kernel.rand).second.from_now
53
+ @next_poll_at.set (1 + (2 * Kernel.rand)).seconds.from_now.to_f
52
54
  start_thread
53
55
  true
54
56
  end
55
57
 
56
58
  # Stops the provider
57
59
  def stop
58
- set_state :stopped
60
+ self.state = :stopped
59
61
  @event.set
60
62
  end
61
63
 
@@ -63,7 +65,7 @@ module Exekutor
63
65
  def poll
64
66
  raise Exekutor::Error, "Provider is not running" unless running?
65
67
 
66
- @next_poll_at.set Time.now
68
+ @next_poll_at.set Time.now.to_f
67
69
  @event.set
68
70
  end
69
71
 
@@ -71,51 +73,39 @@ module Exekutor
71
73
  # argument is given. Updates the timestamp for the earliest job is a timestamp is given and that timestamp is
72
74
  # before the known timestamp. Does nothing if a timestamp is given and the earliest job timestamp is not known.
73
75
  # @param scheduled_at [Time,Numeric] the time a job is scheduled at
74
- # @return [Time] the timestamp for the next job, or +nil+ if the timestamp is unknown or no jobs are pending
76
+ # @return [Float,nil] the timestamp for the next job, or +nil+ if the timestamp is unknown or no jobs are pending
75
77
  def update_earliest_scheduled_at(scheduled_at = UNKNOWN)
76
- overwrite_unknown = false
77
- case scheduled_at
78
- when UNKNOWN
79
- # If we fetch the value from the DB, we can safely overwrite the UNKNOWN value
80
- overwrite_unknown = true
81
- scheduled_at = @reserver.earliest_scheduled_at
82
- when Numeric
83
- scheduled_at = Time.at(scheduled_at)
84
- when Time
85
- # All good
86
- else
78
+ scheduled_at = scheduled_at.to_f if scheduled_at.is_a? Time
79
+ unless scheduled_at == UNKNOWN || scheduled_at.is_a?(Numeric)
87
80
  raise ArgumentError, "scheduled_at must be a Time or Numeric"
88
81
  end
89
82
 
90
- updated = false
91
- scheduled_at = @next_job_scheduled_at.update do |current|
92
- if current == UNKNOWN
93
- if overwrite_unknown || scheduled_at <= Time.now
94
- updated = true
95
- scheduled_at
96
- else
97
- current
98
- end
99
- elsif current.nil? || scheduled_at.nil? || current > scheduled_at
100
- updated = true
101
- scheduled_at
102
- else
103
- current
104
- end
83
+ changed = false
84
+ earliest_scheduled_at = @next_job_scheduled_at.update do |current|
85
+ earliest = ScheduledAtComparer.determine_earliest(current, scheduled_at, @reserver)
86
+ changed = earliest != current
87
+ earliest
105
88
  end
106
- if scheduled_at == UNKNOWN
89
+
90
+ if earliest_scheduled_at == UNKNOWN
107
91
  nil
108
92
  else
109
- @event.set if updated && scheduled_at.present?
110
- scheduled_at
93
+ @event.set if changed && earliest_scheduled_at.present?
94
+ earliest_scheduled_at
111
95
  end
112
96
  end
113
97
 
114
98
  private
115
99
 
116
100
  # Starts the provision thread
117
- def start_thread
118
- @pool.post(&method(:run)) if running?
101
+ def start_thread(delay: nil)
102
+ return unless running?
103
+
104
+ if delay
105
+ Concurrent::ScheduledTask.execute(delay, executor: @pool) { run }
106
+ else
107
+ @pool.post { run }
108
+ end
119
109
  end
120
110
 
121
111
  # Does the provisioning of jobs to the executor. Blocks until the provider is stopped.
@@ -134,19 +124,24 @@ module Exekutor
134
124
  consecutive_errors.value = 0
135
125
  end
136
126
  end
137
- rescue StandardError => err
138
- Exekutor.on_fatal_error err, "[Provider] Runtime error!"
139
- consecutive_errors.increment
140
- if running?
141
- delay = restart_delay
142
- logger.info "Restarting in %0.1f seconds…" % [delay]
143
- Concurrent::ScheduledTask.execute(delay, executor: @pool, &method(:run))
144
- end
127
+ rescue StandardError => e
128
+ on_thread_error(e)
145
129
  ensure
146
130
  BaseRecord.connection_pool.release_connection
147
131
  @thread_running.make_false
148
132
  end
149
133
 
134
+ # Called when an error is raised in #run
135
+ def on_thread_error(error)
136
+ Exekutor.on_fatal_error error, "[Provider] Runtime error!"
137
+ return unless running?
138
+
139
+ consecutive_errors.increment
140
+ delay = restart_delay
141
+ logger.info format("Restarting in %0.1f seconds…", delay)
142
+ start_thread delay: delay
143
+ end
144
+
150
145
  # Waits for any event to happen. An event could be:
151
146
  # - The listener was notified of a new job;
152
147
  # - The next job is scheduled for the current time;
@@ -157,8 +152,8 @@ module Exekutor
157
152
  return unless timeout.positive?
158
153
 
159
154
  @event.wait timeout
160
- rescue StandardError => err
161
- Exekutor.on_fatal_error err, "[Provider] An error occurred while waiting"
155
+ rescue StandardError => e
156
+ Exekutor.on_fatal_error e, "[Provider] An error occurred while waiting"
162
157
  sleep 0.1 if running?
163
158
  ensure
164
159
  throw :shutdown unless running?
@@ -171,21 +166,8 @@ module Exekutor
171
166
  return unless available_workers.positive?
172
167
 
173
168
  jobs = @reserver.reserve available_workers
174
- unless jobs.nil?
175
- begin
176
- logger.debug "Reserved #{jobs.size} job(s)"
177
- jobs.each(&@executor.method(:post))
178
- rescue Exception # rubocop:disable Lint/RescueException
179
- # Try to release all jobs before re-raising
180
- begin
181
- Exekutor::Job.where(id: jobs.collect { |job| job[:id] }, status: "e")
182
- .update_all(status: "p", worker_id: nil)
183
- rescue # rubocop:disable Lint/RescueStandardError
184
- # ignored
185
- end
186
- raise
187
- end
188
- end
169
+ execute_jobs jobs unless jobs.nil?
170
+
189
171
  if jobs.nil? || jobs.size.to_i < available_workers
190
172
  # If we ran out of work, update the earliest scheduled at
191
173
  update_earliest_scheduled_at
@@ -194,8 +176,24 @@ module Exekutor
194
176
 
195
177
  elsif @next_job_scheduled_at.get == UNKNOWN
196
178
  # If the next job timestamp is still unknown, set it to now to indicate there's still work to do
197
- @next_job_scheduled_at.set Time.now
179
+ @next_job_scheduled_at.set Time.now.to_f
180
+ end
181
+ end
182
+
183
+ # Posts the given jobs to the executor thread pool
184
+ # @param jobs [Array] the jobs to execute
185
+ def execute_jobs(jobs)
186
+ logger.debug "Reserved #{jobs.size} job(s)"
187
+ jobs.each { |job| @executor.post(job) }
188
+ rescue Exception # rubocop:disable Lint/RescueException
189
+ # Try to release all jobs before re-raising
190
+ begin
191
+ Exekutor::Job.where(id: jobs.pluck(:id), status: "e")
192
+ .update_all(status: "p", worker_id: nil)
193
+ rescue StandardError
194
+ # ignored
198
195
  end
196
+ raise
199
197
  end
200
198
 
201
199
  def perform_pending_job_updates
@@ -224,7 +222,7 @@ module Exekutor
224
222
  return if jobs&.size.to_i.zero?
225
223
 
226
224
  logger.info "Restarting #{jobs.size} abandoned job#{"s" if jobs.size > 1}"
227
- jobs.each(&@executor.method(:post))
225
+ jobs.each { |job| @executor.post(job) }
228
226
  end
229
227
 
230
228
  # @return [Boolean] Whether the polling is enabled. Ie. whether a polling interval is set.
@@ -232,7 +230,7 @@ module Exekutor
232
230
  @polling_interval.present?
233
231
  end
234
232
 
235
- # @return [Time,nil] the 'scheduled at' value for the next job, or nil if unknown or if there is no pending job
233
+ # @return [Float,nil] the 'scheduled at' value for the next job, or nil if unknown or if there is no pending job
236
234
  def next_job_scheduled_at
237
235
  at = @next_job_scheduled_at.get
238
236
  if at == UNKNOWN
@@ -243,10 +241,10 @@ module Exekutor
243
241
  end
244
242
  end
245
243
 
246
- # @return [Time,nil] When the next poll is scheduled, or nil if polling is disabled
244
+ # @return [Float,nil] When the next poll is scheduled, or nil if polling is disabled
247
245
  def next_poll_scheduled_at
248
246
  if polling_enabled?
249
- @next_poll_at.update { |planned_at| planned_at || Time.now + polling_interval }
247
+ @next_poll_at.update { |planned_at| planned_at || (Time.now.to_f + polling_interval) }
250
248
  else
251
249
  # noinspection RubyMismatchedReturnType
252
250
  @next_poll_at.get
@@ -262,9 +260,9 @@ module Exekutor
262
260
 
263
261
  timeout = [MAX_WAIT_TIMEOUT].tap do |timeouts|
264
262
  # noinspection RubyMismatchedArgumentType
265
- timeouts.append next_job_at - Time.now if next_job_at
263
+ timeouts.append next_job_at - Time.now.to_f if next_job_at
266
264
  # noinspection RubyMismatchedArgumentType
267
- timeouts.append next_poll_at - Time.now if next_poll_at
265
+ timeouts.append next_poll_at - Time.now.to_f if next_poll_at
268
266
  end.min
269
267
 
270
268
  if timeout <= 0.001
@@ -278,13 +276,13 @@ module Exekutor
278
276
  # @return [Boolean] Whether the `reserver` should be called.
279
277
  def reserve_jobs_now?
280
278
  next_poll_at = next_poll_scheduled_at
281
- if next_poll_at && next_poll_at - Time.now <= 0.001
282
- @next_poll_at.update { Time.now + polling_interval if polling_enabled? }
279
+ if next_poll_at && next_poll_at - Time.now.to_f <= 0.001
280
+ @next_poll_at.update { Time.now.to_f + polling_interval if polling_enabled? }
283
281
  return true
284
282
  end
285
283
 
286
284
  next_job_at = next_job_scheduled_at
287
- next_job_at && next_job_at <= Time.now
285
+ next_job_at && next_job_at <= Time.now.to_f
288
286
  end
289
287
 
290
288
  # @return [Float] Gets the polling interval jitter
@@ -295,7 +293,7 @@ module Exekutor
295
293
  # Get the polling interval. If a jitter is configured, the interval is reduced or increased by `0.5 * jitter`.
296
294
  # @return [Float] The amount of seconds before the next poll
297
295
  def polling_interval
298
- raise "Polling is disabled" unless @polling_interval.present?
296
+ raise "Polling is disabled" if @polling_interval.blank?
299
297
 
300
298
  @polling_interval + if polling_interval_jitter.zero?
301
299
  0
@@ -303,6 +301,27 @@ module Exekutor
303
301
  (Kernel.rand - 0.5) * polling_interval_jitter
304
302
  end
305
303
  end
304
+
305
+ # Abstraction to determine earliest out of 2 scheduled at timestamps
306
+ class ScheduledAtComparer
307
+ def self.determine_earliest(current_value, other_value, reserver)
308
+ if other_value == UNKNOWN
309
+ # Fetch the value from the db
310
+ reserver.earliest_scheduled_at&.to_f
311
+ elsif current_value == UNKNOWN
312
+ # We don't know the value yet, don't use the given value as earliest unless work is scheduled immediately
313
+ other_value > Time.now.to_f ? UNKNOWN : other_value
314
+ elsif current_value.nil? || current_value > other_value
315
+ # The given value is earlier than the known value
316
+ other_value
317
+ else
318
+ # No changes
319
+ current_value
320
+ end
321
+ end
322
+ end
323
+
324
+ private_constant :ScheduledAtComparer
306
325
  end
307
326
  end
308
327
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Exekutor
3
4
  # @private
4
5
  module Internal
@@ -10,23 +11,23 @@ module Exekutor
10
11
  # Creates a new Reserver
11
12
  # @param worker_id [String] the id of the worker
12
13
  # @param queues [Array<String>] the queues to watch
13
- def initialize(worker_id, queues)
14
+ def initialize(worker_id, queues: nil, min_priority: nil, max_priority: nil)
14
15
  @worker_id = worker_id
15
- @queue_filter_sql = build_queue_filter_sql(queues)
16
+ @reserve_filter_sql = build_filter_sql(queues: queues, min_priority: min_priority, max_priority: max_priority)
16
17
  @json_serializer = Exekutor.config.load_json_serializer
17
18
  end
18
19
 
19
20
  # Reserves pending jobs
20
21
  # @param limit [Integer] the number of jobs to reserve
21
- # @return [Array<Job>,nil] the reserved jobs, or nil if no jobs were reserved
22
+ # @return [Array<Hash>,nil] the reserved jobs, or nil if no jobs were reserved
22
23
  def reserve(limit)
23
24
  return unless limit.positive?
24
25
 
25
26
  results = Exekutor::Job.connection.exec_query <<~SQL, ACTION_NAME, [@worker_id, limit], prepare: true
26
27
  UPDATE exekutor_jobs SET worker_id = $1, status = 'e' WHERE id IN (
27
28
  SELECT id FROM exekutor_jobs
28
- WHERE scheduled_at <= now() AND "status"='p' #{@queue_filter_sql}
29
- ORDER BY priority, scheduled_at, enqueued_at
29
+ WHERE scheduled_at <= now() AND "status"='p'#{" AND #{@reserve_filter_sql}" if @reserve_filter_sql}
30
+ ORDER BY priority#{" DESC" if Exekutor.config.inverse_priority?}, scheduled_at, enqueued_at
30
31
  FOR UPDATE SKIP LOCKED
31
32
  LIMIT $2
32
33
  ) RETURNING "id", "payload", "options", "scheduled_at"
@@ -36,6 +37,9 @@ module Exekutor
36
37
  parse_jobs results
37
38
  end
38
39
 
40
+ # Gets the jobs that are assigned to this worker and have an id that is not included in +active_job_ids+
41
+ # @param active_job_ids [Array<String>] The ids of the jobs that should be excluded
42
+ # @return [Array<Hash>] the jobs
39
43
  def get_abandoned_jobs(active_job_ids)
40
44
  jobs = Exekutor::Job.executing.where(worker_id: @worker_id)
41
45
  jobs = jobs.where.not(id: active_job_ids) if active_job_ids.present?
@@ -47,7 +51,7 @@ module Exekutor
47
51
  # @return [Time,nil] The earliest scheduled at, or nil if the queues are empty
48
52
  def earliest_scheduled_at
49
53
  jobs = Exekutor::Job.pending
50
- jobs.where! @queue_filter_sql.gsub(/^\s*AND\s+/, "") unless @queue_filter_sql.nil?
54
+ jobs.where! @reserve_filter_sql unless @reserve_filter_sql.nil?
51
55
  jobs.minimum(:scheduled_at)
52
56
  end
53
57
 
@@ -59,7 +63,7 @@ module Exekutor
59
63
  { id: result["id"],
60
64
  payload: parse_json(result["payload"]),
61
65
  options: parse_json(result["options"]),
62
- scheduled_at: result['scheduled_at'] }
66
+ scheduled_at: result["scheduled_at"] }
63
67
  end
64
68
  end
65
69
 
@@ -68,28 +72,71 @@ module Exekutor
68
72
  @json_serializer.load str unless str.nil?
69
73
  end
70
74
 
75
+ # Builds SQL filter for the given queues and priorities
76
+ def build_filter_sql(queues:, min_priority:, max_priority:)
77
+ filters = [
78
+ build_queue_filter_sql(queues),
79
+ build_priority_filter_sql(min_priority, max_priority)
80
+ ]
81
+ filters.compact!
82
+ filters.join(" AND ") unless filters.empty?
83
+ end
84
+
71
85
  # Builds SQL filter for the given queues
72
86
  def build_queue_filter_sql(queues)
73
87
  return nil if queues.nil? || (queues.is_a?(Array) && queues.empty?)
74
- unless queues.is_a?(String) || queues.is_a?(Symbol) || queues.is_a?(Array)
75
- raise ArgumentError, "queues must be nil, a String, Symbol, or an array of Strings or Symbols"
76
- end
77
88
 
78
89
  queues = queues.first if queues.is_a?(Array) && queues.one?
79
- if queues.is_a? Array
80
- unless queues.all? { |q| (q.is_a?(String) || q.is_a?(Symbol)) && !q.blank? }
81
- raise ArgumentError, "queues contains an invalid value"
82
- end
90
+ validate_queues! queues
83
91
 
84
- Exekutor::Job.sanitize_sql_for_conditions(["AND queue IN (?)", queues])
85
- else
86
- raise ArgumentError, "queue name cannot be empty" if queues.blank?
92
+ conditions = if queues.is_a? Array
93
+ ["queue IN (?)", queues]
94
+ else
95
+ ["queue = ?", queues]
96
+ end
97
+ Exekutor::Job.sanitize_sql_for_conditions conditions
98
+ end
99
+
100
+ # Builds SQL filter for the given priorities
101
+ def build_priority_filter_sql(minimum, maximum)
102
+ minimum = coerce_priority(minimum)
103
+ maximum = coerce_priority(maximum)
87
104
 
88
- Exekutor::Job.sanitize_sql_for_conditions(["AND queue = ?", queues])
105
+ conditions = if minimum && maximum
106
+ ["priority BETWEEN ? AND ?", minimum, maximum]
107
+ elsif minimum
108
+ ["priority >= ?", minimum]
109
+ elsif maximum
110
+ ["priority <= ?", maximum]
111
+ end
112
+ Exekutor::Job.sanitize_sql_for_conditions conditions if conditions
113
+ end
114
+
115
+ # @return [Integer,nil] returns nil unless +priority+ is between 1 and 32,766
116
+ def coerce_priority(priority)
117
+ priority if priority && (1..32_766).cover?(priority)
118
+ end
119
+
120
+ # Raises an error if the queues value is invalid
121
+ # @param queues [String,Symbol,Array<String,Symbol>] the queues to validate
122
+ # @raise [ArgumentError] if the queue is invalid or includes an invalid value
123
+ def validate_queues!(queues)
124
+ case queues
125
+ when Array
126
+ raise ArgumentError, "queues contains an invalid value" unless queues.all? { |queue| valid_queue_name? queue }
127
+ when String, Symbol
128
+ raise ArgumentError, "queue name cannot be empty" unless valid_queue_name? queues
129
+ else
130
+ raise ArgumentError,
131
+ "queues must be nil, a String, Symbol, or an array of Strings or Symbols (Actual: #{queues.class})"
89
132
  end
90
133
  end
91
134
 
135
+ # @param queue [String,Symbol] the name of a queue
136
+ # @return [Boolean] whether the name is a valid queue name
137
+ def valid_queue_name?(queue)
138
+ (queue.is_a?(String) || queue.is_a?(Symbol)) && queue.present? && queue.length <= Queue::MAX_NAME_LENGTH
139
+ end
92
140
  end
93
141
  end
94
142
  end
95
-