exekutor 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/exekutor/configuration.rb +55 -24
  4. data/lib/exekutor/internal/cli/app.rb +3 -1
  5. data/lib/exekutor/internal/cli/info.rb +1 -1
  6. data/lib/exekutor/internal/cli/manager.rb +79 -35
  7. data/lib/exekutor/internal/configuration_builder.rb +13 -7
  8. data/lib/exekutor/internal/listener.rb +22 -7
  9. data/lib/exekutor/internal/logger.rb +1 -0
  10. data/lib/exekutor/internal/provider.rb +3 -3
  11. data/lib/exekutor/internal/reserver.rb +43 -11
  12. data/lib/exekutor/internal/status_server.rb +9 -8
  13. data/lib/exekutor/queue.rb +40 -19
  14. data/lib/exekutor/version.rb +1 -1
  15. data/lib/exekutor/worker.rb +6 -6
  16. data/lib/generators/exekutor/configuration_generator.rb +4 -3
  17. data/lib/generators/exekutor/install_generator.rb +3 -5
  18. data/lib/generators/exekutor/templates/install/functions/exekutor_broadcast_job_enqueued.sql +10 -0
  19. data/lib/generators/exekutor/templates/install/functions/exekutor_requeue_orphaned_jobs.sql +11 -0
  20. data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +12 -12
  21. data/lib/generators/exekutor/templates/install/triggers/exekutor_broadcast_job_enqueued.sql +7 -0
  22. data/lib/generators/exekutor/templates/install/triggers/exekutor_requeue_orphaned_jobs.sql +5 -0
  23. data.tar.gz.sig +0 -0
  24. metadata +6 -6
  25. metadata.gz.sig +0 -0
  26. data/lib/generators/exekutor/templates/install/functions/job_notifier.sql +0 -7
  27. data/lib/generators/exekutor/templates/install/functions/requeue_orphaned_jobs.sql +0 -7
  28. data/lib/generators/exekutor/templates/install/triggers/notify_workers.sql +0 -6
  29. data/lib/generators/exekutor/templates/install/triggers/requeue_orphaned_jobs.sql +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94b3158b3e0df0ca892836df74beb23ccfd8bddf5a1377a46b5ebd881cb5bee8
4
- data.tar.gz: a24e415df40ba9bf69d6972da2fb6461f60c655976d81f87ff36f2e365da42a6
3
+ metadata.gz: d5b178052eb277168916a76f663ba53882204481f740a7c30bc309d243de79cf
4
+ data.tar.gz: bfb648002d86bf278b41b5f7ca6fde0f1d3cb39954c445eec0e268bf72120efe
5
5
  SHA512:
6
- metadata.gz: 8de1e356a03b80f1e7dd82802956902531f76111cbea7210ee61eeeb34dbccc22120e5127f24ec9112328bfcb0f175f593f7ab4a9e6f347165b168503c4dbf49
7
- data.tar.gz: 33c945b0e29d611d2e25d98985e75df68494bb3463d014efda5a60c7402acb711541a465b6e4db10afcbb8f2db4b86d9d2c2fd3d6b23a140bf324eefbeb8cf51
6
+ metadata.gz: e2d7a7787f83dc362aeca814d511d0584e5e3e2af24209f1bfc00bd1efbae47d9226bc91e5615a0c7b38b5a515ad64146ec43d74e6cb3b757f3144fcc100d05e
7
+ data.tar.gz: 651ee14b261990adfe498e017afd6ea0e3e382e12f7bf4a352d01dd4d203938f62f2223159e7930bd517ee5fd68f191b2d0ad9189c13670158064e8381278b5b
checksums.yaml.gz.sig CHANGED
Binary file
@@ -180,18 +180,19 @@ module Exekutor
180
180
 
181
181
  # @!macro
182
182
  # @!method $1
183
- # The polling interval in seconds. When set, the worker will poll the database with this interval to check for
183
+ # The polling interval. When set, the worker will poll the database with this interval to check for
184
184
  # any pending jobs that a listener might have missed (if enabled).
185
185
  # === Default value:
186
- # 60
187
- # @return [Integer]
186
+ # 60 seconds
187
+ # @return [ActiveSupport::Duration]
188
188
  # @!method $1=(value)
189
- # Sets the polling interval in seconds. Set to +nil+ to disable polling. If the listener is disabled, this value
190
- # should be reasonably low so jobs don't have to wait in the queue too long; if it is enabled, this value can
191
- # be reasonably high.
192
- # @param value [Integer] the interval
189
+ # Sets the polling interval. Set to +nil+ to disable polling. If the listener is disabled, this value
190
+ # should be reasonably low so jobs don't have to wait in the queue too long; if the listener is enabled, this
191
+ # value can be reasonably high.
192
+ # @param value [ActiveSupport::Duration] the interval
193
193
  # @return [self]
194
- define_option :polling_interval, default: 60, type: [Integer, nil], range: 1...(1.day.to_i)
194
+ define_option :polling_interval, default: 1.minute, type: [ActiveSupport::Duration, nil],
195
+ range: (1.second)...(1.day)
195
196
 
196
197
  # @!macro
197
198
  # @!method $1
@@ -239,21 +240,22 @@ module Exekutor
239
240
 
240
241
  # @!macro
241
242
  # @!method $1
242
- # The maximum number of seconds a thread may be idle before being stopped.
243
+ # The maximum duration a thread may be idle before being stopped.
243
244
  # === Default value:
244
- # 60
245
- # @return [Integer]
245
+ # 60 seconds
246
+ # @return [ActiveSupport::Duration]
246
247
  # @!method $1=(value)
247
- # Sets the maximum number of seconds a thread may be idle before being stopped
248
- # @param value [Integer] the number of threads
248
+ # Sets the maximum duration a thread may be idle before being stopped
249
+ # @param value [ActiveSupport::Duration] the number of threads
249
250
  # @return [self]
250
- define_option :max_execution_thread_idletime, default: 60, type: Integer, range: 1..(1.day.to_i)
251
+ define_option :max_execution_thread_idletime, default: 1.minute, type: ActiveSupport::Duration,
252
+ range: (1.second)..(1.day)
251
253
 
252
254
  # @!macro
