exekutor 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -3
  3. data/exe/exekutor +2 -2
  4. data/lib/active_job/queue_adapters/exekutor_adapter.rb +2 -1
  5. data/lib/exekutor/asynchronous.rb +143 -75
  6. data/lib/exekutor/cleanup.rb +27 -28
  7. data/lib/exekutor/configuration.rb +102 -48
  8. data/lib/exekutor/hook.rb +15 -11
  9. data/lib/exekutor/info/worker.rb +3 -3
  10. data/lib/exekutor/internal/base_record.rb +2 -1
  11. data/lib/exekutor/internal/callbacks.rb +55 -35
  12. data/lib/exekutor/internal/cli/app.rb +33 -23
  13. data/lib/exekutor/internal/cli/application_loader.rb +17 -6
  14. data/lib/exekutor/internal/cli/cleanup.rb +54 -40
  15. data/lib/exekutor/internal/cli/daemon.rb +9 -11
  16. data/lib/exekutor/internal/cli/default_option_value.rb +3 -1
  17. data/lib/exekutor/internal/cli/info.rb +117 -84
  18. data/lib/exekutor/internal/cli/manager.rb +234 -123
  19. data/lib/exekutor/internal/configuration_builder.rb +49 -30
  20. data/lib/exekutor/internal/database_connection.rb +6 -0
  21. data/lib/exekutor/internal/executable.rb +12 -7
  22. data/lib/exekutor/internal/executor.rb +50 -21
  23. data/lib/exekutor/internal/hooks.rb +11 -8
  24. data/lib/exekutor/internal/listener.rb +85 -43
  25. data/lib/exekutor/internal/logger.rb +29 -10
  26. data/lib/exekutor/internal/provider.rb +96 -77
  27. data/lib/exekutor/internal/reserver.rb +66 -19
  28. data/lib/exekutor/internal/status_server.rb +87 -54
  29. data/lib/exekutor/job.rb +1 -1
  30. data/lib/exekutor/job_error.rb +1 -1
  31. data/lib/exekutor/job_options.rb +22 -13
  32. data/lib/exekutor/plugins/appsignal.rb +7 -5
  33. data/lib/exekutor/plugins.rb +8 -4
  34. data/lib/exekutor/queue.rb +69 -30
  35. data/lib/exekutor/version.rb +1 -1
  36. data/lib/exekutor/worker.rb +89 -48
  37. data/lib/exekutor.rb +2 -2
  38. data/lib/generators/exekutor/configuration_generator.rb +11 -6
  39. data/lib/generators/exekutor/install_generator.rb +24 -15
  40. data/lib/generators/exekutor/templates/install/functions/exekutor_broadcast_job_enqueued.sql +10 -0
  41. data/lib/generators/exekutor/templates/install/functions/exekutor_requeue_orphaned_jobs.sql +11 -0
  42. data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +23 -22
  43. data/lib/generators/exekutor/templates/install/triggers/exekutor_broadcast_job_enqueued.sql +7 -0
  44. data/lib/generators/exekutor/templates/install/triggers/exekutor_requeue_orphaned_jobs.sql +5 -0
  45. data.tar.gz.sig +0 -0
  46. metadata +67 -23
  47. metadata.gz.sig +0 -0
  48. data/lib/generators/exekutor/templates/install/functions/job_notifier.sql +0 -7
  49. data/lib/generators/exekutor/templates/install/functions/requeue_orphaned_jobs.sql +0 -7
  50. data/lib/generators/exekutor/templates/install/triggers/notify_workers.sql +0 -6
  51. 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
- set_state :stopped
51
+ self.state = :stopped
34
52
  return unless @thread_running.value
35
53
 
36
54
  server = @server.value
37
- if server&.respond_to? :shutdown
38
- server.shutdown
39
- elsif server&.respond_to? :stop
40
- server.stop
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.say! "Cannot shutdown status server, #{server.class.name} does not respond to shutdown or stop"
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} minutes)"
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 => err
57
- Exekutor.on_fatal_error err, "[HealthServer] Runtime 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, &method(:start_thread))
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
- [200, {}, [
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
- running = @worker.running?
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
- running = @worker.running?
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
- if @worker.running?
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, &method(:run)) if state == :started
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
@@ -28,4 +28,4 @@ module Exekutor
28
28
  update! status: "d"
29
29
  end
30
30
  end
31
- end
31
+ end
@@ -8,4 +8,4 @@ module Exekutor
8
8
  self.implicit_order_column = :created_at
9
9
  belongs_to :job
10
10
  end
11
- end
11
+ end
@@ -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 "VALID_EXEKUTOR_OPTIONS"
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, Object>] the options to validate
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 unless options.present?
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
- "#{invalid_options.map(&:inspect).join(", ")}. " \
81
- "Valid options are: #{VALID_EXEKUTOR_OPTIONS.map(&:inspect).join(", ")}"
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['job_class'],
26
+ class: payload["job_class"],
25
27
  method: "perform",
26
28
  params: params,
27
29
  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)
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
@@ -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
- if File.exist? File.join(__dir__, "plugins/#{name}.rb")
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
@@ -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 "ACTION_NAME"
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 [void]
22
- def push(*jobs)
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 [void]
30
- def schedule_at(*jobs, timestamp)
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
- unless jobs.is_a?(Array) && jobs.all? { |job| job.is_a?(ActiveJob::Base) }
42
- raise ArgumentError, "jobs must be an array with ActiveJob items"
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
- scheduled_at = Time.now.to_i
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 = scheduled_at.to_f
72
+ scheduled_at.to_f
53
73
  when Date
54
- scheduled_at = scheduled_at.at_beginning_of_day.to_f
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
- 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
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, "The queue name \"#{job.queue_name}\" is too long, the limit is #{Queue::MAX_NAME_LENGTH} characters"
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.stringify_keys
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Exekutor
4
4
  # The current version of Exekutor
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.2"
6
6
  end