exekutor 0.1.0 → 0.1.2
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 +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