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.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bc7c7a8f6af32b1f764124d3905673240c167abd19b93e492016433414078a9
4
- data.tar.gz: 37f8fb516c32a2a71595b2d608b2610ba78a5396e7f1b1e6bc7c2e90da8d770d
3
+ metadata.gz: d5b178052eb277168916a76f663ba53882204481f740a7c30bc309d243de79cf
4
+ data.tar.gz: bfb648002d86bf278b41b5f7ca6fde0f1d3cb39954c445eec0e268bf72120efe
5
5
  SHA512:
6
- metadata.gz: 8f36b7e5a5968009ba7f33657a0444053a72a5e7b91ebb8e5605c435d80a6861eec4a2d39af81c71ec6e88564a57f7d91fdaca43182ca9e5faf8c86cdd5ab7ff
7
- data.tar.gz: ab262caf9827f3eccd5995309b8f0debc0da125af459b8a8321fbd43c6a25d9bcd42dc6fa65f5a98bbb341be07fca4251a8ab73860d01c2ce43efd388499a145
6
+ metadata.gz: e2d7a7787f83dc362aeca814d511d0584e5e3e2af24209f1bfc00bd1efbae47d9226bc91e5615a0c7b38b5a515ad64146ec43d74e6cb3b757f3144fcc100d05e
7
+ data.tar.gz: 651ee14b261990adfe498e017afd6ea0e3e382e12f7bf4a352d01dd4d203938f62f2223159e7930bd517ee5fd68f191b2d0ad9189c13670158064e8381278b5b
checksums.yaml.gz.sig CHANGED
@@ -1,3 +1,2 @@
1
- e.o����+s9�=S�����y��C����[6*�s���>��؁Wu ��._���q���_��.�No�57Z�U�t�'kj}�V�l���9h�?���@ЋYݭJ�nW
2
- ���a ��XFB#�]6|���R�!�YҚ��Ʃ��0P��FG�"q��2�{<���b��W{��^��gAm034R/��qf.r��"�:�I��h0�>�|yryg1r8�^�#ۆ�7�/�W����Gv�1+���Fq�<�����y%��7Ҁ��9�sc�jc�0�Z����Ϯ��LP��%�
3
- ��;g\Rl��g���K_�1W�-J����YՃXz��js;���|����_���4{ B�WV�sڟ6 �7vʇ�b4�
1
+ ��<����}gw�2���ȝ�T���L�7!��wc��]<o���6���f{o���&�9���H@��<Zy7���-"׈'}5Hbu^�҄���W����I̒�k$턍�� )2-�*�߷���ci
2
+ ��#�s)�������b`gư�H� (H�����Uu��&v9(��H��*t()��`{3��<gmf(��?M�,$������r`3“�'��WkةyH��;'���^��0d)0>��(��!�9����@KI�<�������� ����a6��������V�\!�k���|��q�V X%�&�u��IK�˛�H+�>���lnY�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
- Process.setproctitle "Exekutor worker (Initializing…) [#{$PROGRAM_NAME}]"
4
+ require "exekutor/version"
5
+ require "exekutor/internal/cli/app"
6
6
 
7
7
  exit Exekutor::Internal::CLI::App.run(ARGV)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "active_job"
3
4
  require "active_job/queue_adapters"
4
5
 
@@ -11,4 +12,4 @@ module ActiveJob
11
12
  alias enqueue_at schedule_at
12
13
  end
13
14
  end
14
- end
15
+ end
@@ -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
- raise ArgumentError, "method_name must be a Symbol (actual: #{method_name.class.name})" unless method_name.is_a? Symbol
40
- raise ArgumentError, "alias_to must be present" unless alias_to.present?
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
- delegate = singleton_class
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
- delegate = self
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
- definitions[method_name] = alias_to
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
- obj_method = delegate.method(method)
87
- min_arg_length = 0
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
- args_len = args.length
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
- # {Exekutor::Asynchronous#perform_asynchronously} to mark a method as asynchronous.
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
- raise Error, "#{class_name} does not respond to #{method}"
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
- unless definitions.include? method.to_sym
179
- raise Error, "#{class_name}##{method} is not marked as asynchronous"
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
@@ -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 = case timeout
10
- when ActiveSupport::Duration
11
- timeout.ago
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 = case before
30
- when ActiveSupport::Duration
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 "DEFAULT_BASE_RECORD_CLASS"
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 = self.json_serializer
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 serializer.respond_to?(:dump) && serializer.respond_to?(:load)
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 in seconds. When set, the worker will poll the database with this interval to check for
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 [Integer]
186
+ # 60 seconds
187
+ # @return [ActiveSupport::Duration]
199
188
  # @!method $1=(value)
200
- # Sets the polling interval in seconds. Set to +nil+ to disable polling. If the listener is disabled, this value
201
- # should be reasonably low so jobs don't have to wait in the queue too long; if it is enabled, this value can
202
- # be reasonably high.
203
- # @param value [Integer] the interval
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: 60, type: [Integer, nil], range: 1...(1.day.to_i)
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 number of seconds a thread may be idle before being stopped.
243
+ # The maximum duration a thread may be idle before being stopped.
254
244
  # === Default value:
255
- # 60
256
- # @return [Integer]
245
+ # 60 seconds
246
+ # @return [ActiveSupport::Duration]
257
247
  # @!method $1=(value)
258
- # Sets the maximum number of seconds a thread may be idle before being stopped
259
- # @param value [Integer] the number of threads
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: 60, type: Integer, range: 1..(1.day.to_i)
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 heartbeat timeout for the `/live` endpoint of the status server, in minutes. If the heartbeat of a worker
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 [Integer]
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, in minutes. Must be between 2
285
- # and 1440 (24 hours).
286
- # @param value [Integer] The timeout in minutes
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: Integer, range: 2..1440
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].each do |option|
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 a Hash" unless opts.nil? || opts.is_a?(Hash)
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
- config.set(**opts) if opts
365
- return unless block_given?
413
+ if opts
414
+ raise ArgumentError, "opts must be a Hash" unless opts.is_a?(Hash)
366
415
 
367
- if block.arity == 1
368
- block.call config
369
- else
370
- instance_eval(&block)
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