253
255
  # @!method $1?
254
256
  # The rack handler for the status server
255
257
  # === Default value:
256
- # webrick
258
+ # +"webrick"+
257
259
  # @return [String]
258
260
  # @!method $1=(value)
259
261
  # Sets the rack handler for the status server. The handler should respond to +#shutdown+ or +#stop+.
@@ -263,18 +265,46 @@ module Exekutor
263
265
 
264
266
  # @!macro
265
267
  # @!method $1?
266
- # The heartbeat timeout for the `/live` endpoint of the status server, in minutes. If the heartbeat of a worker
268
+ # The port number for the status server
269
+ # === Default value:
270
+ # +nil+ (ie. the status server is disabled)
271
+ # @return [Integer]
272
+ # @!method $1=(value)
273
+ # Sets the port number for the status server.
274
+ # @param value [Integer] the port number
275
+ # @return [self]
276
+ define_option :status_server_port, default: nil, type: Integer
277
+
278
+ # @!macro
279
+ # @!method $1?
280
+ # The heartbeat timeout for the `/live` endpoint of the status server. If the heartbeat of a worker
267
281
  # is older than this timeout, the status server will respond with a 503 status indicating the service is
268
282
  # down.
269
283
  # === Default value:
270
- # 30
271
- # @return [Integer]
284
+ # 30 minutes
285
+ # @return [ActiveSupport::Duration]
286
+ # @!method $1=(value)
287
+ # Sets the heartbeat timeout for the `/live` endpoint of the status server. Must be between 1 minute
288
+ # and 24 hours.
289
+ # @param value [ActiveSupport::Duration] The timeout in minutes
290
+ # @return [self]
291
+ define_option :healthcheck_timeout, default: 30.minutes, type: ActiveSupport::Duration,
292
+ range: (1.minute)..(1.day)
293
+
294
+ # @!macro
295
+ # @!method $1?
296
+ # Whether the priority should be inverted. If true, the job with the highest value as the priority is the most
297
+ # important. If false, the job with the lowest value as the priority is the most important.
298
+ # === Default value:
299
+ # false
300
+ # @return [Boolean]
272
301
  # @!method $1=(value)
273
- # Sets the heartbeat timeout for the `/live` endpoint of the status server, in minutes. Must be between 2
274
- # and 1440 (24 hours).
275
- # @param value [Integer] The timeout in minutes
302
+ # Sets whether the the priority should be inverted. When true, the job with the highest value as the priority is
303
+ # the most important; when false, the job with the lowest value as the priority is the most important.
304
+ # @param value [Boolean] whether the job with the highest priority value is the most important.
276
305
  # @return [self]
277
- define_option :healthcheck_timeout, default: 30, type: Integer, range: 2..1440
306
+ define_option :inverse_priority, reader: :inverse_priority?, type: [TrueClass, FalseClass], required: true,
307
+ default: false
278
308
 
279
309
  # @!macro
280
310
  # @!method $1?
@@ -294,13 +324,14 @@ module Exekutor
294
324
  {
295
325
  min_threads: min_execution_threads,
296
326
  max_threads: max_execution_threads,
297
- max_thread_idletime: max_execution_thread_idletime
327
+ max_thread_idletime: max_execution_thread_idletime.to_f
298
328
  }.tap do |opts|
299
329
  opts[:set_db_connection_name] = set_db_connection_name? unless set_db_connection_name.nil?
300
330
  %i[enable_listener delete_completed_jobs delete_discarded_jobs delete_failed_jobs].each do |option|
301
331
  opts[option] = send(:"#{option}?") ? true : false
302
332
  end
303
- %i[polling_interval polling_jitter status_server_handler healthcheck_timeout].each do |option|
333
+ %i[polling_interval polling_jitter status_server_handler status_server_port healthcheck_timeout]
334
+ .each do |option|
304
335
  opts[option] = send(option)
305
336
  end
306
337
  end
@@ -35,9 +35,11 @@ module Exekutor
35
35
  cmd.flag %i[env environment], desc: "The Rails environment"
36
36
  cmd.flag %i[q queue], default_value: Manager::DEFAULT_QUEUE, multiple: true,
37
37
  desc: "Queue to work from"
38
+ cmd.flag %i[p priority], type: String, default_value: Manager::DEFAULT_PRIORITIES,
39
+ desc: "The job priorities to execute, specified as `min` or `min:max`"
38
40
  cmd.flag %i[t threads], type: String, default_value: Manager::DEFAULT_THREADS,
39
41
  desc: "The number of threads for executing jobs, specified as `min:max`"
40
- cmd.flag %i[p poll_interval], type: Integer, default_value: DefaultOptionValue.new(value: 60),
42
+ cmd.flag %i[i poll_interval], type: Integer, default_value: DefaultOptionValue.new(value: 60),
41
43
  desc: "Interval between polls for available jobs (in seconds)"
42
44
  cmd.flag %i[cfg configfile], type: String, default_value: Manager::DEFAULT_CONFIG_FILES, multiple: true,
43
45
  desc: "The YAML configuration file to load. If specifying multiple files, the last file takes " \
@@ -101,7 +101,7 @@ module Exekutor
101
101
  table = Terminal::Table.new headings: ["id", "Status", "Last heartbeat"]
102
102
  table.title = host if many_hosts
103
103
  worker_count = 0
104
- Exekutor::Info::Worker.where(hostname: host).each do |worker|
104
+ Exekutor::Info::Worker.where(hostname: host).find_each do |worker|
105
105
  worker_count += 1
106
106
  table << worker_info_row(worker)
107
107
  end
@@ -23,6 +23,7 @@ module Exekutor
23
23
  # @option options [String] :environment The Rails environment to load
24
24
  # @option options [String] :queue The queue(s) to watch
25
25
  # @option options [String] :threads The number of threads to use for job execution
26
+ # @option options [String] :priority The priorities to execute
26
27
  # @option options [Integer] :poll_interval The interval in seconds for job polling
27
28
  # @return [Void]
28
29
  def start(options)
@@ -76,7 +77,7 @@ module Exekutor
76
77
  def worker_options(config_file, cli_overrides)
77
78
  worker_options = DEFAULT_CONFIGURATION.dup
