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