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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data/LICENSE.txt +21 -0
- data/exe/exekutor +7 -0
- data/lib/active_job/queue_adapters/exekutor_adapter.rb +14 -0
- data/lib/exekutor/asynchronous.rb +188 -0
- data/lib/exekutor/cleanup.rb +56 -0
- data/lib/exekutor/configuration.rb +373 -0
- data/lib/exekutor/hook.rb +172 -0
- data/lib/exekutor/info/worker.rb +20 -0
- data/lib/exekutor/internal/base_record.rb +11 -0
- data/lib/exekutor/internal/callbacks.rb +138 -0
- data/lib/exekutor/internal/cli/app.rb +173 -0
- data/lib/exekutor/internal/cli/application_loader.rb +36 -0
- data/lib/exekutor/internal/cli/cleanup.rb +96 -0
- data/lib/exekutor/internal/cli/daemon.rb +108 -0
- data/lib/exekutor/internal/cli/default_option_value.rb +29 -0
- data/lib/exekutor/internal/cli/info.rb +126 -0
- data/lib/exekutor/internal/cli/manager.rb +260 -0
- data/lib/exekutor/internal/configuration_builder.rb +113 -0
- data/lib/exekutor/internal/database_connection.rb +21 -0
- data/lib/exekutor/internal/executable.rb +75 -0
- data/lib/exekutor/internal/executor.rb +242 -0
- data/lib/exekutor/internal/hooks.rb +87 -0
- data/lib/exekutor/internal/listener.rb +176 -0
- data/lib/exekutor/internal/logger.rb +74 -0
- data/lib/exekutor/internal/provider.rb +308 -0
- data/lib/exekutor/internal/reserver.rb +95 -0
- data/lib/exekutor/internal/status_server.rb +132 -0
- data/lib/exekutor/job.rb +31 -0
- data/lib/exekutor/job_error.rb +11 -0
- data/lib/exekutor/job_options.rb +95 -0
- data/lib/exekutor/plugins/appsignal.rb +46 -0
- data/lib/exekutor/plugins.rb +13 -0
- data/lib/exekutor/queue.rb +141 -0
- data/lib/exekutor/version.rb +6 -0
- data/lib/exekutor/worker.rb +219 -0
- data/lib/exekutor.rb +49 -0
- data/lib/generators/exekutor/configuration_generator.rb +18 -0
- data/lib/generators/exekutor/install_generator.rb +43 -0
- data/lib/generators/exekutor/templates/install/functions/job_notifier.sql +7 -0
- data/lib/generators/exekutor/templates/install/functions/requeue_orphaned_jobs.sql +7 -0
- data/lib/generators/exekutor/templates/install/initializers/exekutor.rb.erb +14 -0
- data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +83 -0
- data/lib/generators/exekutor/templates/install/triggers/notify_workers.sql +6 -0
- data/lib/generators/exekutor/templates/install/triggers/requeue_orphaned_jobs.sql +5 -0
- data.tar.gz.sig +0 -0
- metadata +403 -0
- 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,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,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
|
data.tar.gz.sig
ADDED
Binary file
|