78
79
 
79
- load_config_files(config_file, worker_options)
80
+ ConfigLoader.new(config_file, @global_options).load_config(worker_options)
80
81
 
81
82
  worker_options.merge! Exekutor.config.worker_options
82
83
  worker_options.merge! @global_options.slice(:identifier)
@@ -97,16 +98,22 @@ module Exekutor
97
98
  .reject { |_, value| value.is_a? DefaultOptionValue }
98
99
  .transform_keys(poll_interval: :polling_interval)
99
100
 
100
- min_threads, max_threads = parse_thread_limitations(cli_options[:threads])
101
+ min_threads, max_threads = parse_integer_range(cli_options[:threads])
101
102
  if min_threads
102
103
  worker_options[:min_threads] = min_threads
103
104
  worker_options[:max_threads] = max_threads || min_threads
104
105
  end
105
106
 
107
+ min_priority, max_priority = parse_integer_range(cli_options[:priority])
108
+ if min_threads
109
+ worker_options[:min_priority] = min_priority
110
+ worker_options[:max_priority] = max_priority if max_priority
111
+ end
112
+
106
113
  worker_options
107
114
  end
108
115
 
109
- def parse_thread_limitations(threads)
116
+ def parse_integer_range(threads)
110
117
  return if threads.blank? || threads.is_a?(DefaultOptionValue)
111
118
 
112
119
  if threads.is_a?(Integer)
@@ -116,37 +123,6 @@ module Exekutor
116
123
  end
117
124
  end
118
125
 
119
- def load_config_files(config_files, worker_options)
120
- config_files = if config_files.is_a? DefaultConfigFileValue
121
- config_files.to_a(@global_options[:identifier])
122
- else
123
- config_files&.map { |path| File.expand_path(path, Rails.root) }
124
- end
125
-
126
- config_files&.each { |path| load_config_file(path, worker_options) }
127
- end
128
-
129
- def load_config_file(path, worker_options)
130
- puts "Loading config file: #{path}" if verbose?
131
- config = begin
132
- YAML.safe_load(File.read(path), symbolize_names: true)
133
- rescue StandardError => e
134
- raise Error, "Cannot read config file: #{path} (#{e})"
135
- end
136
- unless config.keys == [:exekutor]
137
- raise Error, "Config should have an `exekutor` root node: #{path} (Found: #{config.keys.join(", ")})"
138
- end
139
-
140
- # Remove worker specific options before calling Exekutor.config.set
141
- worker_options.merge! config[:exekutor].extract!(:queue, :status_server_port)
142
-
143
- begin
144
- Exekutor.config.set(**config[:exekutor])
145
- rescue StandardError => e
146
- raise Error, "Cannot load config file: #{path} (#{e})"
147
- end
148
- end
149
-
150
126
  def start_and_join_worker(worker_options, is_daemonized)
151
127
  worker = Worker.new(worker_options)
152
128
  %w[INT TERM QUIT].each do |signal|
@@ -258,6 +234,73 @@ module Exekutor
258
234
  end
259
235
  end
260
236
 
237
+ # Takes care of loading YAML configuration
238
+ class ConfigLoader
239
+ def initialize(files, options)
240
+ @config_files = files
241
+ @options = options
242
+ end
243
+
244
+ def load_config(worker_options)
245
+ each_file do |path|
246
+ config = load_config_file(path)
247
+ convert_duration_options! config
248
+
249
+ worker_options.merge! extract_worker_options!(config)
250
+ apply_config_file(config)
251
+ end
252
+ Exekutor.config
253
+ end
254
+
255
+ private
256
+
257
+ WORKER_OPTIONS = %i[queues min_priority max_priority min_threads max_threads max_thread_idletime
258
+ wait_for_termination].freeze
259
+
260
+ def each_file(&block)
261
+ if @config_files.is_a? DefaultConfigFileValue
262
+ @config_files.to_a(@options[:identifier]).each(&block)
263
+ elsif @config_files.is_a? String
264
+ yield File.expand_path(@config_files, Rails.root)
265
+ else
266
+ @config_files.map { |path| File.expand_path(path, Rails.root) }.each(&block)
267
+ end
268
+ end
269
+
270
+ def extract_worker_options!(config)
271
+ config.extract!(*WORKER_OPTIONS)
272
+ end
273
+
274
+ def load_config_file(path)
275
+ puts "Loading config file: #{path}" if @options[:verbose]
276
+ config = begin
277
+ YAML.safe_load(File.read(path), symbolize_names: true)
278
+ rescue StandardError => e
279
+ raise Error, "Cannot read config file: #{path} (#{e})"
280
+ end
281
+ unless config.keys == [:exekutor]
282
+ raise Error, "Config should have an `exekutor` root node: #{path} (Found: #{config.keys.join(", ")})"
283
+ end
284
+
285
+ config[:exekutor]
286
+ end
287
+
288
+ def apply_config_file(config)
289
+ Exekutor.config.set(**config)
290
+ rescue StandardError => e
291
+ raise Error, "Cannot load config file (#{e})"
292
+ end
293
+
294
+ def convert_duration_options!(config)
295
+ { polling_interval: :seconds, max_execution_thread_idletime: :seconds, healthcheck_timeout: :minutes }
296
+ .each do |duration_option, duration_interval|
297
+ if config[duration_option].is_a? Numeric
298
+ config[duration_option] = config[duration_option].send(duration_interval)
299
+ end
300
+ end
301
+ end
302
+ end
303
+
261
304
  # The default value for the pid file
262
305
  class DefaultPidFileValue < DefaultOptionValue
263
306
  def initialize
@@ -278,7 +321,7 @@ module Exekutor
278
321
  # The default value for the config file
279
322
  class DefaultConfigFileValue < DefaultOptionValue
280
323
  def initialize
281
- super <<~DESC
324
+ super(<<~DESC)
282
325
  "config/exekutor.yml", overridden by "config/exekutor.%{identifier}.yml" if an identifier is specified
283
326
  DESC
284
327
  end
@@ -316,6 +359,7 @@ module Exekutor
316
359
  "Minimum: 1, Maximum: Active record pool size minus 1, with a minimum of 1"
317
360
  ).freeze
