exekutor 0.1.0 → 0.1.2

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