exekutor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/exe/exekutor +7 -0
  5. data/lib/active_job/queue_adapters/exekutor_adapter.rb +14 -0
  6. data/lib/exekutor/asynchronous.rb +188 -0
  7. data/lib/exekutor/cleanup.rb +56 -0
  8. data/lib/exekutor/configuration.rb +373 -0
  9. data/lib/exekutor/hook.rb +172 -0
  10. data/lib/exekutor/info/worker.rb +20 -0
  11. data/lib/exekutor/internal/base_record.rb +11 -0
  12. data/lib/exekutor/internal/callbacks.rb +138 -0
  13. data/lib/exekutor/internal/cli/app.rb +173 -0
  14. data/lib/exekutor/internal/cli/application_loader.rb +36 -0
  15. data/lib/exekutor/internal/cli/cleanup.rb +96 -0
  16. data/lib/exekutor/internal/cli/daemon.rb +108 -0
  17. data/lib/exekutor/internal/cli/default_option_value.rb +29 -0
  18. data/lib/exekutor/internal/cli/info.rb +126 -0
  19. data/lib/exekutor/internal/cli/manager.rb +260 -0
  20. data/lib/exekutor/internal/configuration_builder.rb +113 -0
  21. data/lib/exekutor/internal/database_connection.rb +21 -0
  22. data/lib/exekutor/internal/executable.rb +75 -0
  23. data/lib/exekutor/internal/executor.rb +242 -0
  24. data/lib/exekutor/internal/hooks.rb +87 -0
  25. data/lib/exekutor/internal/listener.rb +176 -0
  26. data/lib/exekutor/internal/logger.rb +74 -0
  27. data/lib/exekutor/internal/provider.rb +308 -0
  28. data/lib/exekutor/internal/reserver.rb +95 -0
  29. data/lib/exekutor/internal/status_server.rb +132 -0
  30. data/lib/exekutor/job.rb +31 -0
  31. data/lib/exekutor/job_error.rb +11 -0
  32. data/lib/exekutor/job_options.rb +95 -0
  33. data/lib/exekutor/plugins/appsignal.rb +46 -0
  34. data/lib/exekutor/plugins.rb +13 -0
  35. data/lib/exekutor/queue.rb +141 -0
  36. data/lib/exekutor/version.rb +6 -0
  37. data/lib/exekutor/worker.rb +219 -0
  38. data/lib/exekutor.rb +49 -0
  39. data/lib/generators/exekutor/configuration_generator.rb +18 -0
  40. data/lib/generators/exekutor/install_generator.rb +43 -0
  41. data/lib/generators/exekutor/templates/install/functions/job_notifier.sql +7 -0
  42. data/lib/generators/exekutor/templates/install/functions/requeue_orphaned_jobs.sql +7 -0
  43. data/lib/generators/exekutor/templates/install/initializers/exekutor.rb.erb +14 -0
  44. data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +83 -0
  45. data/lib/generators/exekutor/templates/install/triggers/notify_workers.sql +6 -0
  46. data/lib/generators/exekutor/templates/install/triggers/requeue_orphaned_jobs.sql +5 -0
  47. data.tar.gz.sig +0 -0
  48. metadata +403 -0
  49. metadata.gz.sig +0 -0