318
361
  DEFAULT_QUEUE = DefaultOptionValue.new("All queues").freeze
362
+ DEFAULT_PRIORITIES = DefaultOptionValue.new("All priorities").freeze
319
363
  DEFAULT_FOREVER = DefaultOptionValue.new("Forever").freeze
320
364
 
321
365
  DEFAULT_CONFIGURATION = { set_db_connection_name: true }.freeze
@@ -26,7 +26,7 @@ module Exekutor
26
26
  end
27
27
 
28
28
  options.each do |name, value|
29
- send "#{name}=", value
29
+ send :"#{name}=", value
30
30
  end
31
31
  self
32
32
  end
@@ -54,7 +54,7 @@ module Exekutor
54
54
  private
55
55
 
56
56
  def define_writer(name, required, type, enum, range)
57
- define_method "#{name}=" do |value|
57
+ define_method :"#{name}=" do |value|
58
58
  validate_option_presence! name, value if required
59
59
  validate_option_type! name, value, *type if type.present?
60
60
  validate_option_enum! name, value, *enum if enum.present?
@@ -93,7 +93,9 @@ module Exekutor
93
93
  return if allowed_types.include?(value.class)
94
94
 
95
95
  raise error_class, "##{name} should be an instance of #{
96
- allowed_types.to_sentence(last_word_connector: ", or ")} (Actual: #{value.class})"
96
+ allowed_types.map { |c| c == NilClass || c.nil? ? "nil" : c }
97
+ .to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
98
+ } (Actual: #{value.class})"
97
99
  end
98
100
 
99
101
  # Validates whether the value is a valid enum option
@@ -101,8 +103,10 @@ module Exekutor
101
103
  def validate_option_enum!(name, value, *allowed_values)
102
104
  return if allowed_values.include?(value)
103
105
 
104
- raise error_class, "##{name} should be one of #{allowed_values.map(&:inspect)
105
- .to_sentence(last_word_connector: ", or ")}"
106
+ raise error_class, "##{name} should be one of #{
107
+ allowed_values.map(&:inspect)
108
+ .to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
109
+ }"
106
110
  end
107
111
 
108
112
  # Validates whether the value falls in the allowed range
@@ -110,8 +114,10 @@ module Exekutor
110
114
  def validate_option_range!(name, value, allowed_range)
111
115
  return if allowed_range.include?(value)
112
116
 
113
- raise error_class, "##{name} should be between #{allowed_range.first} and #{allowed_range.last}#{
114
- " (exclusive)" if allowed_range.respond_to?(:exclude_end?) && allowed_range.exclude_end?}"
117
+ raise error_class, <<~MSG
118
+ ##{name} should be between #{allowed_range.first.inspect} and #{allowed_range.last.inspect}#{
119
+ " (exclusive)" if allowed_range.respond_to?(:exclude_end?) && allowed_range.exclude_end?}
120
+ MSG
115
121
  end
116
122
 
117
123
  protected
@@ -24,11 +24,14 @@ module Exekutor
24
24
  # @param pool [ThreadPoolExecutor] the thread pool to use
25
25
  # @param wait_timeout [Integer] the time to listen for notifications
26
26
  # @param set_db_connection_name [Boolean] whether to set the application name on the DB connection
27
- def initialize(worker_id:, provider:, pool:, queues: nil, wait_timeout: 60, set_db_connection_name: false)
27
+ def initialize(worker_id:, provider:, pool:, queues: nil, min_priority: nil, max_priority: nil, wait_timeout: 60,
28
+ set_db_connection_name: false)
28
29
  super()
29
30
  @config = {
30
31
  worker_id: worker_id,
31
- queues: queues || [],
32
+ queues: queues.presence,
33
+ min_priority: min_priority,
34
+ max_priority: max_priority,
32
35
  wait_timeout: wait_timeout,
33
36
  set_db_connection_name: set_db_connection_name
34
37
  }
@@ -67,11 +70,22 @@ module Exekutor
67
70
  PROVIDER_CHANNEL % @config[:worker_id]
68
71
  end
69
72
 
70
- # Whether this listener is listening to the given queue
71
- # @return [Boolean]
73
+ # @return [Boolean] Whether the job matches the configured queues and priority range
74
+ def job_filter_match?(job_info)
75
+ listening_to_queue?(job_info["q"]) && listening_to_priority?(job_info["p"].to_i)
76
+ end
77
+
78
+ # @return [Boolean] Whether this listener is listening to the given queue
72
79
  def listening_to_queue?(queue)
73
80
  queues = @config[:queues]
74
- queues.empty? || queues.include?(queue)
81
+ queues.nil? || queues.include?(queue)
82
+ end
83
+
84
+ # @return [Boolean] Whether this listener is listening to the given priority
85
+ def listening_to_priority?(priority)
86
+ minimum = @config[:min_priority]
87
+ maximum = @config[:max_priority]
88
+ (minimum.nil? || minimum <= priority) && (maximum.nil? || maximum >= priority)
75
89
  end
76
90
 
77
91
  # Starts the listener thread
@@ -129,8 +143,9 @@ module Exekutor
129
143
  JobParser.parse(payload)
130
144
  rescue StandardError => e
131
145
  logger.error e.message
146
+ nil
132
147
  end
133
- next unless job_info && listening_to_queue?(job_info["q"])
148
+ next unless job_info && job_filter_match?(job_info)
134
149
 
135
150
  @provider.update_earliest_scheduled_at(job_info["t"].to_f)
136
151
  end
@@ -177,7 +192,7 @@ module Exekutor
177
192
 
178
193
  # Parses a NOTIFY payload to a job
179
194
  class JobParser
180
- JOB_INFO_KEYS = %w[id q t].freeze
195
+ JOB_INFO_KEYS = %w[id q p t].freeze
181
196
 
182
197
  def self.parse(payload)
183
198
  job_info = begin
@@ -82,6 +82,7 @@ module Exekutor
82
82
  # @return [String] The given error class, message, and cleaned backtrace as a string
83
83
  def self.strferr(err)
84
84
  raise ArgumentError, "err must not be nil" if err.nil?
85
+ return err if err.is_a? String
85
86
 
86
87
  "#{err.class} – #{err.message}\nat #{
