exekutor 0.1.0 → 0.1.2
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 +2 -3
- 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 +102 -48
- 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 +33 -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 +234 -123
- data/lib/exekutor/internal/configuration_builder.rb +49 -30
- 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 +85 -43
- data/lib/exekutor/internal/logger.rb +29 -10
- data/lib/exekutor/internal/provider.rb +96 -77
- data/lib/exekutor/internal/reserver.rb +66 -19
- data/lib/exekutor/internal/status_server.rb +87 -54
- 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 +69 -30
- data/lib/exekutor/version.rb +1 -1
- data/lib/exekutor/worker.rb +89 -48
- data/lib/exekutor.rb +2 -2
- data/lib/generators/exekutor/configuration_generator.rb +11 -6
- data/lib/generators/exekutor/install_generator.rb +24 -15
- data/lib/generators/exekutor/templates/install/functions/exekutor_broadcast_job_enqueued.sql +10 -0
- data/lib/generators/exekutor/templates/install/functions/exekutor_requeue_orphaned_jobs.sql +11 -0
- data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +23 -22
- data/lib/generators/exekutor/templates/install/triggers/exekutor_broadcast_job_enqueued.sql +7 -0
- data/lib/generators/exekutor/templates/install/triggers/exekutor_requeue_orphaned_jobs.sql +5 -0
- data.tar.gz.sig +0 -0
- metadata +67 -23
- metadata.gz.sig +0 -0
- data/lib/generators/exekutor/templates/install/functions/job_notifier.sql +0 -7
- data/lib/generators/exekutor/templates/install/functions/requeue_orphaned_jobs.sql +0 -7
- data/lib/generators/exekutor/templates/install/triggers/notify_workers.sql +0 -6
- data/lib/generators/exekutor/templates/install/triggers/requeue_orphaned_jobs.sql +0 -5
@@ -1,14 +1,29 @@
|
|
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
|
8
23
|
|
9
24
|
DEFAULT_HANDLER = "webrick"
|
10
25
|
|
11
|
-
def initialize(worker:, pool:, port:, handler: DEFAULT_HANDLER, heartbeat_timeout: 30)
|
26
|
+
def initialize(worker:, pool:, port:, handler: DEFAULT_HANDLER, heartbeat_timeout: 30.minutes)
|
12
27
|
super()
|
13
28
|
@worker = worker
|
14
29
|
@pool = pool
|
@@ -19,45 +34,50 @@ 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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
server
|
55
|
+
shutdown_method = %i[shutdown stop].find { |method| server.respond_to? method }
|
56
|
+
if shutdown_method
|
57
|
+
server.send(shutdown_method)
|
58
|
+
Exekutor.say "Status server stopped"
|
41
59
|
elsif server
|
42
|
-
Exekutor.
|
60
|
+
Exekutor.print_error "Cannot shutdown status server, " \
|
61
|
+
"#{server.class.name} does not respond to `shutdown` or `stop`"
|
43
62
|
end
|
44
63
|
end
|
45
64
|
|
46
65
|
protected
|
47
66
|
|
67
|
+
# Runs the web server, should be called from a separate thread
|
48
68
|
def run(worker, port)
|
49
69
|
return unless state == :started && @thread_running.make_true
|
50
70
|
|
51
|
-
Exekutor.say "Starting status server at 0.0.0.0:#{port}… (Timeout: #{@heartbeat_timeout}
|
71
|
+
Exekutor.say "Starting status server at 0.0.0.0:#{port}… (Timeout: #{@heartbeat_timeout.inspect})"
|
52
72
|
@handler.run(App.new(worker, @heartbeat_timeout), Port: port, Host: "0.0.0.0", Silent: true,
|
53
73
|
Logger: ::Logger.new(File.open(File::NULL, "w")), AccessLog: []) do |server|
|
54
74
|
@server.set server
|
55
75
|
end
|
56
|
-
rescue StandardError =>
|
57
|
-
Exekutor.on_fatal_error
|
76
|
+
rescue StandardError => e
|
77
|
+
Exekutor.on_fatal_error e, "[Status server] Runtime error!"
|
58
78
|
if running?
|
59
79
|
logger.info "Restarting in 10 seconds…"
|
60
|
-
Concurrent::ScheduledTask.execute(10.0, executor: @pool
|
80
|
+
Concurrent::ScheduledTask.execute(10.0, executor: @pool) { start_thread }
|
61
81
|
end
|
62
82
|
ensure
|
63
83
|
@thread_running.make_false
|
@@ -65,68 +85,81 @@ module Exekutor
|
|
65
85
|
|
66
86
|
# The Rack-app for the health-check server
|
67
87
|
class App
|
68
|
-
|
69
88
|
def initialize(worker, heartbeat_timeout)
|
70
89
|
@worker = worker
|
71
90
|
@heartbeat_timeout = heartbeat_timeout
|
72
91
|
end
|
73
92
|
|
74
|
-
def flatlined?
|
75
|
-
last_heartbeat = @worker.last_heartbeat
|
76
|
-
last_heartbeat.nil? || last_heartbeat < @heartbeat_timeout.minutes.ago
|
77
|
-
end
|
78
|
-
|
79
93
|
def call(env)
|
80
94
|
case Rack::Request.new(env).path
|
81
95
|
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
|
-
]]
|
96
|
+
render_root
|
90
97
|
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
|
-
]]
|
98
|
+
render_ready
|
101
99
|
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
|
-
]]
|
100
|
+
render_live
|
112
101
|
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
|
102
|
+
render_threads
|
119
103
|
else
|
120
104
|
[404, {}, ["Not found"]]
|
121
105
|
end
|
122
106
|
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def flatlined?(last_heartbeat = @worker.last_heartbeat)
|
111
|
+
last_heartbeat.nil? || last_heartbeat < @heartbeat_timeout.ago
|
112
|
+
end
|
113
|
+
|
114
|
+
def render_threads
|
115
|
+
if @worker.running?
|
116
|
+
info = @worker.thread_stats
|
117
|
+
[(info ? 200 : 503), { "Content-Type" => "application/json" }, [info.to_json]]
|
118
|
+
else
|
119
|
+
[503, { "Content-Type" => "application/json" }, [{ error: "Worker not running" }.to_json]]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def render_live
|
124
|
+
running = @worker.running?
|
125
|
+
last_heartbeat = (@worker.last_heartbeat if running)
|
126
|
+
running = false if flatlined?(last_heartbeat)
|
127
|
+
[(running ? 200 : 503), { "Content-Type" => "text/plain" }, [
|
128
|
+
"#{running ? "[OK]" : "[Service unavailable]"} ID: #{@worker.id}; State: #{@worker.state}; " \
|
129
|
+
"Heartbeat: #{last_heartbeat&.iso8601 || "null"}"
|
130
|
+
]]
|
131
|
+
end
|
132
|
+
|
133
|
+
def render_ready
|
134
|
+
running = @worker.running?
|
135
|
+
if running
|
136
|
+
Exekutor::Job.connection_pool.with_connection do |connection|
|
137
|
+
running = connection.active?
|
138
|
+
end
|
139
|
+
end
|
140
|
+
running = false if running && flatlined?
|
141
|
+
[(running ? 200 : 503), { "Content-Type" => "text/plain" }, [
|
142
|
+
"#{running ? "[OK]" : "[Service unavailable]"} ID: #{@worker.id}; State: #{@worker.state}"
|
143
|
+
]]
|
144
|
+
end
|
145
|
+
|
146
|
+
def render_root
|
147
|
+
[200, {}, [
|
148
|
+
<<~RESPONSE
|
149
|
+
[Exekutor]
|
150
|
+
- Use GET /ready to check whether the worker is running and connected to the DB
|
151
|
+
- Use GET /live to check whether the worker is running and is not hanging
|
152
|
+
- Use GET /threads to check thread usage
|
153
|
+
RESPONSE
|
154
|
+
]]
|
155
|
+
end
|
123
156
|
end
|
124
157
|
|
125
158
|
private
|
126
159
|
|
127
160
|
def start_thread
|
128
|
-
@pool.post(@worker, @port
|
161
|
+
@pool.post(@worker, @port) { |*args| run(*args) } if state == :started
|
129
162
|
end
|
130
163
|
end
|
131
164
|
end
|
132
|
-
end
|
165
|
+
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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
raise Exekutor::Plugins::LoadError, "Appsignal not found, is the gem loaded?" unless defined? Appsignal
|
2
4
|
|
3
5
|
module Exekutor
|
@@ -21,14 +23,14 @@ module Exekutor
|
|
21
23
|
|
22
24
|
::Appsignal.monitor_transaction(
|
23
25
|
"perform_job.exekutor",
|
24
|
-
class: payload[
|
26
|
+
class: payload["job_class"],
|
25
27
|
method: "perform",
|
26
28
|
params: params,
|
27
29
|
metadata: {
|
28
|
-
id: payload[
|
29
|
-
queue: payload[
|
30
|
-
priority: payload.fetch(
|
31
|
-
attempts: payload.fetch(
|
30
|
+
id: payload["job_id"],
|
31
|
+
queue: payload["queue_name"],
|
32
|
+
priority: payload.fetch("priority", Exekutor.config.default_queue_priority),
|
33
|
+
attempts: payload.fetch("attempts", 0)
|
32
34
|
},
|
33
35
|
queue_start: job[:scheduled_at]
|
34
36
|
) do
|
data/lib/exekutor/plugins.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The Exekutor namespace
|
1
4
|
module Exekutor
|
2
5
|
module Plugins
|
6
|
+
# Raised when a plugin cannot be loaded
|
3
7
|
class LoadError < ::LoadError; end
|
4
8
|
end
|
5
9
|
|
6
10
|
def self.load_plugin(name)
|
7
|
-
|
8
|
-
require_relative "plugins/#{name}"
|
9
|
-
else
|
11
|
+
unless File.exist? File.join(__dir__, "plugins/#{name}.rb")
|
10
12
|
raise Plugins::LoadError, "The #{name} plugin does not exist. Have you spelled it correctly?"
|
11
13
|
end
|
14
|
+
|
15
|
+
require_relative "plugins/#{name}"
|
12
16
|
end
|
13
|
-
end
|
17
|
+
end
|
data/lib/exekutor/queue.rb
CHANGED
@@ -6,7 +6,7 @@ module Exekutor
|
|
6
6
|
# Used when logging the SQL queries
|
7
7
|
# @private
|
8
8
|
ACTION_NAME = "Exekutor::Enqueue"
|
9
|
-
private_constant
|
9
|
+
private_constant :ACTION_NAME
|
10
10
|
|
11
11
|
# Valid range for job priority
|
12
12
|
# @private
|
@@ -17,18 +17,18 @@ module Exekutor
|
|
17
17
|
MAX_NAME_LENGTH = 63
|
18
18
|
|
19
19
|
# Adds a job to the queue, scheduled to perform immediately
|
20
|
-
# @param jobs [Array<ActiveJob::Base>] the jobs to enqueue
|
21
|
-
# @return [
|
22
|
-
def push(
|
23
|
-
create_records(jobs)
|
20
|
+
# @param jobs [ActiveJob::Base,Array<ActiveJob::Base>] the jobs to enqueue
|
21
|
+
# @return [Integer] the number of enqueued jobs
|
22
|
+
def push(jobs)
|
23
|
+
create_records Array.wrap(jobs)
|
24
24
|
end
|
25
25
|
|
26
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
|
27
|
+
# @param jobs [ActiveJob::Base,Array<ActiveJob::Base>] the jobs to enqueue
|
28
28
|
# @param timestamp [Time,Date,Integer,Float] when the job should be performed
|
29
|
-
# @return [
|
30
|
-
def schedule_at(
|
31
|
-
create_records(jobs, scheduled_at: timestamp)
|
29
|
+
# @return [Integer] the number of enqueued jobs
|
30
|
+
def schedule_at(jobs, timestamp)
|
31
|
+
create_records(Array.wrap(jobs), scheduled_at: timestamp)
|
32
32
|
end
|
33
33
|
|
34
34
|
private
|
@@ -38,45 +38,83 @@ module Exekutor
|
|
38
38
|
# @param scheduled_at [Time,Date,Integer,Float] when the job should be performed
|
39
39
|
# @return [void]
|
40
40
|
def create_records(jobs, scheduled_at: nil)
|
41
|
-
|
42
|
-
|
41
|
+
raise ArgumentError, "jobs must be an array with ActiveJob items (Actual: #{jobs.class})" unless jobs.is_a?(Array)
|
42
|
+
|
43
|
+
unless jobs.all?(ActiveJob::Base)
|
44
|
+
raise ArgumentError, "jobs must be an array with ActiveJob items (Found: #{
|
45
|
+
jobs.map { |job| job.class unless job.is_a? ActiveJob::Base }.compact.join(", ")
|
46
|
+
})"
|
43
47
|
end
|
44
48
|
|
49
|
+
scheduled_at = parse_scheduled_at(scheduled_at)
|
50
|
+
json_serializer = Exekutor.config.load_json_serializer
|
51
|
+
|
52
|
+
inserted_records = nil
|
53
|
+
Internal::Hooks.run :enqueue, jobs do
|
54
|
+
inserted_records = insert_job_records(jobs, scheduled_at, json_serializer)
|
55
|
+
end
|
56
|
+
inserted_records
|
57
|
+
end
|
58
|
+
|
59
|
+
# Converts the given value to an epoch timestamp. Returns the current epoch timestamp if the given value is nil
|
60
|
+
# @param scheduled_at [nil,Numeric,Time,Date] The timestamp to convert to an epoch timestamp
|
61
|
+
# @return [Float,Integer] The epoch equivalent of +scheduled_at+
|
62
|
+
def parse_scheduled_at(scheduled_at)
|
45
63
|
if scheduled_at.nil?
|
46
|
-
|
64
|
+
Time.now.to_i
|
47
65
|
else
|
48
66
|
case scheduled_at
|
49
67
|
when Integer, Float
|
50
68
|
raise ArgumentError, "scheduled_at must be a valid epoch" unless scheduled_at.positive?
|
69
|
+
|
70
|
+
scheduled_at
|
51
71
|
when Time
|
52
|
-
scheduled_at
|
72
|
+
scheduled_at.to_f
|
53
73
|
when Date
|
54
|
-
scheduled_at
|
74
|
+
scheduled_at.at_beginning_of_day.to_f
|
55
75
|
else
|
56
76
|
raise ArgumentError, "scheduled_at must be an epoch, time, or date"
|
57
77
|
end
|
58
78
|
end
|
79
|
+
end
|
59
80
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
81
|
+
# Fires off an INSERT INTO query for the given jobs
|
82
|
+
# @param jobs [Array<ActiveJob::Base>] the jobs to insert
|
83
|
+
# @param scheduled_at [Integer,Float] the scheduled execution time for the jobs as an epoch timestamp
|
84
|
+
# @param json_serializer [#dump] the serializer to use to convert hashes into JSON
|
85
|
+
def insert_job_records(jobs, scheduled_at, json_serializer)
|
86
|
+
if jobs.one?
|
87
|
+
insert_singular_job(jobs.first, json_serializer, scheduled_at)
|
88
|
+
else
|
89
|
+
insert_statements = jobs.map do |job|
|
90
|
+
Exekutor::Job.sanitize_sql_for_assignment(
|
91
|
+
["(?, ?, to_timestamp(?), ?, ?::jsonb, ?::jsonb)", *job_sql_binds(job, scheduled_at, json_serializer)]
|
92
|
+
)
|
93
|
+
end
|
94
|
+
begin
|
95
|
+
pg_result = Exekutor::Job.connection.execute <<~SQL, ACTION_NAME
|
74
96
|
INSERT INTO exekutor_jobs ("queue", "priority", "scheduled_at", "active_job_id", "payload", "options") VALUES #{insert_statements.join(",")}
|
75
97
|
SQL
|
98
|
+
inserted_records = pg_result.cmd_tuples
|
99
|
+
ensure
|
100
|
+
pg_result.clear
|
76
101
|
end
|
102
|
+
inserted_records
|
77
103
|
end
|
78
104
|
end
|
79
105
|
|
106
|
+
# Fires off an INSERT INTO query for the given job using a prepared statement
|
107
|
+
# @param job [ActiveJob::Base] the job to insert
|
108
|
+
# @param scheduled_at [Integer,Float] the scheduled execution time for the jobs as an epoch timestamp
|
109
|
+
# @param json_serializer [#dump] the serializer to use to convert hashes into JSON
|
110
|
+
def insert_singular_job(job, json_serializer, scheduled_at)
|
111
|
+
sql_binds = job_sql_binds(job, scheduled_at, json_serializer)
|
112
|
+
ar_result = Exekutor::Job.connection.exec_query <<~SQL, ACTION_NAME, sql_binds, prepare: true
|
113
|
+
INSERT INTO exekutor_jobs ("queue", "priority", "scheduled_at", "active_job_id", "payload", "options") VALUES ($1, $2, to_timestamp($3), $4, $5, $6) RETURNING id;
|
114
|
+
SQL
|
115
|
+
ar_result.length
|
116
|
+
end
|
117
|
+
|
80
118
|
# Converts the specified job to SQL bind parameters to insert it into the database
|
81
119
|
# @param job [ActiveJob::Base] the job to insert
|
82
120
|
# @param scheduled_at [Float] the epoch timestamp for when the job should be executed
|
@@ -86,7 +124,8 @@ module Exekutor
|
|
86
124
|
if job.queue_name.blank?
|
87
125
|
raise Error, "The queue must be set"
|
88
126
|
elsif job.queue_name && job.queue_name.length > Queue::MAX_NAME_LENGTH
|
89
|
-
raise Error,
|
127
|
+
raise Error,
|
128
|
+
"The queue name \"#{job.queue_name}\" is too long, the limit is #{Queue::MAX_NAME_LENGTH} characters"
|
90
129
|
end
|
91
130
|
|
92
131
|
options = exekutor_options job
|
@@ -106,7 +145,7 @@ module Exekutor
|
|
106
145
|
def exekutor_options(job)
|
107
146
|
return nil unless job.respond_to?(:exekutor_options)
|
108
147
|
|
109
|
-
options = job.exekutor_options
|
148
|
+
options = job.exekutor_options&.stringify_keys
|
110
149
|
if options && options["queue_timeout"]
|
111
150
|
options["start_execution_before"] = Time.now.to_f + options.delete("queue_timeout").to_f
|
112
151
|
end
|
data/lib/exekutor/version.rb
CHANGED