exekutor 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/exe/exekutor +2 -2
- data/lib/active_job/queue_adapters/exekutor_adapter.rb +2 -1
- data/lib/exekutor/asynchronous.rb +143 -75
- data/lib/exekutor/cleanup.rb +27 -28
- data/lib/exekutor/configuration.rb +48 -25
- data/lib/exekutor/hook.rb +15 -11
- data/lib/exekutor/info/worker.rb +3 -3
- data/lib/exekutor/internal/base_record.rb +2 -1
- data/lib/exekutor/internal/callbacks.rb +55 -35
- data/lib/exekutor/internal/cli/app.rb +31 -23
- data/lib/exekutor/internal/cli/application_loader.rb +17 -6
- data/lib/exekutor/internal/cli/cleanup.rb +54 -40
- data/lib/exekutor/internal/cli/daemon.rb +9 -11
- data/lib/exekutor/internal/cli/default_option_value.rb +3 -1
- data/lib/exekutor/internal/cli/info.rb +117 -84
- data/lib/exekutor/internal/cli/manager.rb +190 -123
- data/lib/exekutor/internal/configuration_builder.rb +40 -27
- data/lib/exekutor/internal/database_connection.rb +6 -0
- data/lib/exekutor/internal/executable.rb +12 -7
- data/lib/exekutor/internal/executor.rb +50 -21
- data/lib/exekutor/internal/hooks.rb +11 -8
- data/lib/exekutor/internal/listener.rb +66 -39
- data/lib/exekutor/internal/logger.rb +28 -10
- data/lib/exekutor/internal/provider.rb +93 -74
- data/lib/exekutor/internal/reserver.rb +27 -12
- data/lib/exekutor/internal/status_server.rb +81 -49
- data/lib/exekutor/job.rb +1 -1
- data/lib/exekutor/job_error.rb +1 -1
- data/lib/exekutor/job_options.rb +22 -13
- data/lib/exekutor/plugins/appsignal.rb +7 -5
- data/lib/exekutor/plugins.rb +8 -4
- data/lib/exekutor/queue.rb +40 -22
- data/lib/exekutor/version.rb +1 -1
- data/lib/exekutor/worker.rb +88 -47
- data/lib/exekutor.rb +2 -2
- data/lib/generators/exekutor/configuration_generator.rb +9 -5
- data/lib/generators/exekutor/install_generator.rb +26 -15
- data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +11 -10
- data.tar.gz.sig +0 -0
- metadata +63 -19
- metadata.gz.sig +0 -0
@@ -8,16 +8,18 @@ module Exekutor
|
|
8
8
|
module Internal
|
9
9
|
# Reserves jobs and provides them to an executor
|
10
10
|
class Provider
|
11
|
-
include
|
11
|
+
include Logger
|
12
|
+
include Callbacks
|
13
|
+
include Executable
|
12
14
|
|
13
15
|
define_callbacks :on_queue_empty, freeze: true
|
14
16
|
|
15
17
|
# Represents an unknown value
|
16
18
|
UNKNOWN = Object.new.freeze
|
17
|
-
private_constant
|
19
|
+
private_constant :UNKNOWN
|
18
20
|
|
19
21
|
MAX_WAIT_TIMEOUT = 300
|
20
|
-
private_constant
|
22
|
+
private_constant :MAX_WAIT_TIMEOUT
|
21
23
|
|
22
24
|
# Creates a new provider
|
23
25
|
# @param reserver [Reserver] the job reserver
|
@@ -48,14 +50,14 @@ module Exekutor
|
|
48
50
|
|
49
51
|
# Always poll at startup to fill up threads, use small jitter so workers started at the same time dont hit
|
50
52
|
# the db at the same time
|
51
|
-
@next_poll_at.set (1 + 2 * Kernel.rand).
|
53
|
+
@next_poll_at.set (1 + (2 * Kernel.rand)).seconds.from_now.to_f
|
52
54
|
start_thread
|
53
55
|
true
|
54
56
|
end
|
55
57
|
|
56
58
|
# Stops the provider
|
57
59
|
def stop
|
58
|
-
|
60
|
+
self.state = :stopped
|
59
61
|
@event.set
|
60
62
|
end
|
61
63
|
|
@@ -63,7 +65,7 @@ module Exekutor
|
|
63
65
|
def poll
|
64
66
|
raise Exekutor::Error, "Provider is not running" unless running?
|
65
67
|
|
66
|
-
@next_poll_at.set Time.now
|
68
|
+
@next_poll_at.set Time.now.to_f
|
67
69
|
@event.set
|
68
70
|
end
|
69
71
|
|
@@ -71,51 +73,39 @@ module Exekutor
|
|
71
73
|
# argument is given. Updates the timestamp for the earliest job is a timestamp is given and that timestamp is
|
72
74
|
# before the known timestamp. Does nothing if a timestamp is given and the earliest job timestamp is not known.
|
73
75
|
# @param scheduled_at [Time,Numeric] the time a job is scheduled at
|
74
|
-
# @return [
|
76
|
+
# @return [Float,nil] the timestamp for the next job, or +nil+ if the timestamp is unknown or no jobs are pending
|
75
77
|
def update_earliest_scheduled_at(scheduled_at = UNKNOWN)
|
76
|
-
|
77
|
-
|
78
|
-
when UNKNOWN
|
79
|
-
# If we fetch the value from the DB, we can safely overwrite the UNKNOWN value
|
80
|
-
overwrite_unknown = true
|
81
|
-
scheduled_at = @reserver.earliest_scheduled_at
|
82
|
-
when Numeric
|
83
|
-
scheduled_at = Time.at(scheduled_at)
|
84
|
-
when Time
|
85
|
-
# All good
|
86
|
-
else
|
78
|
+
scheduled_at = scheduled_at.to_f if scheduled_at.is_a? Time
|
79
|
+
unless scheduled_at == UNKNOWN || scheduled_at.is_a?(Numeric)
|
87
80
|
raise ArgumentError, "scheduled_at must be a Time or Numeric"
|
88
81
|
end
|
89
82
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
scheduled_at
|
96
|
-
else
|
97
|
-
current
|
98
|
-
end
|
99
|
-
elsif current.nil? || scheduled_at.nil? || current > scheduled_at
|
100
|
-
updated = true
|
101
|
-
scheduled_at
|
102
|
-
else
|
103
|
-
current
|
104
|
-
end
|
83
|
+
changed = false
|
84
|
+
earliest_scheduled_at = @next_job_scheduled_at.update do |current|
|
85
|
+
earliest = ScheduledAtComparer.determine_earliest(current, scheduled_at, @reserver)
|
86
|
+
changed = earliest != current
|
87
|
+
earliest
|
105
88
|
end
|
106
|
-
|
89
|
+
|
90
|
+
if earliest_scheduled_at == UNKNOWN
|
107
91
|
nil
|
108
92
|
else
|
109
|
-
@event.set if
|
110
|
-
|
93
|
+
@event.set if changed && earliest_scheduled_at.present?
|
94
|
+
earliest_scheduled_at
|
111
95
|
end
|
112
96
|
end
|
113
97
|
|
114
98
|
private
|
115
99
|
|
116
100
|
# Starts the provision thread
|
117
|
-
def start_thread
|
118
|
-
|
101
|
+
def start_thread(delay: nil)
|
102
|
+
return unless running?
|
103
|
+
|
104
|
+
if delay
|
105
|
+
Concurrent::ScheduledTask.execute(delay, executor: @pool) { run }
|
106
|
+
else
|
107
|
+
@pool.post { run }
|
108
|
+
end
|
119
109
|
end
|
120
110
|
|
121
111
|
# Does the provisioning of jobs to the executor. Blocks until the provider is stopped.
|
@@ -134,19 +124,24 @@ module Exekutor
|
|
134
124
|
consecutive_errors.value = 0
|
135
125
|
end
|
136
126
|
end
|
137
|
-
rescue StandardError =>
|
138
|
-
|
139
|
-
consecutive_errors.increment
|
140
|
-
if running?
|
141
|
-
delay = restart_delay
|
142
|
-
logger.info "Restarting in %0.1f seconds…" % [delay]
|
143
|
-
Concurrent::ScheduledTask.execute(delay, executor: @pool, &method(:run))
|
144
|
-
end
|
127
|
+
rescue StandardError => e
|
128
|
+
on_thread_error(e)
|
145
129
|
ensure
|
146
130
|
BaseRecord.connection_pool.release_connection
|
147
131
|
@thread_running.make_false
|
148
132
|
end
|
149
133
|
|
134
|
+
# Called when an error is raised in #run
|
135
|
+
def on_thread_error(error)
|
136
|
+
Exekutor.on_fatal_error error, "[Provider] Runtime error!"
|
137
|
+
return unless running?
|
138
|
+
|
139
|
+
consecutive_errors.increment
|
140
|
+
delay = restart_delay
|
141
|
+
logger.info format("Restarting in %0.1f seconds…", delay)
|
142
|
+
start_thread delay: delay
|
143
|
+
end
|
144
|
+
|
150
145
|
# Waits for any event to happen. An event could be:
|
151
146
|
# - The listener was notified of a new job;
|
152
147
|
# - The next job is scheduled for the current time;
|
@@ -157,8 +152,8 @@ module Exekutor
|
|
157
152
|
return unless timeout.positive?
|
158
153
|
|
159
154
|
@event.wait timeout
|
160
|
-
rescue StandardError =>
|
161
|
-
Exekutor.on_fatal_error
|
155
|
+
rescue StandardError => e
|
156
|
+
Exekutor.on_fatal_error e, "[Provider] An error occurred while waiting"
|
162
157
|
sleep 0.1 if running?
|
163
158
|
ensure
|
164
159
|
throw :shutdown unless running?
|
@@ -171,21 +166,8 @@ module Exekutor
|
|
171
166
|
return unless available_workers.positive?
|
172
167
|
|
173
168
|
jobs = @reserver.reserve available_workers
|
174
|
-
unless jobs.nil?
|
175
|
-
|
176
|
-
logger.debug "Reserved #{jobs.size} job(s)"
|
177
|
-
jobs.each(&@executor.method(:post))
|
178
|
-
rescue Exception # rubocop:disable Lint/RescueException
|
179
|
-
# Try to release all jobs before re-raising
|
180
|
-
begin
|
181
|
-
Exekutor::Job.where(id: jobs.collect { |job| job[:id] }, status: "e")
|
182
|
-
.update_all(status: "p", worker_id: nil)
|
183
|
-
rescue # rubocop:disable Lint/RescueStandardError
|
184
|
-
# ignored
|
185
|
-
end
|
186
|
-
raise
|
187
|
-
end
|
188
|
-
end
|
169
|
+
execute_jobs jobs unless jobs.nil?
|
170
|
+
|
189
171
|
if jobs.nil? || jobs.size.to_i < available_workers
|
190
172
|
# If we ran out of work, update the earliest scheduled at
|
191
173
|
update_earliest_scheduled_at
|
@@ -194,8 +176,24 @@ module Exekutor
|
|
194
176
|
|
195
177
|
elsif @next_job_scheduled_at.get == UNKNOWN
|
196
178
|
# If the next job timestamp is still unknown, set it to now to indicate there's still work to do
|
197
|
-
@next_job_scheduled_at.set Time.now
|
179
|
+
@next_job_scheduled_at.set Time.now.to_f
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Posts the given jobs to the executor thread pool
|
184
|
+
# @param jobs [Array] the jobs to execute
|
185
|
+
def execute_jobs(jobs)
|
186
|
+
logger.debug "Reserved #{jobs.size} job(s)"
|
187
|
+
jobs.each { |job| @executor.post(job) }
|
188
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
189
|
+
# Try to release all jobs before re-raising
|
190
|
+
begin
|
191
|
+
Exekutor::Job.where(id: jobs.pluck(:id), status: "e")
|
192
|
+
.update_all(status: "p", worker_id: nil)
|
193
|
+
rescue StandardError
|
194
|
+
# ignored
|
198
195
|
end
|
196
|
+
raise
|
199
197
|
end
|
200
198
|
|
201
199
|
def perform_pending_job_updates
|
@@ -224,7 +222,7 @@ module Exekutor
|
|
224
222
|
return if jobs&.size.to_i.zero?
|
225
223
|
|
226
224
|
logger.info "Restarting #{jobs.size} abandoned job#{"s" if jobs.size > 1}"
|
227
|
-
jobs.each
|
225
|
+
jobs.each { |job| @executor.post(job) }
|
228
226
|
end
|
229
227
|
|
230
228
|
# @return [Boolean] Whether the polling is enabled. Ie. whether a polling interval is set.
|
@@ -232,7 +230,7 @@ module Exekutor
|
|
232
230
|
@polling_interval.present?
|
233
231
|
end
|
234
232
|
|
235
|
-
# @return [
|
233
|
+
# @return [Float,nil] the 'scheduled at' value for the next job, or nil if unknown or if there is no pending job
|
236
234
|
def next_job_scheduled_at
|
237
235
|
at = @next_job_scheduled_at.get
|
238
236
|
if at == UNKNOWN
|
@@ -243,10 +241,10 @@ module Exekutor
|
|
243
241
|
end
|
244
242
|
end
|
245
243
|
|
246
|
-
# @return [
|
244
|
+
# @return [Float,nil] When the next poll is scheduled, or nil if polling is disabled
|
247
245
|
def next_poll_scheduled_at
|
248
246
|
if polling_enabled?
|
249
|
-
@next_poll_at.update { |planned_at| planned_at || Time.now + polling_interval }
|
247
|
+
@next_poll_at.update { |planned_at| planned_at || (Time.now.to_f + polling_interval) }
|
250
248
|
else
|
251
249
|
# noinspection RubyMismatchedReturnType
|
252
250
|
@next_poll_at.get
|
@@ -262,9 +260,9 @@ module Exekutor
|
|
262
260
|
|
263
261
|
timeout = [MAX_WAIT_TIMEOUT].tap do |timeouts|
|
264
262
|
# noinspection RubyMismatchedArgumentType
|
265
|
-
timeouts.append next_job_at - Time.now if next_job_at
|
263
|
+
timeouts.append next_job_at - Time.now.to_f if next_job_at
|
266
264
|
# noinspection RubyMismatchedArgumentType
|
267
|
-
timeouts.append next_poll_at - Time.now if next_poll_at
|
265
|
+
timeouts.append next_poll_at - Time.now.to_f if next_poll_at
|
268
266
|
end.min
|
269
267
|
|
270
268
|
if timeout <= 0.001
|
@@ -278,13 +276,13 @@ module Exekutor
|
|
278
276
|
# @return [Boolean] Whether the `reserver` should be called.
|
279
277
|
def reserve_jobs_now?
|
280
278
|
next_poll_at = next_poll_scheduled_at
|
281
|
-
if next_poll_at && next_poll_at - Time.now <= 0.001
|
282
|
-
@next_poll_at.update { Time.now + polling_interval if polling_enabled? }
|
279
|
+
if next_poll_at && next_poll_at - Time.now.to_f <= 0.001
|
280
|
+
@next_poll_at.update { Time.now.to_f + polling_interval if polling_enabled? }
|
283
281
|
return true
|
284
282
|
end
|
285
283
|
|
286
284
|
next_job_at = next_job_scheduled_at
|
287
|
-
next_job_at && next_job_at <= Time.now
|
285
|
+
next_job_at && next_job_at <= Time.now.to_f
|
288
286
|
end
|
289
287
|
|
290
288
|
# @return [Float] Gets the polling interval jitter
|
@@ -295,7 +293,7 @@ module Exekutor
|
|
295
293
|
# Get the polling interval. If a jitter is configured, the interval is reduced or increased by `0.5 * jitter`.
|
296
294
|
# @return [Float] The amount of seconds before the next poll
|
297
295
|
def polling_interval
|
298
|
-
raise "Polling is disabled"
|
296
|
+
raise "Polling is disabled" if @polling_interval.blank?
|
299
297
|
|
300
298
|
@polling_interval + if polling_interval_jitter.zero?
|
301
299
|
0
|
@@ -303,6 +301,27 @@ module Exekutor
|
|
303
301
|
(Kernel.rand - 0.5) * polling_interval_jitter
|
304
302
|
end
|
305
303
|
end
|
304
|
+
|
305
|
+
# Abstraction to determine earliest out of 2 scheduled at timestamps
|
306
|
+
class ScheduledAtComparer
|
307
|
+
def self.determine_earliest(current_value, other_value, reserver)
|
308
|
+
if other_value == UNKNOWN
|
309
|
+
# Fetch the value from the db
|
310
|
+
reserver.earliest_scheduled_at&.to_f
|
311
|
+
elsif current_value == UNKNOWN
|
312
|
+
# We don't know the value yet, don't use the given value as earliest unless work is scheduled immediately
|
313
|
+
other_value > Time.now.to_f ? UNKNOWN : other_value
|
314
|
+
elsif current_value.nil? || current_value > other_value
|
315
|
+
# The given value is earlier than the known value
|
316
|
+
other_value
|
317
|
+
else
|
318
|
+
# No changes
|
319
|
+
current_value
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
private_constant :ScheduledAtComparer
|
306
325
|
end
|
307
326
|
end
|
308
327
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Exekutor
|
3
4
|
# @private
|
4
5
|
module Internal
|
@@ -18,7 +19,7 @@ module Exekutor
|
|
18
19
|
|
19
20
|
# Reserves pending jobs
|
20
21
|
# @param limit [Integer] the number of jobs to reserve
|
21
|
-
# @return [Array<
|
22
|
+
# @return [Array<Hash>,nil] the reserved jobs, or nil if no jobs were reserved
|
22
23
|
def reserve(limit)
|
23
24
|
return unless limit.positive?
|
24
25
|
|
@@ -36,6 +37,9 @@ module Exekutor
|
|
36
37
|
parse_jobs results
|
37
38
|
end
|
38
39
|
|
40
|
+
# Gets the jobs that are assigned to this worker and have an id that is not included in +active_job_ids+
|
41
|
+
# @param active_job_ids [Array<String>] The ids of the jobs that should be excluded
|
42
|
+
# @return [Array<Hash>] the jobs
|
39
43
|
def get_abandoned_jobs(active_job_ids)
|
40
44
|
jobs = Exekutor::Job.executing.where(worker_id: @worker_id)
|
41
45
|
jobs = jobs.where.not(id: active_job_ids) if active_job_ids.present?
|
@@ -59,7 +63,7 @@ module Exekutor
|
|
59
63
|
{ id: result["id"],
|
60
64
|
payload: parse_json(result["payload"]),
|
61
65
|
options: parse_json(result["options"]),
|
62
|
-
scheduled_at: result[
|
66
|
+
scheduled_at: result["scheduled_at"] }
|
63
67
|
end
|
64
68
|
end
|
65
69
|
|
@@ -71,25 +75,36 @@ module Exekutor
|
|
71
75
|
# Builds SQL filter for the given queues
|
72
76
|
def build_queue_filter_sql(queues)
|
73
77
|
return nil if queues.nil? || (queues.is_a?(Array) && queues.empty?)
|
74
|
-
unless queues.is_a?(String) || queues.is_a?(Symbol) || queues.is_a?(Array)
|
75
|
-
raise ArgumentError, "queues must be nil, a String, Symbol, or an array of Strings or Symbols"
|
76
|
-
end
|
77
78
|
|
78
79
|
queues = queues.first if queues.is_a?(Array) && queues.one?
|
79
|
-
|
80
|
-
unless queues.all? { |q| (q.is_a?(String) || q.is_a?(Symbol)) && !q.blank? }
|
81
|
-
raise ArgumentError, "queues contains an invalid value"
|
82
|
-
end
|
80
|
+
validate_queues! queues
|
83
81
|
|
82
|
+
if queues.is_a? Array
|
84
83
|
Exekutor::Job.sanitize_sql_for_conditions(["AND queue IN (?)", queues])
|
85
84
|
else
|
86
|
-
raise ArgumentError, "queue name cannot be empty" if queues.blank?
|
87
|
-
|
88
85
|
Exekutor::Job.sanitize_sql_for_conditions(["AND queue = ?", queues])
|
89
86
|
end
|
90
87
|
end
|
91
88
|
|
89
|
+
# Raises an error if the queues value is invalid
|
90
|
+
# @param queues [String,Symbol,Array<String,Symbol>] the queues to validate
|
91
|
+
# @raise [ArgumentError] if the queue is invalid or includes an invalid value
|
92
|
+
def validate_queues!(queues)
|
93
|
+
case queues
|
94
|
+
when Array
|
95
|
+
raise ArgumentError, "queues contains an invalid value" unless queues.all? { |queue| valid_queue_name? queue }
|
96
|
+
when String, Symbol
|
97
|
+
raise ArgumentError, "queue name cannot be empty" unless valid_queue_name? queues
|
98
|
+
else
|
99
|
+
raise ArgumentError, "queues must be nil, a String, Symbol, or an array of Strings or Symbols"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param queue [String,Symbol] the name of a queue
|
104
|
+
# @return [Boolean] whether the name is a valid queue name
|
105
|
+
def valid_queue_name?(queue)
|
106
|
+
(queue.is_a?(String) || queue.is_a?(Symbol)) && queue.present? && queue.length <= Queue::MAX_NAME_LENGTH
|
107
|
+
end
|
92
108
|
end
|
93
109
|
end
|
94
110
|
end
|
95
|
-
|
@@ -1,7 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Exekutor
|
3
4
|
module Internal
|
4
|
-
# Serves a simple health check app
|
5
|
+
# Serves a simple health check app. The app provides 4 endpoints:
|
6
|
+
# - +/+, which lists the other endpoints;
|
7
|
+
# - +/ready+, which indicates whether the worker is ready to start work;
|
8
|
+
# - +/live+, which indicates whether the worker is ready and whether the worker is still alive;
|
9
|
+
# - +/threads+, which indicated the thread usage of the worker.
|
10
|
+
#
|
11
|
+
# Please note that this server uses +webrick+ by default, which is no longer a default gem from ruby 3.0 onwards.
|
12
|
+
#
|
13
|
+
# === Example requests
|
14
|
+
# $ curl localhost:9000/ready
|
15
|
+
# [OK] ID: f1a2ee6a-cdac-459c-a4b8-de7c6a8bbae6; State: started
|
16
|
+
# $ curl localhost:9000/live
|
17
|
+
# [OK] ID: f1a2ee6a-cdac-459c-a4b8-de7c6a8bbae6; State: started; Heartbeat: 2023-04-05T16:27:00Z
|
18
|
+
# $ curl localhost:9000/threads
|
19
|
+
# {"minimum":1,"maximum":10,"available":4,"usage_percent":60.0}
|
5
20
|
class StatusServer
|
6
21
|
include Internal::Logger
|
7
22
|
include Internal::Executable
|
@@ -19,24 +34,27 @@ module Exekutor
|
|
19
34
|
@server = Concurrent::AtomicReference.new
|
20
35
|
end
|
21
36
|
|
37
|
+
# Starts the web server
|
22
38
|
def start
|
23
39
|
return false unless compare_and_set_state :pending, :started
|
24
40
|
|
25
41
|
start_thread
|
26
42
|
end
|
27
43
|
|
44
|
+
# @return [Boolean] whether the web server is active
|
28
45
|
def running?
|
29
46
|
super && @thread_running.value
|
30
47
|
end
|
31
48
|
|
49
|
+
# Stops the web server
|
32
50
|
def stop
|
33
|
-
|
51
|
+
self.state = :stopped
|
34
52
|
return unless @thread_running.value
|
35
53
|
|
36
54
|
server = @server.value
|
37
|
-
if server
|
55
|
+
if server.respond_to? :shutdown
|
38
56
|
server.shutdown
|
39
|
-
elsif server
|
57
|
+
elsif server.respond_to? :stop
|
40
58
|
server.stop
|
41
59
|
elsif server
|
42
60
|
Exekutor.say! "Cannot shutdown status server, #{server.class.name} does not respond to shutdown or stop"
|
@@ -45,6 +63,7 @@ module Exekutor
|
|
45
63
|
|
46
64
|
protected
|
47
65
|
|
66
|
+
# Runs the web server, should be called from a separate thread
|
48
67
|
def run(worker, port)
|
49
68
|
return unless state == :started && @thread_running.make_true
|
50
69
|
|
@@ -53,11 +72,11 @@ module Exekutor
|
|
53
72
|
Logger: ::Logger.new(File.open(File::NULL, "w")), AccessLog: []) do |server|
|
54
73
|
@server.set server
|
55
74
|
end
|
56
|
-
rescue StandardError =>
|
57
|
-
Exekutor.on_fatal_error
|
75
|
+
rescue StandardError => e
|
76
|
+
Exekutor.on_fatal_error e, "[Status server] Runtime error!"
|
58
77
|
if running?
|
59
78
|
logger.info "Restarting in 10 seconds…"
|
60
|
-
Concurrent::ScheduledTask.execute(10.0, executor: @pool
|
79
|
+
Concurrent::ScheduledTask.execute(10.0, executor: @pool) { start_thread }
|
61
80
|
end
|
62
81
|
ensure
|
63
82
|
@thread_running.make_false
|
@@ -65,68 +84,81 @@ module Exekutor
|
|
65
84
|
|
66
85
|
# The Rack-app for the health-check server
|
67
86
|
class App
|
68
|
-
|
69
87
|
def initialize(worker, heartbeat_timeout)
|
70
88
|
@worker = worker
|
71
89
|
@heartbeat_timeout = heartbeat_timeout
|
72
90
|
end
|
73
91
|
|
74
|
-
def flatlined?
|
75
|
-
last_heartbeat = @worker.last_heartbeat
|
76
|
-
last_heartbeat.nil? || last_heartbeat < @heartbeat_timeout.minutes.ago
|
77
|
-
end
|
78
|
-
|
79
92
|
def call(env)
|
80
93
|
case Rack::Request.new(env).path
|
81
94
|
when "/"
|
82
|
-
|
83
|
-
<<~RESPONSE
|
84
|
-
[Exekutor]
|
85
|
-
- Use GET /ready to check whether the worker is running and connected to the DB
|
86
|
-
- Use GET /live to check whether the worker is running and is not hanging
|
87
|
-
- Use GET /threads to check thread usage
|
88
|
-
RESPONSE
|
89
|
-
]]
|
95
|
+
render_root
|
90
96
|
when "/ready"
|
91
|
-
|
92
|
-
if running
|
93
|
-
Exekutor::Job.connection_pool.with_connection do |connection|
|
94
|
-
running = connection.active?
|
95
|
-
end
|
96
|
-
end
|
97
|
-
running = false if running && flatlined?
|
98
|
-
[(running ? 200 : 503), { "Content-Type" => "text/plain" }, [
|
99
|
-
"#{running ? "[OK]" : "[Service unavailable]"} ID: #{@worker.id}; State: #{@worker.state}"
|
100
|
-
]]
|
97
|
+
render_ready
|
101
98
|
when "/live"
|
102
|
-
|
103
|
-
last_heartbeat = if running
|
104
|
-
@worker.last_heartbeat
|
105
|
-
end
|
106
|
-
if running && (last_heartbeat.nil? || last_heartbeat < @heartbeat_timeout.minutes.ago)
|
107
|
-
running = false
|
108
|
-
end
|
109
|
-
[(running ? 200 : 503), { "Content-Type" => "text/plain" }, [
|
110
|
-
"#{running ? "[OK]" : "[Service unavailable]"} ID: #{@worker.id}; State: #{@worker.state}; Heartbeat: #{last_heartbeat&.iso8601 || "null"}"
|
111
|
-
]]
|
99
|
+
render_live
|
112
100
|
when "/threads"
|
113
|
-
|
114
|
-
info = @worker.thread_stats
|
115
|
-
[(info ? 200 : 503), { "Content-Type" => "application/json" }, [info.to_json]]
|
116
|
-
else
|
117
|
-
[503, {"Content-Type" => "application/json"}, [{ error: "Worker not running" }.to_json]]
|
118
|
-
end
|
101
|
+
render_threads
|
119
102
|
else
|
120
103
|
[404, {}, ["Not found"]]
|
121
104
|
end
|
122
105
|
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def flatlined?(last_heartbeat = @worker.last_heartbeat)
|
110
|
+
last_heartbeat.nil? || last_heartbeat < @heartbeat_timeout.minutes.ago
|
111
|
+
end
|
112
|
+
|
113
|
+
def render_threads
|
114
|
+
if @worker.running?
|
115
|
+
info = @worker.thread_stats
|
116
|
+
[(info ? 200 : 503), { "Content-Type" => "application/json" }, [info.to_json]]
|
117
|
+
else
|
118
|
+
[503, { "Content-Type" => "application/json" }, [{ error: "Worker not running" }.to_json]]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_live
|
123
|
+
running = @worker.running?
|
124
|
+
last_heartbeat = (@worker.last_heartbeat if running)
|
125
|
+
running = false if flatlined?(last_heartbeat)
|
126
|
+
[(running ? 200 : 503), { "Content-Type" => "text/plain" }, [
|
127
|
+
"#{running ? "[OK]" : "[Service unavailable]"} ID: #{@worker.id}; State: #{@worker.state}; " \
|
128
|
+
"Heartbeat: #{last_heartbeat&.iso8601 || "null"}"
|
129
|
+
]]
|
130
|
+
end
|
131
|
+
|
132
|
+
def render_ready
|
133
|
+
running = @worker.running?
|
134
|
+
if running
|
135
|
+
Exekutor::Job.connection_pool.with_connection do |connection|
|
136
|
+
running = connection.active?
|
137
|
+
end
|
138
|
+
end
|
139
|
+
running = false if running && flatlined?
|
140
|
+
[(running ? 200 : 503), { "Content-Type" => "text/plain" }, [
|
141
|
+
"#{running ? "[OK]" : "[Service unavailable]"} ID: #{@worker.id}; State: #{@worker.state}"
|
142
|
+
]]
|
143
|
+
end
|
144
|
+
|
145
|
+
def render_root
|
146
|
+
[200, {}, [
|
147
|
+
<<~RESPONSE
|
148
|
+
[Exekutor]
|
149
|
+
- Use GET /ready to check whether the worker is running and connected to the DB
|
150
|
+
- Use GET /live to check whether the worker is running and is not hanging
|
151
|
+
- Use GET /threads to check thread usage
|
152
|
+
RESPONSE
|
153
|
+
]]
|
154
|
+
end
|
123
155
|
end
|
124
156
|
|
125
157
|
private
|
126
158
|
|
127
159
|
def start_thread
|
128
|
-
@pool.post(@worker, @port
|
160
|
+
@pool.post(@worker, @port) { |*args| run(*args) } if state == :started
|
129
161
|
end
|
130
162
|
end
|
131
163
|
end
|
132
|
-
end
|
164
|
+
end
|
data/lib/exekutor/job.rb
CHANGED
data/lib/exekutor/job_error.rb
CHANGED
data/lib/exekutor/job_options.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Exekutor
|
2
4
|
# Mixin which defines custom job options for Exekutor. This module should be included in your job class.
|
3
5
|
# You can define the following options after including this module:
|
@@ -32,7 +34,7 @@ module Exekutor
|
|
32
34
|
|
33
35
|
# @private
|
34
36
|
VALID_EXEKUTOR_OPTIONS = %i[queue_timeout execution_timeout].freeze
|
35
|
-
private_constant
|
37
|
+
private_constant :VALID_EXEKUTOR_OPTIONS
|
36
38
|
|
37
39
|
# @return [Hash<Symbol, Object>] the exekutor options for this job
|
38
40
|
attr_reader :exekutor_options
|
@@ -67,28 +69,35 @@ module Exekutor
|
|
67
69
|
end
|
68
70
|
|
69
71
|
# Validates the exekutor job options passed to {#exekutor_options} and +#set+
|
70
|
-
# @param options [Hash<Symbol,
|
72
|
+
# @param options [Hash<Symbol, Any>] the options to validate
|
71
73
|
# @raise [InvalidOption] if any of the options are invalid
|
72
74
|
# @private
|
73
75
|
# @return [void]
|
74
76
|
def validate_exekutor_options!(options)
|
75
|
-
return
|
77
|
+
return true if options.blank?
|
76
78
|
|
77
|
-
invalid_options = options.keys - VALID_EXEKUTOR_OPTIONS
|
78
|
-
if invalid_options.present?
|
79
|
+
if (invalid_options = options.keys - VALID_EXEKUTOR_OPTIONS).present?
|
79
80
|
raise InvalidOption, "Invalid option#{"s" if invalid_options.many?}: " \
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
if options[:queue_timeout]
|
84
|
-
raise InvalidOption, ":queue_timeout must be an interval" unless options[:queue_timeout].is_a? ActiveSupport::Duration
|
85
|
-
end
|
86
|
-
if options[:execution_timeout]
|
87
|
-
raise InvalidOption, ":execution_timeout must be an interval" unless options[:execution_timeout].is_a? ActiveSupport::Duration
|
81
|
+
"#{invalid_options.map(&:inspect).join(", ")}. " \
|
82
|
+
"Valid options are: #{VALID_EXEKUTOR_OPTIONS.map(&:inspect).join(", ")}"
|
88
83
|
end
|
84
|
+
JobOptions.validate_option_type! options, :queue_timeout, ::ActiveSupport::Duration
|
85
|
+
JobOptions.validate_option_type! options, :execution_timeout, ::ActiveSupport::Duration
|
86
|
+
true
|
89
87
|
end
|
90
88
|
end
|
91
89
|
|
90
|
+
# Validates the type of an option
|
91
|
+
# @param options [Hash<Symbol, Any>] the options
|
92
|
+
# @param name [Symbol] the name of the option to validate
|
93
|
+
# @param valid_type [Class] the valid type for the value
|
94
|
+
# @raise [InvalidOption] if the configured value is not an instance of +valid_type+
|
95
|
+
def self.validate_option_type!(options, name, valid_type)
|
96
|
+
return if options[name].nil? || options[name].is_a?(valid_type)
|
97
|
+
|
98
|
+
raise InvalidOption, ":#{name} must be an instance of #{valid_type.name} (given: #{options[name].class.name})"
|
99
|
+
end
|
100
|
+
|
92
101
|
# Raised when invalid options are given
|
93
102
|
class InvalidOption < ::Exekutor::Error; end
|
94
103
|
end
|