87
88
  err.backtrace ? backtrace_cleaner.clean(err.backtrace).join("\n ") : "unknown location"
@@ -25,16 +25,16 @@ module Exekutor
25
25
  # @param reserver [Reserver] the job reserver
26
26
  # @param executor [Executor] the job executor
27
27
  # @param pool [ThreadPoolExecutor] the thread pool to use
28
- # @param polling_interval [Integer] the polling interval
28
+ # @param polling_interval [ActiveSupport::Duration] the polling interval
29
29
  # @param interval_jitter [Float] the polling interval jitter
30
- def initialize(reserver:, executor:, pool:, polling_interval: 60,
30
+ def initialize(reserver:, executor:, pool:, polling_interval: 60.seconds,
31
31
  interval_jitter: polling_interval.to_i > 1 ? polling_interval * 0.1 : 0)
32
32
  super()
33
33
  @reserver = reserver
34
34
  @executor = executor
35
35
  @pool = pool
36
36
 
37
- @polling_interval = polling_interval.freeze
37
+ @polling_interval = polling_interval.to_i.freeze
38
38
  @interval_jitter = interval_jitter.to_f.freeze
39
39
 
40
40
  @event = Concurrent::Event.new
@@ -11,9 +11,9 @@ module Exekutor
11
11
  # Creates a new Reserver
12
12
  # @param worker_id [String] the id of the worker
13
13
  # @param queues [Array<String>] the queues to watch
14
- def initialize(worker_id, queues)
14
+ def initialize(worker_id, queues: nil, min_priority: nil, max_priority: nil)
15
15
  @worker_id = worker_id
16
- @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)
17
17
  @json_serializer = Exekutor.config.load_json_serializer
18
18
  end
19
19
 
@@ -26,8 +26,8 @@ module Exekutor
26
26
  results = Exekutor::Job.connection.exec_query <<~SQL, ACTION_NAME, [@worker_id, limit], prepare: true
27
27
  UPDATE exekutor_jobs SET worker_id = $1, status = 'e' WHERE id IN (
28
28
  SELECT id FROM exekutor_jobs
29
- WHERE scheduled_at <= now() AND "status"='p' #{@queue_filter_sql}
30
- 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
31
31
  FOR UPDATE SKIP LOCKED
32
32
  LIMIT $2
33
33
  ) RETURNING "id", "payload", "options", "scheduled_at"
@@ -51,7 +51,7 @@ module Exekutor
51
51
  # @return [Time,nil] The earliest scheduled at, or nil if the queues are empty
52
52
  def earliest_scheduled_at
53
53
  jobs = Exekutor::Job.pending
54
- 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?
55
55
  jobs.minimum(:scheduled_at)
56
56
  end
57
57
 
@@ -72,6 +72,16 @@ module Exekutor
72
72
  @json_serializer.load str unless str.nil?
73
73
  end
74
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
+
75
85
  # Builds SQL filter for the given queues
76
86
  def build_queue_filter_sql(queues)
77
87
  return nil if queues.nil? || (queues.is_a?(Array) && queues.empty?)
@@ -79,11 +89,32 @@ module Exekutor
79
89
  queues = queues.first if queues.is_a?(Array) && queues.one?
80
90
  validate_queues! queues
81
91
 
82
- if queues.is_a? Array
83
- Exekutor::Job.sanitize_sql_for_conditions(["AND queue IN (?)", queues])
84
- else
85
- Exekutor::Job.sanitize_sql_for_conditions(["AND queue = ?", queues])
86
- end
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)
104
+
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)
87
118
  end
88
119
 
89
120
  # Raises an error if the queues value is invalid
@@ -96,7 +127,8 @@ module Exekutor
96
127
  when String, Symbol
97
128
  raise ArgumentError, "queue name cannot be empty" unless valid_queue_name? queues
98
129
  else
99
- raise ArgumentError, "queues must be nil, a String, Symbol, or an array of Strings or Symbols"
130
+ raise ArgumentError,
131
+ "queues must be nil, a String, Symbol, or an array of Strings or Symbols (Actual: #{queues.class})"
100
132
  end
101
133
  end
102
134
 
@@ -23,7 +23,7 @@ module Exekutor
23
23
 
24
24
  DEFAULT_HANDLER = "webrick"
25
25
 
26
- def initialize(worker:, pool:, port:, handler: DEFAULT_HANDLER, heartbeat_timeout: 30)
26
+ def initialize(worker:, pool:, port:, handler: DEFAULT_HANDLER, heartbeat_timeout: 30.minutes)
27
27
  super()
28
28
  @worker = worker
29
29
  @pool = pool
@@ -52,12 +52,13 @@ module Exekutor
52
52
  return unless @thread_running.value
53
53
 
54
54
  server = @server.value
55
- if server.respond_to? :shutdown
56
- server.shutdown
57
- elsif server.respond_to? :stop
58
- server.stop
55
+ shutdown_method = %i[shutdown stop].find { |method| server.respond_to? method }
56
+ if shutdown_method
57
+ server.send(shutdown_method)
58
+ Exekutor.say "Status server stopped"
59
59
  elsif server
60
- Exekutor.say! "Cannot shutdown status server, #{server.class.name} does not respond to shutdown or stop"
60
+ Exekutor.print_error "Cannot shutdown status server, " \
61
+ "#{server.class.name} does not respond to `shutdown` or `stop`"
61
62
  end
62
63
  end
63
64
 
@@ -67,7 +68,7 @@ module Exekutor
67
68
  def run(worker, port)
68
69
  return unless state == :started && @thread_running.make_true
69
70
 
70
- Exekutor.say "Starting status server at 0.0.0.0:#{port}… (Timeout: #{@heartbeat_timeout} minutes)"
71
+ Exekutor.say "Starting status server at 0.0.0.0:#{port}… (Timeout: #{@heartbeat_timeout.inspect})"
71
72
  @handler.run(App.new(worker, @heartbeat_timeout), Port: port, Host: "0.0.0.0", Silent: true,
72
73
  Logger: ::Logger.new(File.open(File::NULL, "w")), AccessLog: []) do |server|
73
74
  @server.set server
@@ -107,7 +108,7 @@ module Exekutor
107
108
  private
