exekutor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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