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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5b178052eb277168916a76f663ba53882204481f740a7c30bc309d243de79cf
|
4
|
+
data.tar.gz: bfb648002d86bf278b41b5f7ca6fde0f1d3cb39954c445eec0e268bf72120efe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e2d7a7787f83dc362aeca814d511d0584e5e3e2af24209f1bfc00bd1efbae47d9226bc91e5615a0c7b38b5a515ad64146ec43d74e6cb3b757f3144fcc100d05e
|
7
|
+
data.tar.gz: 651ee14b261990adfe498e017afd6ea0e3e382e12f7bf4a352d01dd4d203938f62f2223159e7930bd517ee5fd68f191b2d0ad9189c13670158064e8381278b5b
|
checksums.yaml.gz.sig
CHANGED
@@ -1,3 +1,2 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
��;g\Rl��g���K_�1W�-J����YՃXz��js;���|����_���4{ B�WV�sڟ6 �7vʇ�b4�
|
1
|
+
��<����}g�w�2���ȝ�T���L�7!��wc��]<o���6���f{o���&�9���H@��<Zy7���-"'}5Hbu^�҄���W����I̒�k$턍��)2-�*�߷���ci
|
2
|
+
��#�s)�������b`gư�H� (H�����U�u��&v9(��H��*t()��`{3��<g�mf(��?M�,$������r`3�'��WkةyH��;'���^��0�d�)0>��(��!�9����@KI�<������������a6��������V�\!�k���|��q�V��X%�&�u��IK�˛�H+�>���l�nY�3D6�-���"1�Y�����a�7�ے�x��X
|
data/exe/exekutor
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
|
-
require "exekutor/internal/cli/app"
|
4
3
|
|
5
|
-
|
4
|
+
require "exekutor/version"
|
5
|
+
require "exekutor/internal/cli/app"
|
6
6
|
|
7
7
|
exit Exekutor::Internal::CLI::App.run(ARGV)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Exekutor
|
2
4
|
# Mixin to let methods be executed asynchronously by active job
|
3
5
|
#
|
@@ -21,10 +23,10 @@ module Exekutor
|
|
21
23
|
included do
|
22
24
|
mattr_reader :__async_class_methods, instance_accessor: false, default: {}
|
23
25
|
mattr_reader :__async_instance_methods, instance_accessor: false, default: {}
|
24
|
-
private_class_method :perform_asynchronously
|
26
|
+
private_class_method :perform_asynchronously, :async_delegate_and_definitions, :redefine_method
|
25
27
|
end
|
26
28
|
|
27
|
-
class_methods do
|
29
|
+
class_methods do # rubocop:disable Metrics/BlockLength
|
28
30
|
# Changes a method to be executed asynchronously.
|
29
31
|
# Be aware that you can no longer use the return value for
|
30
32
|
# asynchronous methods, because the actual method will be performed by a worker at a later time. The new
|
@@ -36,46 +38,62 @@ module Exekutor
|
|
36
38
|
# @param class_method [Boolean] whether the method is a class method.
|
37
39
|
# @raise [Error] if the method could not be replaced with the asynchronous version
|
38
40
|
def perform_asynchronously(method_name, alias_to: "__immediately_#{method_name}", class_method: false)
|
39
|
-
|
40
|
-
|
41
|
+
unless method_name.is_a? Symbol
|
42
|
+
raise ArgumentError, "method_name must be a Symbol (actual: #{method_name.class.name})"
|
43
|
+
end
|
44
|
+
raise ArgumentError, "alias_to must be present" if alias_to.blank?
|
45
|
+
|
46
|
+
delegate, definitions = async_delegate_and_definitions(method_name, class_method: class_method)
|
47
|
+
raise Error, "##{method_name} was already marked as asynchronous" if definitions.include? method_name
|
48
|
+
|
49
|
+
redefine_method_as_async(delegate, method_name, alias_to)
|
50
|
+
definitions[method_name] = alias_to
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Gets the object to define aliased method on, and the definitions that are already defined
|
55
|
+
# @return [Array(Any, Hash<Symbol=>Symbol>)] The delegate and the existing definitions
|
56
|
+
def async_delegate_and_definitions(method_name, class_method:)
|
41
57
|
if class_method
|
42
58
|
raise ArgumentError, "##{method_name} does not exist" unless respond_to? method_name, true
|
43
59
|
|
44
|
-
|
45
|
-
definitions = __async_class_methods
|
60
|
+
[singleton_class, __async_class_methods]
|
46
61
|
else
|
47
62
|
unless method_defined?(method_name, true) || private_method_defined?(method_name, true)
|
48
63
|
raise ArgumentError, "##{method_name} does not exist"
|
49
64
|
end
|
50
65
|
|
51
|
-
|
52
|
-
definitions = __async_instance_methods
|
53
|
-
end
|
54
|
-
if definitions.include? method_name
|
55
|
-
raise Error, "##{method_name} was already marked as asynchronous"
|
66
|
+
[self, __async_instance_methods]
|
56
67
|
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Aliases the indicated method and redefines the method to call the original method asynchronously.
|
71
|
+
# @param delegate [Any] the delegate to redefine the method on
|
72
|
+
# @param method_name [Symbol] the name of the method to redefine
|
73
|
+
# @param alias_to [String] the name to alias the original method to
|
74
|
+
# @return [Void]
|
75
|
+
def redefine_method_as_async(delegate, method_name, alias_to)
|
76
|
+
visibility = if delegate.private_method_defined?(method_name)
|
77
|
+
:private
|
78
|
+
elsif delegate.protected_method_defined?(method_name)
|
79
|
+
:protected
|
80
|
+
else
|
81
|
+
:public
|
82
|
+
end
|
57
83
|
|
58
84
|
delegate.alias_method alias_to, method_name
|
59
85
|
delegate.define_method method_name do |*args, **kwargs|
|
60
86
|
error = Asynchronous.validate_args(self, alias_to, *args, **kwargs)
|
61
87
|
raise error if error
|
62
88
|
raise ArgumentError, "Cannot asynchronously execute with a block argument" if block_given?
|
89
|
+
|
63
90
|
AsyncMethodJob.perform_later self, method_name, [args, kwargs.presence]
|
64
91
|
end
|
65
92
|
|
66
|
-
|
67
|
-
if delegate.public_method_defined?(alias_to)
|
68
|
-
delegate.send :public, method_name
|
69
|
-
elsif delegate.protected_method_defined?(alias_to)
|
70
|
-
delegate.send :protected, method_name
|
71
|
-
else
|
72
|
-
delegate.send :private, method_name
|
73
|
-
end
|
93
|
+
delegate.send(visibility, method_name)
|
74
94
|
end
|
75
95
|
end
|
76
96
|
|
77
|
-
private
|
78
|
-
|
79
97
|
# Validates whether the given arguments match the expected parameters for +method+
|
80
98
|
# @param delegate [Object] the object the +method+ will be called on
|
81
99
|
# @param method [Symbol] the method that will be called on +delegate+
|
@@ -83,60 +101,16 @@ module Exekutor
|
|
83
101
|
# @param kwargs [Hash] the keyword arguments that will be given to the method
|
84
102
|
# @return [ArgumentError,nil] nil if the keywords are valid; an ArgumentError otherwise
|
85
103
|
def self.validate_args(delegate, method, *args, **kwargs)
|
86
|
-
|
87
|
-
|
88
|
-
max_arg_length = 0
|
89
|
-
accepts_keywords = false
|
90
|
-
missing_keywords = []
|
91
|
-
unknown_keywords = kwargs.keys
|
92
|
-
obj_method.parameters.each do |type, name|
|
93
|
-
if type == :req
|
94
|
-
min_arg_length += 1
|
95
|
-
max_arg_length += 1 if max_arg_length
|
96
|
-
elsif type == :opt
|
97
|
-
max_arg_length += 1 if max_arg_length
|
98
|
-
elsif type == :rest
|
99
|
-
max_arg_length = nil
|
100
|
-
elsif type == :keyreq
|
101
|
-
accepts_keywords = true
|
102
|
-
missing_keywords << name if kwargs.exclude?(name)
|
103
|
-
unknown_keywords.delete(name)
|
104
|
-
elsif type == :key
|
105
|
-
accepts_keywords = true
|
106
|
-
unknown_keywords.delete(name)
|
107
|
-
elsif type == :keyrest
|
108
|
-
accepts_keywords = true
|
109
|
-
unknown_keywords = []
|
110
|
-
end
|
111
|
-
end
|
112
|
-
if missing_keywords.present?
|
113
|
-
return ArgumentError.new "missing keyword#{"s" if missing_keywords.many?}: #{missing_keywords.map(&:inspect).join(", ")}"
|
114
|
-
end
|
115
|
-
if accepts_keywords
|
116
|
-
if unknown_keywords.present?
|
117
|
-
return ArgumentError.new "unknown keyword#{"s" if unknown_keywords.many?}: #{unknown_keywords.map(&:inspect).join(", ")}"
|
118
|
-
end
|
119
|
-
elsif kwargs.present?
|
120
|
-
args += [kwargs]
|
121
|
-
end
|
104
|
+
error = ArgumentValidator.new(delegate, method).validate(args, kwargs)
|
105
|
+
return nil unless error
|
122
106
|
|
123
|
-
|
124
|
-
if min_arg_length > args_len || (max_arg_length.present? && max_arg_length < args_len)
|
125
|
-
expected = min_arg_length.to_s
|
126
|
-
if max_arg_length.nil?
|
127
|
-
expected += "+"
|
128
|
-
elsif max_arg_length > min_arg_length
|
129
|
-
expected += "..#{max_arg_length}"
|
130
|
-
end
|
131
|
-
return ArgumentError.new "wrong number of arguments (given #{args_len}, expected #{expected})"
|
132
|
-
end
|
107
|
+
ArgumentError.new(error)
|
133
108
|
end
|
134
109
|
|
135
110
|
# The internal job used for {Exekutor::Asynchronous}. Only works for methods that are marked as asynchronous to
|
136
111
|
# prevent remote code execution. Include the {Exekutor::Asynchronous} and call
|
137
|
-
#
|
138
|
-
class AsyncMethodJob < ActiveJob::Base
|
139
|
-
|
112
|
+
# +perform_asynchronously+ to mark a method as asynchronous.
|
113
|
+
class AsyncMethodJob < ActiveJob::Base # rubocop:disable Rails/ApplicationJob
|
140
114
|
# Calls the original, synchronous method
|
141
115
|
# @!visibility private
|
142
116
|
def perform(object, method, args)
|
@@ -172,16 +146,110 @@ module Exekutor
|
|
172
146
|
class_name = object.class.name
|
173
147
|
definitions = object.class.__async_instance_methods
|
174
148
|
end
|
175
|
-
unless object.respond_to? method, true
|
176
|
-
|
149
|
+
raise Error, "#{class_name} does not respond to #{method}" unless object.respond_to? method, true
|
150
|
+
raise Error, "#{class_name}##{method} is not marked as asynchronous" unless definitions.include? method.to_sym
|
151
|
+
|
152
|
+
definitions[method.to_sym]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Validates whether a set of arguments is valid for a particular method
|
157
|
+
class ArgumentValidator
|
158
|
+
def initialize(delegate, method)
|
159
|
+
@required_keywords = []
|
160
|
+
@optional_keywords = []
|
161
|
+
@accepts_keyrest = false
|
162
|
+
parse_method_params delegate.method(method)
|
163
|
+
end
|
164
|
+
|
165
|
+
def parse_method_params(method)
|
166
|
+
arguments = ArgumentCounter.new
|
167
|
+
method.parameters.each do |type, name|
|
168
|
+
case type
|
169
|
+
when :req, :opt
|
170
|
+
arguments.increment(type)
|
171
|
+
when :rest
|
172
|
+
arguments.clear_max
|
173
|
+
when :keyreq
|
174
|
+
@required_keywords << name
|
175
|
+
when :key
|
176
|
+
@optional_keywords << name
|
177
|
+
when :keyrest
|
178
|
+
@accepts_keyrest = true
|
179
|
+
else
|
180
|
+
Exekutor.say "Unsupported parameter type: #{type.inspect}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
@arg_length = arguments.to_range
|
184
|
+
end
|
185
|
+
|
186
|
+
def accepts_keywords?
|
187
|
+
@accepts_keyrest || @required_keywords.present? || @optional_keywords.present?
|
188
|
+
end
|
189
|
+
|
190
|
+
def fixed_keywords?
|
191
|
+
!@accepts_keyrest && (@required_keywords.present? || @optional_keywords.present?)
|
192
|
+
end
|
193
|
+
|
194
|
+
def validate(args, kwargs)
|
195
|
+
args += [kwargs] unless kwargs.empty? || accepts_keywords?
|
196
|
+
|
197
|
+
return argument_length_error(args.length) unless @arg_length.cover? args.length
|
198
|
+
|
199
|
+
missing_keywords = @required_keywords - kwargs.keys
|
200
|
+
return missing_keywords_error(missing_keywords) if missing_keywords.present?
|
201
|
+
|
202
|
+
unknown_keywords = (kwargs.keys - @required_keywords - @optional_keywords if fixed_keywords?)
|
203
|
+
return unknown_keywords_error(unknown_keywords) if unknown_keywords.present?
|
204
|
+
|
205
|
+
nil
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def unknown_keywords_error(unknown_keywords)
|
211
|
+
"unknown keyword#{"s" if unknown_keywords.many?}: #{
|
212
|
+
unknown_keywords.map(&:inspect).join(", ")}"
|
213
|
+
end
|
214
|
+
|
215
|
+
def missing_keywords_error(missing_keywords)
|
216
|
+
"missing keyword#{"s" if missing_keywords.many?}: #{
|
217
|
+
missing_keywords.map(&:inspect).join(", ")}"
|
218
|
+
end
|
219
|
+
|
220
|
+
def argument_length_error(given_length)
|
221
|
+
expected = @arg_length.begin.to_s
|
222
|
+
if @arg_length.end.nil?
|
223
|
+
expected += "+"
|
224
|
+
elsif @arg_length.end > @arg_length.begin
|
225
|
+
expected += "..#{@arg_length.end}"
|
177
226
|
end
|
178
|
-
|
179
|
-
|
227
|
+
"wrong number of arguments (given #{given_length}, expected #{expected})"
|
228
|
+
end
|
229
|
+
|
230
|
+
# Keeps track of the minimum and maximum number of allowed arguments
|
231
|
+
class ArgumentCounter
|
232
|
+
def initialize
|
233
|
+
@min = @max = 0
|
234
|
+
end
|
235
|
+
|
236
|
+
def increment(type)
|
237
|
+
@min += 1 if type == :req
|
238
|
+
@max += 1 if @max
|
239
|
+
end
|
240
|
+
|
241
|
+
def clear_max
|
242
|
+
@max = nil
|
243
|
+
end
|
244
|
+
|
245
|
+
def to_range
|
246
|
+
@min..@max
|
180
247
|
end
|
181
|
-
definitions[method.to_sym]
|
182
248
|
end
|
183
249
|
end
|
184
250
|
|
251
|
+
private_constant :ArgumentValidator
|
252
|
+
|
185
253
|
# Raised when an error occurs while configuring or executing asynchronous methods
|
186
254
|
class Error < Exekutor::DiscardJob; end
|
187
255
|
end
|
data/lib/exekutor/cleanup.rb
CHANGED
@@ -1,23 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Exekutor
|
2
4
|
# Helper class to clean up finished jobs and stale workers.
|
3
5
|
class Cleanup
|
4
|
-
|
5
6
|
# Purges all workers where the last heartbeat is over the +timeout+ ago.
|
6
7
|
# @param timeout [ActiveSupport::Duration,Numeric,Time] the timeout. Default: 4 hours
|
7
8
|
# @return [Array<Exekutor::Info::Worker>] the purged workers
|
8
9
|
def cleanup_workers(timeout: 4.hours)
|
9
|
-
destroy_before =
|
10
|
-
|
11
|
-
|
12
|
-
when Numeric
|
13
|
-
timeout.hours.ago
|
14
|
-
when Date, Time
|
15
|
-
timeout
|
16
|
-
else
|
17
|
-
raise ArgumentError, "Unsupported value for timeout: #{timeout.class}"
|
18
|
-
end
|
19
|
-
# TODO PG-NOTIFY each worker with an EXIT command
|
20
|
-
Exekutor::Info::Worker.where(%{"last_heartbeat_at"<?}, destroy_before).destroy_all
|
10
|
+
destroy_before = parse_timeout_arg :timeout, timeout
|
11
|
+
# TODO: PG-NOTIFY each worker with an EXIT command
|
12
|
+
Exekutor::Info::Worker.where(%("last_heartbeat_at"<?), destroy_before).destroy_all
|
21
13
|
end
|
22
14
|
|
23
15
|
# Purges all jobs where scheduled at is before +before+. Only purges jobs with the given status, if no status is
|
@@ -26,24 +18,13 @@ module Exekutor
|
|
26
18
|
# @param status [Array<String,Symbol>,String,Symbol] the statuses to purge. Default: All except +:pending+
|
27
19
|
# @return [Integer] the number of purged jobs
|
28
20
|
def cleanup_jobs(before: 48.hours.ago, status: nil)
|
29
|
-
destroy_before =
|
30
|
-
|
31
|
-
before.ago
|
32
|
-
when Numeric
|
33
|
-
before.hours.ago
|
34
|
-
when Date, Time
|
35
|
-
before
|
36
|
-
else
|
37
|
-
raise ArgumentError, "Unsupported value for before: #{before.class}"
|
38
|
-
end
|
39
|
-
unless [Array, String, Symbol, NilClass].any?(&status.method(:is_a?))
|
21
|
+
destroy_before = parse_timeout_arg :before, before
|
22
|
+
unless [Array, String, Symbol, NilClass].any? { |c| status.is_a? c }
|
40
23
|
raise ArgumentError, "Unsupported value for status: #{status.class}"
|
41
24
|
end
|
42
25
|
|
43
26
|
jobs = Exekutor::Job.all
|
44
|
-
unless before.nil?
|
45
|
-
jobs.where!(%{"scheduled_at"<?}, destroy_before)
|
46
|
-
end
|
27
|
+
jobs.where!(%("scheduled_at"<?), destroy_before) unless before.nil?
|
47
28
|
if status
|
48
29
|
jobs.where! status: status
|
49
30
|
else
|
@@ -52,5 +33,23 @@ module Exekutor
|
|
52
33
|
jobs.delete_all
|
53
34
|
end
|
54
35
|
|
36
|
+
private
|
37
|
+
|
38
|
+
# Converts timout argument to a Time
|
39
|
+
# @param name [Symbol,String] the name of the argument
|
40
|
+
# @param value [ActiveSupport::Duration,Numeric,Date,Time] the argument to parse
|
41
|
+
# @return [Date,Time] The point in time
|
42
|
+
def parse_timeout_arg(name, value)
|
43
|
+
case value
|
44
|
+
when ActiveSupport::Duration
|
45
|
+
value.ago
|
46
|
+
when Numeric
|
47
|
+
value.hours.ago
|
48
|
+
when Date, Time
|
49
|
+
value
|
50
|
+
else
|
51
|
+
raise ArgumentError, "Unsupported value for #{name}: #{value.class}"
|
52
|
+
end
|
53
|
+
end
|
55
54
|
end
|
56
|
-
end
|
55
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative "internal/configuration_builder"
|
4
4
|
|
5
|
+
# The Exekutor namespace
|
5
6
|
module Exekutor
|
6
7
|
# Configuration for the Exekutor library
|
7
8
|
class Configuration
|
@@ -9,7 +10,7 @@ module Exekutor
|
|
9
10
|
|
10
11
|
# @private
|
11
12
|
DEFAULT_BASE_RECORD_CLASS = "ActiveRecord::Base"
|
12
|
-
private_constant
|
13
|
+
private_constant :DEFAULT_BASE_RECORD_CLASS
|
13
14
|
|
14
15
|
# @!macro
|
15
16
|
# @!method $1
|
@@ -73,8 +74,7 @@ module Exekutor
|
|
73
74
|
# @param value [String,Symbol,Proc,Object] the serializer
|
74
75
|
# @return [self]
|
75
76
|
define_option :json_serializer, default: "::JSON", required: true do |value|
|
76
|
-
unless value.is_a?(String) || value.is_a?(Symbol) || value.respond_to?(:call) ||
|
77
|
-
(value.respond_to?(:dump) && value.respond_to?(:load))
|
77
|
+
unless value.is_a?(String) || value.is_a?(Symbol) || value.respond_to?(:call) || SerializerValidator.valid?(value)
|
78
78
|
raise Error, "#json_serializer must either be a String, a Proc, or respond to #dump and #load"
|
79
79
|
end
|
80
80
|
end
|
@@ -83,24 +83,13 @@ module Exekutor
|
|
83
83
|
# @raise [Error] when the class cannot be found, or does not respond to +#dump+ and +#load+
|
84
84
|
# @return [Object]
|
85
85
|
def load_json_serializer
|
86
|
-
raw_value =
|
86
|
+
raw_value = json_serializer
|
87
87
|
if defined?(@json_serializer_instance) && @json_serializer_instance[0] == raw_value
|
88
88
|
return @json_serializer_instance[1]
|
89
89
|
end
|
90
90
|
|
91
91
|
serializer = const_get :json_serializer
|
92
|
-
unless
|
93
|
-
serializer = serializer.call if serializer.respond_to?(:call)
|
94
|
-
unless serializer.respond_to?(:dump) && serializer.respond_to?(:load)
|
95
|
-
serializer = serializer.new if serializer.respond_to?(:new)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
unless serializer.respond_to?(:dump) && serializer.respond_to?(:load)
|
99
|
-
raise Error, <<~MSG.squish
|
100
|
-
The configured serializer (#{serializer.class}) does not respond to #dump and #load
|
101
|
-
MSG
|
102
|
-
end
|
103
|
-
|
92
|
+
serializer = SerializerValidator.convert! serializer unless SerializerValidator.valid? serializer
|
104
93
|
@json_serializer_instance = [raw_value, serializer]
|
105
94
|
serializer
|
106
95
|
end
|
@@ -191,18 +180,19 @@ module Exekutor
|
|
191
180
|
|
192
181
|
# @!macro
|
193
182
|
# @!method $1
|
194
|
-
# The polling interval
|
183
|
+
# The polling interval. When set, the worker will poll the database with this interval to check for
|
195
184
|
# any pending jobs that a listener might have missed (if enabled).
|
196
185
|
# === Default value:
|
197
|
-
# 60
|
198
|
-
# @return [
|
186
|
+
# 60 seconds
|
187
|
+
# @return [ActiveSupport::Duration]
|
199
188
|
# @!method $1=(value)
|
200
|
-
# Sets the polling interval
|
201
|
-
# should be reasonably low so jobs don't have to wait in the queue too long; if
|
202
|
-
# be reasonably high.
|
203
|
-
# @param value [
|
189
|
+
# Sets the polling interval. Set to +nil+ to disable polling. If the listener is disabled, this value
|
190
|
+
# should be reasonably low so jobs don't have to wait in the queue too long; if the listener is enabled, this
|
191
|
+
# value can be reasonably high.
|
192
|
+
# @param value [ActiveSupport::Duration] the interval
|
204
193
|
# @return [self]
|
205
|
-
define_option :polling_interval, default:
|
194
|
+
define_option :polling_interval, default: 1.minute, type: [ActiveSupport::Duration, nil],
|
195
|
+
range: (1.second)...(1.day)
|
206
196
|
|
207
197
|
# @!macro
|
208
198
|
# @!method $1
|
@@ -250,21 +240,22 @@ module Exekutor
|
|
250
240
|
|
251
241
|
# @!macro
|
252
242
|
# @!method $1
|
253
|
-
# The maximum
|
243
|
+
# The maximum duration a thread may be idle before being stopped.
|
254
244
|
# === Default value:
|
255
|
-
# 60
|
256
|
-
# @return [
|
245
|
+
# 60 seconds
|
246
|
+
# @return [ActiveSupport::Duration]
|
257
247
|
# @!method $1=(value)
|
258
|
-
# Sets the maximum
|
259
|
-
# @param value [
|
248
|
+
# Sets the maximum duration a thread may be idle before being stopped
|
249
|
+
# @param value [ActiveSupport::Duration] the number of threads
|
260
250
|
# @return [self]
|
261
|
-
define_option :max_execution_thread_idletime, default:
|
251
|
+
define_option :max_execution_thread_idletime, default: 1.minute, type: ActiveSupport::Duration,
|
252
|
+
range: (1.second)..(1.day)
|
262
253
|
|
263
254
|
# @!macro
|
264
255
|
# @!method $1?
|
265
256
|
# The rack handler for the status server
|
266
257
|
# === Default value:
|
267
|
-
# webrick
|
258
|
+
# +"webrick"+
|
268
259
|
# @return [String]
|
269
260
|
# @!method $1=(value)
|
270
261
|
# Sets the rack handler for the status server. The handler should respond to +#shutdown+ or +#stop+.
|
@@ -274,18 +265,46 @@ module Exekutor
|
|
274
265
|
|
275
266
|
# @!macro
|
276
267
|
# @!method $1?
|
277
|
-
# The
|
268
|
+
# The port number for the status server
|
269
|
+
# === Default value:
|
270
|
+
# +nil+ (ie. the status server is disabled)
|
271
|
+
# @return [Integer]
|
272
|
+
# @!method $1=(value)
|
273
|
+
# Sets the port number for the status server.
|
274
|
+
# @param value [Integer] the port number
|
275
|
+
# @return [self]
|
276
|
+
define_option :status_server_port, default: nil, type: Integer
|
277
|
+
|
278
|
+
# @!macro
|
279
|
+
# @!method $1?
|
280
|
+
# The heartbeat timeout for the `/live` endpoint of the status server. If the heartbeat of a worker
|
278
281
|
# is older than this timeout, the status server will respond with a 503 status indicating the service is
|
279
282
|
# down.
|
280
283
|
# === Default value:
|
281
|
-
# 30
|
282
|
-
# @return [
|
284
|
+
# 30 minutes
|
285
|
+
# @return [ActiveSupport::Duration]
|
283
286
|
# @!method $1=(value)
|
284
|
-
# Sets the heartbeat timeout for the `/live` endpoint of the status server
|
285
|
-
# and
|
286
|
-
# @param value [
|
287
|
+
# Sets the heartbeat timeout for the `/live` endpoint of the status server. Must be between 1 minute
|
288
|
+
# and 24 hours.
|
289
|
+
# @param value [ActiveSupport::Duration] The timeout in minutes
|
287
290
|
# @return [self]
|
288
|
-
define_option :healthcheck_timeout, default: 30, type:
|
291
|
+
define_option :healthcheck_timeout, default: 30.minutes, type: ActiveSupport::Duration,
|
292
|
+
range: (1.minute)..(1.day)
|
293
|
+
|
294
|
+
# @!macro
|
295
|
+
# @!method $1?
|
296
|
+
# Whether the priority should be inverted. If true, the job with the highest value as the priority is the most
|
297
|
+
# important. If false, the job with the lowest value as the priority is the most important.
|
298
|
+
# === Default value:
|
299
|
+
# false
|
300
|
+
# @return [Boolean]
|
301
|
+
# @!method $1=(value)
|
302
|
+
# Sets whether the the priority should be inverted. When true, the job with the highest value as the priority is
|
303
|
+
# the most important; when false, the job with the lowest value as the priority is the most important.
|
304
|
+
# @param value [Boolean] whether the job with the highest priority value is the most important.
|
305
|
+
# @return [self]
|
306
|
+
define_option :inverse_priority, reader: :inverse_priority?, type: [TrueClass, FalseClass], required: true,
|
307
|
+
default: false
|
289
308
|
|
290
309
|
# @!macro
|
291
310
|
# @!method $1?
|
@@ -305,13 +324,14 @@ module Exekutor
|
|
305
324
|
{
|
306
325
|
min_threads: min_execution_threads,
|
307
326
|
max_threads: max_execution_threads,
|
308
|
-
max_thread_idletime: max_execution_thread_idletime
|
327
|
+
max_thread_idletime: max_execution_thread_idletime.to_f
|
309
328
|
}.tap do |opts|
|
310
329
|
opts[:set_db_connection_name] = set_db_connection_name? unless set_db_connection_name.nil?
|
311
330
|
%i[enable_listener delete_completed_jobs delete_discarded_jobs delete_failed_jobs].each do |option|
|
312
331
|
opts[option] = send(:"#{option}?") ? true : false
|
313
332
|
end
|
314
|
-
%i[polling_interval polling_jitter status_server_handler healthcheck_timeout]
|
333
|
+
%i[polling_interval polling_jitter status_server_handler status_server_port healthcheck_timeout]
|
334
|
+
.each do |option|
|
315
335
|
opts[option] = send(option)
|
316
336
|
end
|
317
337
|
end
|
@@ -351,6 +371,36 @@ module Exekutor
|
|
351
371
|
def error_class
|
352
372
|
Error
|
353
373
|
end
|
374
|
+
|
375
|
+
# Validates the value for a serializer, which must implement dump & load
|
376
|
+
class SerializerValidator
|
377
|
+
# @param serializer [Any] the value to validate
|
378
|
+
# @return [Boolean] whether the serializer has implemented dump & load
|
379
|
+
def self.valid?(serializer)
|
380
|
+
serializer.respond_to?(:dump) && serializer.respond_to?(:load)
|
381
|
+
end
|
382
|
+
|
383
|
+
# Tries to convert the specified value to a serializer, raises an error if the conversion fails.
|
384
|
+
# @param serializer [Any] the value to convert
|
385
|
+
# @return [#dump&#load]
|
386
|
+
# @raise [Error] if the serializer has not implemented dump & load
|
387
|
+
def self.convert!(serializer)
|
388
|
+
return serializer if SerializerValidator.valid? serializer
|
389
|
+
|
390
|
+
if serializer.respond_to?(:call)
|
391
|
+
serializer = serializer.call
|
392
|
+
return serializer if SerializerValidator.valid? serializer
|
393
|
+
end
|
394
|
+
if serializer.respond_to?(:new)
|
395
|
+
serializer = serializer.new
|
396
|
+
return serializer if SerializerValidator.valid? serializer
|
397
|
+
end
|
398
|
+
|
399
|
+
raise Error, <<~MSG.squish
|
400
|
+
The configured serializer (#{serializer.class}) does not respond to #dump and #load
|
401
|
+
MSG
|
402
|
+
end
|
403
|
+
end
|
354
404
|
end
|
355
405
|
|
356
406
|
def self.config
|
@@ -358,16 +408,20 @@ module Exekutor
|
|
358
408
|
end
|
359
409
|
|
360
410
|
def self.configure(opts = nil, &block)
|
361
|
-
raise ArgumentError, "opts must be
|
362
|
-
raise ArgumentError, "Either opts or a block must be given" unless opts.present? || block_given?
|
411
|
+
raise ArgumentError, "either opts or a block must be given" unless opts || block
|
363
412
|
|
364
|
-
|
365
|
-
|
413
|
+
if opts
|
414
|
+
raise ArgumentError, "opts must be a Hash" unless opts.is_a?(Hash)
|
366
415
|
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
416
|
+
config.set(**opts)
|
417
|
+
end
|
418
|
+
if block
|
419
|
+
if block.arity.zero?
|
420
|
+
instance_eval(&block)
|
421
|
+
else
|
422
|
+
yield config
|
423
|
+
end
|
371
424
|
end
|
425
|
+
self
|
372
426
|
end
|
373
427
|
end
|