108
109
 
109
110
  def flatlined?(last_heartbeat = @worker.last_heartbeat)
110
- last_heartbeat.nil? || last_heartbeat < @heartbeat_timeout.minutes.ago
111
+ last_heartbeat.nil? || last_heartbeat < @heartbeat_timeout.ago
111
112
  end
112
113
 
113
114
  def render_threads
@@ -17,18 +17,18 @@ module Exekutor
17
17
  MAX_NAME_LENGTH = 63
18
18
 
19
19
  # Adds a job to the queue, scheduled to perform immediately
20
- # @param jobs [Array<ActiveJob::Base>] the jobs to enqueue
21
- # @return [void]
22
- def push(*jobs)
23
- create_records(jobs)
20
+ # @param jobs [ActiveJob::Base,Array<ActiveJob::Base>] the jobs to enqueue
21
+ # @return [Integer] the number of enqueued jobs
22
+ def push(jobs)
23
+ create_records Array.wrap(jobs)
24
24
  end
25
25
 
26
26
  # Adds a job to the queue, scheduled to be performed at the indicated time
27
- # @param jobs [Array<ActiveJob::Base>] the jobs to enqueue
27
+ # @param jobs [ActiveJob::Base,Array<ActiveJob::Base>] the jobs to enqueue
28
28
  # @param timestamp [Time,Date,Integer,Float] when the job should be performed
29
- # @return [void]
30
- def schedule_at(*jobs, timestamp)
31
- create_records(jobs, scheduled_at: timestamp)
29
+ # @return [Integer] the number of enqueued jobs
30
+ def schedule_at(jobs, timestamp)
31
+ create_records(Array.wrap(jobs), scheduled_at: timestamp)
32
32
  end
33
33
 
34
34
  private
@@ -38,16 +38,22 @@ module Exekutor
38
38
  # @param scheduled_at [Time,Date,Integer,Float] when the job should be performed
39
39
  # @return [void]
40
40
  def create_records(jobs, scheduled_at: nil)
41
- unless jobs.is_a?(Array) && jobs.all?(ActiveJob::Base)
42
- raise ArgumentError, "jobs must be an array with ActiveJob items"
41
+ raise ArgumentError, "jobs must be an array with ActiveJob items (Actual: #{jobs.class})" unless jobs.is_a?(Array)
42
+
43
+ unless jobs.all?(ActiveJob::Base)
44
+ raise ArgumentError, "jobs must be an array with ActiveJob items (Found: #{
45
+ jobs.map { |job| job.class unless job.is_a? ActiveJob::Base }.compact.join(", ")
46
+ })"
43
47
  end
44
48
 
45
49
  scheduled_at = parse_scheduled_at(scheduled_at)
46
50
  json_serializer = Exekutor.config.load_json_serializer
47
51
 
52
+ inserted_records = nil
48
53
  Internal::Hooks.run :enqueue, jobs do
49
- insert_job_records(jobs, scheduled_at, json_serializer)
54
+ inserted_records = insert_job_records(jobs, scheduled_at, json_serializer)
50
55
  end
56
+ inserted_records
51
57
  end
52
58
 
53
59
  # Converts the given value to an epoch timestamp. Returns the current epoch timestamp if the given value is nil
@@ -78,22 +84,37 @@ module Exekutor
78
84
  # @param json_serializer [#dump] the serializer to use to convert hashes into JSON
79
85
  def insert_job_records(jobs, scheduled_at, json_serializer)
80
86
  if jobs.one?
81
- sql_binds = job_sql_binds(jobs.first, scheduled_at, json_serializer)
82
- Exekutor::Job.connection.exec_query <<~SQL, ACTION_NAME, sql_binds, prepare: true
83
- INSERT INTO exekutor_jobs ("queue", "priority", "scheduled_at", "active_job_id", "payload", "options") VALUES ($1, $2, to_timestamp($3), $4, $5, $6) RETURNING id;
84
- SQL
87
+ insert_singular_job(jobs.first, json_serializer, scheduled_at)
85
88
  else
86
89
  insert_statements = jobs.map do |job|
87
90
  Exekutor::Job.sanitize_sql_for_assignment(
88
91
  ["(?, ?, to_timestamp(?), ?, ?::jsonb, ?::jsonb)", *job_sql_binds(job, scheduled_at, json_serializer)]
89
92
  )
90
93
  end
91
- Exekutor::Job.connection.insert <<~SQL, ACTION_NAME
92
- INSERT INTO exekutor_jobs ("queue", "priority", "scheduled_at", "active_job_id", "payload", "options") VALUES #{insert_statements.join(",")}
93
- SQL
94
+ begin
95
+ pg_result = Exekutor::Job.connection.execute <<~SQL, ACTION_NAME
96
+ INSERT INTO exekutor_jobs ("queue", "priority", "scheduled_at", "active_job_id", "payload", "options") VALUES #{insert_statements.join(",")}
97
+ SQL
98
+ inserted_records = pg_result.cmd_tuples
99
+ ensure
100
+ pg_result.clear
101
+ end
102
+ inserted_records
94
103
  end
95
104
  end
96
105
 
106
+ # Fires off an INSERT INTO query for the given job using a prepared statement
107
+ # @param job [ActiveJob::Base] the job to insert
108
+ # @param scheduled_at [Integer,Float] the scheduled execution time for the jobs as an epoch timestamp
109
+ # @param json_serializer [#dump] the serializer to use to convert hashes into JSON
110
+ def insert_singular_job(job, json_serializer, scheduled_at)
111
+ sql_binds = job_sql_binds(job, scheduled_at, json_serializer)
112
+ ar_result = Exekutor::Job.connection.exec_query <<~SQL, ACTION_NAME, sql_binds, prepare: true
113
+ INSERT INTO exekutor_jobs ("queue", "priority", "scheduled_at", "active_job_id", "payload", "options") VALUES ($1, $2, to_timestamp($3), $4, $5, $6) RETURNING id;
114
+ SQL
115
+ ar_result.length
116
+ end
117
+
97
118
  # Converts the specified job to SQL bind parameters to insert it into the database
98
119
  # @param job [ActiveJob::Base] the job to insert
