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
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
|