@@ -0,0 +1,46 @@
1
+ raise Exekutor::Plugins::LoadError, "Appsignal not found, is the gem loaded?" unless defined? Appsignal
2
+
3
+ module Exekutor
4
+ module Plugins
5
+ # Hooks to send job execution info and raised errors to Appsignal
6
+ class Appsignal
7
+ include Hook
8
+ before_shutdown { ::Appsignal.stop("exekutor") }
9
+
10
+ around_job_execution :invoke_with_instrumentation
11
+
12
+ on_job_failure { |_job, error| report_error error }
13
+ on_fatal_error :report_error
14
+
15
+ def invoke_with_instrumentation(job)
16
+ payload = job[:payload]
17
+ params = ::Appsignal::Utils::HashSanitizer.sanitize(
18
+ payload.fetch("arguments", {}),
19
+ ::Appsignal.config[:filter_parameters]
20
+ )
21
+
22
+ ::Appsignal.monitor_transaction(
23
+ "perform_job.exekutor",
24
+ class: payload['job_class'],
25
+ method: "perform",
26
+ params: params,
27
+ metadata: {
28
+ id: payload['job_id'],
29
+ queue: payload['queue_name'],
30
+ priority: payload.fetch('priority', Exekutor.config.default_queue_priority),
31
+ attempts: payload.fetch('attempts', 0)
32
+ },
33
+ queue_start: job[:scheduled_at]
34
+ ) do
35
+ yield job
36
+ end
37
+ end
38
+
39
+ def report_error(error)
40
+ ::Appsignal.add_exception(error)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ Exekutor.hooks.register Exekutor::Plugins::Appsignal
@@ -0,0 +1,13 @@
1
+ module Exekutor
2
+ module Plugins
3
+ class LoadError < ::LoadError; end
4
+ end
5
+
6
+ def self.load_plugin(name)
7
+ if File.exist? File.join(__dir__, "plugins/#{name}.rb")
8
+ require_relative "plugins/#{name}"
9
+ else
10
+ raise Plugins::LoadError, "The #{name} plugin does not exist. Have you spelled it correctly?"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exekutor
4
+ # The job queue
5
+ class Queue
6
+ # Used when logging the SQL queries
7
+ # @private
8
+ ACTION_NAME = "Exekutor::Enqueue"
9
+ private_constant "ACTION_NAME"
10
+
11
+ # Valid range for job priority
12
+ # @private
13
+ VALID_PRIORITIES = (1..32_767).freeze
14
+
15
+ # Maximum length for the queue name
16
+ # @private
17
+ MAX_NAME_LENGTH = 63
18
+
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)
24
+ end
25
+
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
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)
32
+ end
33
+
34
+ private
35
+
36
+ # Creates {Exekutor::Job} records for the specified jobs, scheduled at the indicated time
37
+ # @param jobs [Array<ActiveJob::Base>] the jobs to enqueue
38
+ # @param scheduled_at [Time,Date,Integer,Float] when the job should be performed
39
+ # @return [void]
40
+ def create_records(jobs, scheduled_at: nil)
41
+ unless jobs.is_a?(Array) && jobs.all? { |job| job.is_a?(ActiveJob::Base) }
42
+ raise ArgumentError, "jobs must be an array with ActiveJob items"
43
+ end
44
+
45
+ if scheduled_at.nil?
46
+ scheduled_at = Time.now.to_i
47
+ else
48
+ case scheduled_at
49
+ when Integer, Float
50
+ raise ArgumentError, "scheduled_at must be a valid epoch" unless scheduled_at.positive?
51
+ when Time
52
+ scheduled_at = scheduled_at.to_f
53
+ when Date
54
+ scheduled_at = scheduled_at.at_beginning_of_day.to_f
55
+ else
56
+ raise ArgumentError, "scheduled_at must be an epoch, time, or date"
57
+ end
58
+ end
59
+
60
+ json_serializer = Exekutor.config.load_json_serializer
61
+
62
+ Internal::Hooks.run :enqueue, jobs do
63
+ if jobs.one?
64
+ Exekutor::Job.connection.exec_query <<~SQL, ACTION_NAME, job_sql_binds(jobs.first, scheduled_at, json_serializer), prepare: true
65
+ INSERT INTO exekutor_jobs ("queue", "priority", "scheduled_at", "active_job_id", "payload", "options") VALUES ($1, $2, to_timestamp($3), $4, $5, $6) RETURNING id;
66
+ SQL
67
+ else
68
+ insert_statements = jobs.map do |job|
69
+ Exekutor::Job.sanitize_sql_for_assignment(
70
+ ["(?, ?, to_timestamp(?), ?, ?::jsonb, ?::jsonb)", *job_sql_binds(job, scheduled_at, json_serializer)]
71
+ )
72
+ end
73
+ Exekutor::Job.connection.insert <<~SQL, ACTION_NAME
74
+ INSERT INTO exekutor_jobs ("queue", "priority", "scheduled_at", "active_job_id", "payload", "options") VALUES #{insert_statements.join(",")}
75
+ SQL
76
+ end
77
+ end
78
+ end
79
+
80
+ # Converts the specified job to SQL bind parameters to insert it into the database
81
+ # @param job [ActiveJob::Base] the job to insert
82
+ # @param scheduled_at [Float] the epoch timestamp for when the job should be executed
83
+ # @param json_serializer [#dump] the serializer to use to convert hashes into JSON
84
+ # @return [Array] the SQL bind parameters for inserting the specified job
85
+ def job_sql_binds(job, scheduled_at, json_serializer)
86
+ if job.queue_name.blank?
87
+ raise Error, "The queue must be set"
88
+ elsif job.queue_name && job.queue_name.length > Queue::MAX_NAME_LENGTH
89
+ raise Error, "The queue name \"#{job.queue_name}\" is too long, the limit is #{Queue::MAX_NAME_LENGTH} characters"
90
+ end
91
+
92
+ options = exekutor_options job
93
+ [
94
+ job.queue_name.presence,
95
+ job_priority(job),
96
+ scheduled_at,
97
+ job.job_id,
98
+ json_serializer.dump(job.serialize),
99
+ options.present? ? json_serializer.dump(options) : nil
100
+ ]
101
+ end
102
+
103
+ # Get the exekutor options for the specified job.
104
+ # @param job [ActiveJob::Base] the job to get the options for
105
+ # @return [Hash<String,Object>] the exekutor options
106
+ def exekutor_options(job)
107
+ return nil unless job.respond_to?(:exekutor_options)
108
+
109
+ options = job.exekutor_options.stringify_keys
110
+ if options && options["queue_timeout"]
111
+ options["start_execution_before"] = Time.now.to_f + options.delete("queue_timeout").to_f
112
+ end
113
+ options["execution_timeout"] = options["execution_timeout"].to_f if options && options["execution_timeout"]
114
+
115
+ options
116
+ end
117
+
118
+ # Get the priority for the specified job.
119
+ # @param job [ActiveJob::Base] the job to get the priority for
120
+ # @return [Integer] the priority
121
+ def job_priority(job)
122
+ priority = job.priority
123
+ if priority.is_a? Integer
124
+ unless VALID_PRIORITIES.include? priority
125
+ raise Error, <<~MESSAGE
126
+ Job priority must be between #{VALID_PRIORITIES.begin} and #{VALID_PRIORITIES.end} (actual: #{priority})
127
+ MESSAGE
128
+ end
129
+
130
+ priority
131
+ elsif priority.nil?
132
+ Exekutor.config.default_queue_priority
133
+ else
134
+ raise Error, "Job priority must be an Integer or nil"
135
+ end
136
+ end
137
+
138
+ # Default error for queueing problems
139
+ class Error < Exekutor::Error; end
140
+ end
141
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exekutor
4
+ # The current version of Exekutor
5
+ VERSION = "0.1.0"
6
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "internal/executable"
4
+
5
+ module Exekutor
6
+ # The job worker
7
+ class Worker
8
+ include Internal::Executable
9
+
10
+ attr_reader :record
11
+
12
+ # Creates a new worker with the specified config and immediately starts it
13
+ # @see #initialize
14
+ #
15
+ # @return The worker
16
+ def self.start(config = {})
17
+ new(config).tap(&:start)
18
+ end
19
+
20
+ # Creates a new worker with the specified config
21
+ # @param config [Hash] The worker configuration
22
+ # @option config [String] :identifier the identifier for this worker
23
+ # @option config [Array<String>] :queues the queues to work on
24
+ # @option config [Integer] :min_threads the minimum number of execution threads that should be active
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 stopped
27
+ # @option config [Integer] :polling_interval the polling interval in seconds
28
+ # @option config [Float] :poling_jitter the polling jitter
29
+ # @option config [Boolean] :set_db_connection_name whether the DB connection name should be set
30
+ # @option config [Integer,Boolean] :wait_for_termination how long the worker should wait on jobs to be completed before exiting
31
+ # @option config [Integer] :status_server_port the port to run the status server on
32
+ # @option config [String] :status_server_handler The name of the rack handler to use for the status server
33
+ # @option config [Integer] :healthcheck_timeout The timeout of a worker in minutes before the healthcheck server deems it as down
34
+ def initialize(config = {})
35
+ super()
36
+ @config = config
37
+ @record = create_record!
38
+
39
+ @reserver = Internal::Reserver.new @record.id, config[:queues]
40
+ @executor = Internal::Executor.new(**config.slice(:min_threads, :max_threads, :max_thread_idletime,
41
+ :delete_completed_jobs, :delete_discarded_jobs,
42
+ :delete_failed_jobs))
43
+
44
+ provider_threads = 1
45
+ provider_threads += 1 if config.fetch(:enable_listener, true)
46
+ provider_threads += 1 if config[:status_server_port].to_i > 0
47
+
48
+ provider_pool = Concurrent::FixedThreadPool.new provider_threads, max_queue: provider_threads,
49
+ name: "exekutor-provider"
50
+
51
+ @provider = Internal::Provider.new reserver: @reserver, executor: @executor, pool: provider_pool,
52
+ **provider_options(config)
53
+
54
+ @executables = [@executor, @provider]
55
+ if config.fetch(:enable_listener, true)
56
+ listener = Internal::Listener.new worker_id: @record.id, provider: @provider, pool: provider_pool,
57
+ **listener_options(config)
58
+ @executables << listener
59
+ end
60
+ if config[:status_server_port].to_i > 0
61
+ server = Internal::StatusServer.new worker: self, pool: provider_pool, **status_server_options(config)
62
+ @executables << server
63
+ end
64
+ @executables.freeze
65
+
66
+ @executor.after_execute(@record) do |_job, worker_info|
67
+ worker_info.heartbeat! rescue nil
68
+ @provider.poll if @provider.running?
69
+ end
70
+ @provider.on_queue_empty(@record) do |worker_info|
71
+ worker_info.heartbeat! rescue nil
72
+ @executor.prune_pool
73
+ end
74
+ end
75
+
76
+ # Starts the worker. Does nothing if the worker has already started.
77
+ # @return [Boolean] whether the worker was started
78
+ def start
79
+ return false unless compare_and_set_state(:pending, :started)
80
+ Internal::Hooks.run :startup, self do
81
+ @executables.each(&:start)
82
+ @record.update(status: "r")
83
+ end
84
+ true
85
+ end
86
+
87
+ # Stops the worker. If +wait_for_termination+ is set, this method blocks until the execution thread is terminated
88
+ # or killed.
89
+ # @return true
90
+ def stop
91
+ Internal::Hooks.run :shutdown, self do
92
+ set_state :stopped
93
+ unless @record.destroyed?
94
+ begin
95
+ @record.update(status: "s")
96
+ rescue
97
+ #ignored
98
+ end
99
+ end
100
+ @executables.reverse_each(&:stop)
101
+
102
+ wait_for_termination @config[:wait_for_termination] if @config[:wait_for_termination]
103
+
104
+ begin
105
+ @record.destroy
106
+ rescue
107
+ #ignored
108
+ end
109
+ @stop_event&.set if defined?(@stop_event)
110
+ end
111
+ true
112
+ end
113
+
114
+ # Kills the worker. Does not wait for any jobs to be completed.
115
+ # @return true
116
+ def kill
117
+ Thread.new do
118
+ @executables.reverse_each(&:stop)
119
+ @stop_event&.set if defined?(@stop_event)
120
+ end
121
+ @executor.kill
122
+ begin
123
+ @record.destroy
124
+ rescue
125
+ #ignored
126
+ end
127
+ true
128
+ end
129
+
130
+ # Blocks until the worker is stopped.
131
+ def join
132
+ @stop_event = Concurrent::Event.new
133
+ Kernel.loop do
134
+ @stop_event.wait 10
135
+ break unless running?
136
+ end
137
+ end
138
+
139
+ # Reserves and executes jobs.
140
+ def reserve_jobs
141
+ @provider.poll
142
+ end
143
+
144
+ # The worker ID.
145
+ def id
146
+ @record.id
147
+ end
148
+
149
+ def last_heartbeat
150
+ @record.last_heartbeat_at
151
+ end
152
+
153
+ def thread_stats
154
+ available = @executor.available_threads
155
+ {
156
+ minimum: @executor.minimum_threads,
157
+ maximum: @executor.maximum_threads,
158
+ available: available,
159
+ usage_percent: if @executor.running?
160
+ ((1 - (available.to_f / @executor.maximum_threads)) * 100).round(2)
161
+ end
162
+ }
163
+ end
164
+
165
+ private
166
+
167
+ def provider_options(worker_options)
168
+ worker_options.slice(:polling_interval, :polling_jitter).transform_keys do |key|
169
+ case key
170
+ when :polling_jitter
171
+ :interval_jitter
172
+ else
173
+ key
174
+ end
175
+ end
176
+ end
177
+
178
+ def listener_options(worker_options)
179
+ worker_options.slice(:queues, :set_db_connection_name)
180
+ end
181
+
182
+ def status_server_options(worker_options)
183
+ worker_options.slice(:status_server_port, :status_server_handler, :healthcheck_timeout).transform_keys do |key|
184
+ case key
185
+ when :healthcheck_timeout
186
+ :heartbeat_timeout
187
+ else
188
+ key.to_s.gsub(/^status_server_/, "").to_sym
189
+ end
190
+ end
191
+ end
192
+
193
+ # Waits for the execution threads to finish. Does nothing if +timeout+ is falsey. If +timeout+ is zero, the
194
+ # execution threads are killed immediately. If +timeout+ is a positive +Numeric+, waits for the indicated amount of
195
+ # seconds to let the execution threads finish and kills the threads if the timeout is exceeded. Otherwise; waits
196
+ # for the execution threads to finish indefinitely.
197
+ # @param timeout The time to wait.
198
+ def wait_for_termination(timeout)
199
+ if timeout.is_a?(Numeric) && timeout.zero?
200
+ @executor.kill
201
+ elsif timeout.is_a?(Numeric) && timeout.positive?
202
+ @executor.kill unless @executor.wait_for_termination timeout
203
+ elsif timeout
204
+ @executor.wait_for_termination
205
+ end
206
+ end
207
+
208
+ # Creates the active record entry for this worker.
209
+ def create_record!
210
+ info = {}
211
+ info.merge!(@config.slice(:identifier, :max_threads, :queues, :polling_interval))
212
+ Info::Worker.create!({
213
+ hostname: Socket.gethostname,
214
+ pid: Process.pid,
215
+ info: info.compact
216
+ })
217
+ end
218
+ end
219
+ end
data/lib/exekutor.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "exekutor/version"
4
+
5
+ module Exekutor
6
+
7
+ # Base error class
8
+ class Error < StandardError; end
9
+
10
+ # Error that can be raised during job execution causing the job to be discarded
11
+ class DiscardJob < StandardError; end
12
+ end
13
+
14
+ require_relative "exekutor/queue"
15
+
16
+ require_relative "exekutor/plugins"
17
+ require_relative "exekutor/configuration"
18
+ require_relative "exekutor/job_options"
19
+
20
+ require_relative "exekutor/internal/database_connection"
21
+ require_relative "exekutor/internal/logger"
22
+
23
+ require_relative "exekutor/internal/executor"
24
+ require_relative "exekutor/internal/reserver"
25
+ require_relative "exekutor/internal/provider"
26
+ require_relative "exekutor/internal/listener"
27
+ require_relative "exekutor/internal/hooks"
28
+
29
+ require_relative "exekutor/asynchronous"
30
+ require_relative "exekutor/cleanup"
31
+ require_relative "exekutor/internal/status_server"
32
+ require_relative "exekutor/hook"
33
+ require_relative "exekutor/worker"
34
+
35
+ ActiveSupport.on_load(:active_job) do
36
+ require_relative "active_job/queue_adapters/exekutor_adapter"
37
+ end
38
+
39
+ ActiveSupport.on_load(:active_record) do
40
+ # Wait until the Rails app is initialized so Exekutor.config.base_record_class can be set.
41
+ ActiveSupport.on_load(:after_initialize) do
42
+ require_relative "exekutor/info/worker"
43
+ require_relative "exekutor/job"
44
+ require_relative "exekutor/job_error"
45
+ end
46
+ end
47
+
48
+ Exekutor.private_constant "Internal"
49
+ ActiveSupport.run_load_hooks(:exekutor, Exekutor)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ require 'rails/generators'
3
+
4
+ module Exekutor
5
+ class ConfigurationGenerator < Rails::Generators::Base
6
+ desc 'Create YAML configuration for Exekutor'
7
+
8
+ class_option :identifier, type: :string, aliases: %i(--id), desc: "The worker identifier"
9
+
10
+ def create_configuration_file
11
+ config = { queues: %w[queues to watch] }.merge(Exekutor.config.worker_options)
12
+ config[:status_port] = 8765
13
+ config[:set_db_connection_name] = true
14
+ config[:wait_for_termination] = 120
15
+ create_file "config/exekutor#{".#{options[:identifier]}" if options[:identifier]}.yml", { "exekutor" => config.stringify_keys }.to_yaml
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ require 'rails/generators'
3
+ require 'rails/generators/active_record'
4
+
5
+ module Exekutor
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+ desc 'Create migrations for Exekutor'
9
+
10
+ TEMPLATE_DIR = File.join(__dir__, 'templates/install')
11
+ source_paths << TEMPLATE_DIR
12
+
13
+ def create_initializer_file
14
+ template 'initializers/exekutor.rb.erb', 'config/initializers/exekutor.rb'
15
+ end
16
+
17
+ def create_migration_file
18
+ migration_template 'migrations/create_exekutor_schema.rb.erb', File.join(db_migrate_path, 'create_exekutor_schema.rb')
19
+ if defined? Fx
20
+ %w(job_notifier requeue_orphaned_jobs).each do |function|
21
+ copy_file "functions/#{function}.sql", Fx::Definition.new(name: function, version: 1).full_path
22
+ end
23
+ %w(notify_workers requeue_orphaned_jobs).each do |trigger|
24
+ copy_file "triggers/#{trigger}.sql", Fx::Definition.new(name: trigger, version: 1, type: "trigger").full_path
25
+ end
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ def migration_version
32
+ ActiveRecord::VERSION::STRING.to_f
33
+ end
34
+
35
+ def function_sql(name)
36
+ File.read File.join(TEMPLATE_DIR, "functions/#{name}.sql")
37
+ end
38
+
39
+ def trigger_sql(name)
40
+ File.read File.join(TEMPLATE_DIR, "triggers/#{name}.sql")
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
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
@@ -0,0 +1,7 @@
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
@@ -0,0 +1,14 @@
1
+ Exekutor.configure do
2
+ # Check the README for all configuration options
3
+
4
+ # config.logger = Logger.new("log/exekutor.log")
5
+ <%= default_config = Exekutor::Configuration.new
6
+ Exekutor::Configuration.__option_names
7
+ .without(:logger, :base_record_class_name, :json_serializer, :polling_jitter,
8
+ :max_execution_thread_idletime, :status_server_handler)
9
+ .select {|name| default_config.respond_to?(name) }
10
+ .map {|name| [name, default_config.send(name)] }
11
+ .filter {|(_name, value)| value.present?}
12
+ .map {|(name, value)| "# config.#{name} = #{ value.inspect}" }
13
+ .join("\n ") %>
14
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+ class CreateExekutorSchema < ActiveRecord::Migration[<%= migration_version %>]
3
+ def change
4
+ create_table :exekutor_workers, id: :uuid do |t|
5
+ t.string :hostname, null: false, limit: 255
6
+ t.integer :pid, null: false
7
+
8
+ t.jsonb :info, null: false
9
+
10
+ t.datetime :created_at, null: false, default: -> { 'now()' }
11
+ t.datetime :last_heartbeat_at, null: false, default: -> { 'now()' }
12
+
13
+ t.column :status, :char, null: false, default: 'i'
14
+
15
+ t.index [:hostname, :pid], unique: true
16
+ end
17
+
18
+ create_table :exekutor_jobs, id: :uuid do |t|
19
+ # Worker options
20
+ t.string :queue, null: false, default: 'default', limit: 200, index: true
21
+ t.integer :priority, null: false, default: 16383, limit: 2
22
+ t.datetime :enqueued_at, null: false, default: -> { 'now()' }
23
+ t.datetime :scheduled_at, null: false, default: -> { 'now()' }
24
+
25
+ # Job options
26
+ t.uuid :active_job_id, null: false, index: true
27
+ t.jsonb :payload, null: false
28
+ t.jsonb :options
29
+
30
+ # Execution options
31
+ t.column :status, :char, index: true, null: false, default: 'p'
32
+ t.float :runtime
33
+ t.references :worker, type: :uuid, foreign_key: { to_table: :exekutor_workers, on_delete: :nullify }
34
+
35
+ t.index [:priority, :scheduled_at, :enqueued_at], where: %Q{"status"='p'}, name: :index_exekutor_jobs_on_dequeue_order
36
+ end
37
+
38
+ create_table :exekutor_job_errors, id: :uuid do |t|
39
+ t.references :job, type: :uuid, null: false, foreign_key: { to_table: :exekutor_jobs, on_delete: :cascade }
40
+ t.datetime :created_at, null: false, default: -> { 'now()' }
41
+ t.jsonb :error, null: false
42
+ end
43
+ <% if defined? Fx %>
44
+ create_function :job_notifier
45
+ create_trigger :notify_workers
46
+
47
+ create_function :requeue_orphaned_jobs
48
+ create_trigger :requeue_orphaned_jobs
49
+ <% else %>
50
+ reversible do |direction|
51
+ direction.up do
52
+ execute <<~SQL
53
+ <%= function_sql "job_notifier" %>
54
+ SQL
55
+ execute <<~SQL
56
+ <%= trigger_sql "notify_workers" %>
57
+ SQL
58
+
59
+ execute <<~SQL
60
+ <%= function_sql "requeue_orphaned_jobs" %>
61
+ SQL
62
+ execute <<~SQL
63
+ <%= trigger_sql "requeue_orphaned_jobs" %>
64
+ SQL
65
+ end
66
+ direction.down do
67
+ execute <<~SQL
68
+ DROP TRIGGER requeue_orphaned_jobs ON exekutor_workers
69
+ SQL
70
+ execute <<~SQL
71
+ DROP FUNCTION requeue_orphaned_jobs
72
+ SQL
73
+ execute <<~SQL
74
+ DROP TRIGGER notify_exekutor_workers ON exekutor_jobs
75
+ SQL
76
+ execute <<~SQL
77
+ DROP FUNCTION exekutor_job_notifier
78
+ SQL
79
+ end
80
+ end
81
+ <% end %>
82
+ end
83
+ end
@@ -0,0 +1,6 @@
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()
@@ -0,0 +1,5 @@
1
+ CREATE TRIGGER requeue_orphaned_jobs
2
+ BEFORE DELETE
3
+ ON exekutor_workers
4
+ FOR EACH ROW
5
+ EXECUTE FUNCTION requeue_orphaned_jobs()
data.tar.gz.sig ADDED
Binary file