99
120
  # @param scheduled_at [Float] the epoch timestamp for when the job should be executed
@@ -124,7 +145,7 @@ module Exekutor
124
145
  def exekutor_options(job)
125
146
  return nil unless job.respond_to?(:exekutor_options)
126
147
 
127
- options = job.exekutor_options.stringify_keys
148
+ options = job.exekutor_options&.stringify_keys
128
149
  if options && options["queue_timeout"]
129
150
  options["start_execution_before"] = Time.now.to_f + options.delete("queue_timeout").to_f
130
151
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Exekutor
4
4
  # The current version of Exekutor
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.2"
6
6
  end
@@ -23,17 +23,17 @@ module Exekutor
23
23
  # @option config [Array<String>] :queues the queues to work on
24
24
  # @option config [Integer] :min_threads the minimum number of execution threads that should be active
25
25
  # @option config [Integer] :max_threads the maximum number of execution threads that may be active
26
- # @option config [Integer] :max_thread_idletime the maximum number of seconds a thread may be idle before being
27
- # stopped
28
- # @option config [Integer] :polling_interval the polling interval in seconds
26
+ # @option config [ActiveSupport::Duration] :max_thread_idletime the maximum duration a thread may be idle before
27
+ # being stopped
28
+ # @option config [ActiveSupport::Duration] :polling_interval the polling interval
29
29
  # @option config [Float] :poling_jitter the polling jitter
30
30
  # @option config [Boolean] :set_db_connection_name whether the DB connection name should be set
31
31
  # @option config [Integer,Boolean] :wait_for_termination how long the worker should wait on jobs to be completed
32
32
  # before exiting
33
33
  # @option config [Integer] :status_server_port the port to run the status server on
34
34
  # @option config [String] :status_server_handler The name of the rack handler to use for the status server
35
- # @option config [Integer] :healthcheck_timeout The timeout of a worker in minutes before the healthcheck server
36
- # deems it as down
35
+ # @option config [ActiveSupport::Duration] :healthcheck_timeout The timeout of a worker in minutes before the
36
+ # healthcheck server deems it as down
37
37
  def initialize(config = {})
38
38
  super()
39
39
  @config = config
@@ -178,7 +178,7 @@ module Exekutor
178
178
  end
179
179
 
180
180
  def create_provider(worker_options, executor, thread_pool)
181
- @reserver = Internal::Reserver.new @record.id, worker_options[:queues]
181
+ @reserver = Internal::Reserver.new @record.id, **worker_options.slice(:queues, :min_priority, :max_priority)
182
182
  provider = Internal::Provider.new reserver: @reserver, executor: executor, pool: thread_pool,
183
183
  **provider_options(worker_options)
184
184
 
@@ -12,11 +12,12 @@ module Exekutor
12
12
  # Creates the configuration file at +config/exekutor.yml+. Uses the current worker configuration as the base.
13
13
  def create_configuration_file
14
14
  config = { queues: %w[queues to watch] }.merge(Exekutor.config.worker_options)
15
- config[:status_port] = 8765
15
+ config[:status_port] = 12_677
16
16
  config[:set_db_connection_name] = true
17
17
  config[:wait_for_termination] = 120
18
- create_file "config/exekutor#{".#{options[:identifier]}" if options[:identifier]}.yml",
19
- { "exekutor" => config.stringify_keys }.to_yaml
18
+
19
+ filename = "config/exekutor#{".#{options[:identifier]}" if options[:identifier]}.yml"
20
+ create_file filename, { "exekutor" => config.stringify_keys }.to_yaml
20
21
  end
21
22
  end
22
23
  end
@@ -43,11 +43,9 @@ module Exekutor
43
43
  def create_fx_files
44
44
  return unless defined?(Fx)
45
45
 
46
- %w[job_notifier requeue_orphaned_jobs].each do |function|
47
- copy_file "functions/#{function}.sql", Fx::Definition.new(name: function, version: 1).full_path
48
- end
49
- %w[notify_workers requeue_orphaned_jobs].each do |trigger|
50
- copy_file "triggers/#{trigger}.sql", Fx::Definition.new(name: trigger, version: 1, type: "trigger").full_path
46
+ %w[exekutor_broadcast_job_enqueued exekutor_requeue_orphaned_jobs].each do |name|
47
+ copy_file "functions/#{name}.sql", Fx::Definition.new(name: name, version: 1).full_path
48
+ copy_file "triggers/#{name}.sql", Fx::Definition.new(name: name, version: 1, type: "trigger").full_path
51
49
  end
52
50
  end
53
51
  end
@@ -0,0 +1,10 @@
1
+ CREATE
2
+ OR REPLACE FUNCTION exekutor_broadcast_job_enqueued() RETURNS TRIGGER AS $$
3
+ BEGIN
4
+ PERFORM
5
+ pg_notify('exekutor::job_enqueued',
6
+ CONCAT('id:', NEW.id,';q:', NEW.queue,';p:', NEW.priority, ';t:', extract ('epoch' from NEW.scheduled_at)));
7
+ RETURN NULL;
8
+ END;
9
+ $$
10
+ LANGUAGE plpgsql
@@ -0,0 +1,11 @@
1
+ CREATE
2
+ OR REPLACE FUNCTION exekutor_requeue_orphaned_jobs() RETURNS TRIGGER AS $$
3
+ BEGIN
4
+ UPDATE exekutor_jobs
5
+ SET status = 'p'
6
+ WHERE worker_id = OLD.id
7
+ AND status = 'e';
8
+ RETURN OLD;
9
+ END;
10
+ $$
11
+ LANGUAGE plpgsql
@@ -42,40 +42,40 @@ class CreateExekutorSchema < ActiveRecord::Migration[<%= migration_version %>]
42
42
  t.jsonb :error, null: false
43
43
  end
44
44
  <% if defined? Fx %>
45
- create_function :job_notifier
46
- create_trigger :notify_workers
45
+ create_function :exekutor_broadcast_job_enqueued
46
+ create_trigger :exekutor_broadcast_job_enqueued, on: :exekutor_jobs
47
47
 
48
- create_function :requeue_orphaned_jobs
49
- create_trigger :requeue_orphaned_jobs
48
+ create_function :exekutor_requeue_orphaned_jobs
49
+ create_trigger :exekutor_requeue_orphaned_jobs, on: :exekutor_workers
50
50
  <% else %>
51
51
  reversible do |direction|
52
52
  direction.up do
53
53
  execute <<~SQL
54
- <%= function_sql "job_notifier" %>
54
+ <%= function_sql "exekutor_broadcast_job_enqueued" %>
55
55
  SQL
56
56
  execute <<~SQL
57
- <%= trigger_sql "notify_workers" %>
57
+ <%= trigger_sql "exekutor_broadcast_job_enqueued" %>
58
58
  SQL
59
59
 
60
60
  execute <<~SQL
61
- <%= function_sql "requeue_orphaned_jobs" %>
61
+ <%= function_sql "exekutor_requeue_orphaned_jobs" %>
62
62
  SQL
63
63
  execute <<~SQL
64
- <%= trigger_sql "requeue_orphaned_jobs" %>
64
+ <%= trigger_sql "exekutor_requeue_orphaned_jobs" %>
65
65
  SQL
66
66
  end
67
67
  direction.down do
68
68
  execute <<~SQL
69
- DROP TRIGGER requeue_orphaned_jobs ON exekutor_workers
69
+ DROP TRIGGER exekutor_requeue_orphaned_jobs ON exekutor_workers
70
70
  SQL
71
71
  execute <<~SQL
72
- DROP FUNCTION requeue_orphaned_jobs
72
+ DROP FUNCTION exekutor_requeue_orphaned_jobs
73
73
  SQL
74
74
  execute <<~SQL
75
- DROP TRIGGER notify_exekutor_workers ON exekutor_jobs
75
+ DROP TRIGGER exekutor_broadcast_job_enqueued ON exekutor_jobs
76
76
  SQL
77
77
  execute <<~SQL
78
- DROP FUNCTION exekutor_job_notifier
78
+ DROP FUNCTION exekutor_broadcast_job_enqueued
79
79
  SQL
80
80
  end
81
81
  end
@@ -0,0 +1,7 @@
1
+ CREATE TRIGGER exekutor_broadcast_job_enqueued
2
+ AFTER INSERT OR
3
+ UPDATE OF queue, scheduled_at, status
4
+ ON exekutor_jobs
5
+ FOR EACH ROW
6
+ WHEN (NEW.status = 'p')
7
+ EXECUTE FUNCTION exekutor_broadcast_job_enqueued()
@@ -0,0 +1,5 @@
1
+ CREATE TRIGGER exekutor_requeue_orphaned_jobs
2
+ BEFORE DELETE
3
+ ON exekutor_workers
4
+ FOR EACH ROW
5
+ EXECUTE FUNCTION exekutor_requeue_orphaned_jobs()
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exekutor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roy
@@ -34,7 +34,7 @@ cert_chain:
34
34
  LMMHpsz0vpxVqcs8USL9494hQUWRVlYd1F2PJeWtdKi0bU8dCduphJd8cTvCtS2l
35
35
  CAY756btGBLeeWMBZ/DRMj1Cz3ifI9DV+KHqXg==
36
36
  -----END CERTIFICATE-----
37
- date: 2023-05-14 00:00:00.000000000 Z
37
+ date: 2023-12-19 00:00:00.000000000 Z
38
38
  dependencies:
39
39
  - !ruby/object:Gem::Dependency
40
40
  name: activejob
@@ -410,12 +410,12 @@ files:
410
410
  - lib/exekutor/worker.rb
411
411
  - lib/generators/exekutor/configuration_generator.rb
412
412
  - lib/generators/exekutor/install_generator.rb
413
- - lib/generators/exekutor/templates/install/functions/job_notifier.sql
414
- - lib/generators/exekutor/templates/install/functions/requeue_orphaned_jobs.sql
413
+ - lib/generators/exekutor/templates/install/functions/exekutor_broadcast_job_enqueued.sql
414
+ - lib/generators/exekutor/templates/install/functions/exekutor_requeue_orphaned_jobs.sql
415
415
  - lib/generators/exekutor/templates/install/initializers/exekutor.rb.erb
416
416
  - lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb
417
- - lib/generators/exekutor/templates/install/triggers/notify_workers.sql
418
- - lib/generators/exekutor/templates/install/triggers/requeue_orphaned_jobs.sql
417
+ - lib/generators/exekutor/templates/install/triggers/exekutor_broadcast_job_enqueued.sql
418
+ - lib/generators/exekutor/templates/install/triggers/exekutor_requeue_orphaned_jobs.sql
419
419
  homepage: https://github.com/devdicated/exekutor
420
420
  licenses:
421
421
  - MIT
metadata.gz.sig CHANGED
Binary file
@@ -1,7 +0,0 @@
1
- CREATE OR REPLACE FUNCTION exekutor_job_notifier() RETURNS TRIGGER AS $$
2
- BEGIN
3
- PERFORM pg_notify('exekutor::job_enqueued',
4
- CONCAT('id:', NEW.id,';q:', NEW.queue,';t:', extract ('epoch' from NEW.scheduled_at)));
5
- RETURN NULL;
6
- END;
7
- $$ LANGUAGE plpgsql
@@ -1,7 +0,0 @@
1
- CREATE OR REPLACE FUNCTION requeue_orphaned_jobs() RETURNS TRIGGER AS $$ BEGIN
2
- UPDATE exekutor_jobs
3
- SET status = 'p'
4
- WHERE worker_id = OLD.id
5
- AND status = 'e';
6
- RETURN OLD; END;
7
- $$ LANGUAGE plpgsql
@@ -1,6 +0,0 @@
1
- CREATE TRIGGER notify_exekutor_workers
2
- AFTER INSERT OR UPDATE OF queue, scheduled_at, status
3
- ON exekutor_jobs
4
- FOR EACH ROW
5
- WHEN (NEW.status = 'p')
6
- EXECUTE FUNCTION exekutor_job_notifier()
@@ -1,5 +0,0 @@
1
- CREATE TRIGGER requeue_orphaned_jobs
2
- BEFORE DELETE
3
- ON exekutor_workers
4
- FOR EACH ROW
5
- EXECUTE FUNCTION requeue_orphaned_jobs()