exekutor 0.1.0 → 0.1.1
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 +0 -0
- 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 +48 -25
- 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 +31 -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 +190 -123
- data/lib/exekutor/internal/configuration_builder.rb +40 -27
- 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 +66 -39
- data/lib/exekutor/internal/logger.rb +28 -10
- data/lib/exekutor/internal/provider.rb +93 -74
- data/lib/exekutor/internal/reserver.rb +27 -12
- data/lib/exekutor/internal/status_server.rb +81 -49
- 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 +40 -22
- data/lib/exekutor/version.rb +1 -1
- data/lib/exekutor/worker.rb +88 -47
- data/lib/exekutor.rb +2 -2
- data/lib/generators/exekutor/configuration_generator.rb +9 -5
- data/lib/generators/exekutor/install_generator.rb +26 -15
- data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +11 -10
- data.tar.gz.sig +0 -0
- metadata +63 -19
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 94b3158b3e0df0ca892836df74beb23ccfd8bddf5a1377a46b5ebd881cb5bee8
|
4
|
+
data.tar.gz: a24e415df40ba9bf69d6972da2fb6461f60c655976d81f87ff36f2e365da42a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8de1e356a03b80f1e7dd82802956902531f76111cbea7210ee61eeeb34dbccc22120e5127f24ec9112328bfcb0f175f593f7ab4a9e6f347165b168503c4dbf49
|
7
|
+
data.tar.gz: 33c945b0e29d611d2e25d98985e75df68494bb3463d014efda5a60c7402acb711541a465b6e4db10afcbb8f2db4b86d9d2c2fd3d6b23a140bf324eefbeb8cf51
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
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
|
@@ -305,7 +294,7 @@ module Exekutor
|
|
305
294
|
{
|
306
295
|
min_threads: min_execution_threads,
|
307
296
|
max_threads: max_execution_threads,
|
308
|
-
max_thread_idletime: max_execution_thread_idletime
|
297
|
+
max_thread_idletime: max_execution_thread_idletime
|
309
298
|
}.tap do |opts|
|
310
299
|
opts[:set_db_connection_name] = set_db_connection_name? unless set_db_connection_name.nil?
|
311
300
|
%i[enable_listener delete_completed_jobs delete_discarded_jobs delete_failed_jobs].each do |option|
|
@@ -351,6 +340,36 @@ module Exekutor
|
|
351
340
|
def error_class
|
352
341
|
Error
|
353
342
|
end
|
343
|
+
|
344
|
+
# Validates the value for a serializer, which must implement dump & load
|
345
|
+
class SerializerValidator
|
346
|
+
# @param serializer [Any] the value to validate
|
347
|
+
# @return [Boolean] whether the serializer has implemented dump & load
|
348
|
+
def self.valid?(serializer)
|
349
|
+
serializer.respond_to?(:dump) && serializer.respond_to?(:load)
|
350
|
+
end
|
351
|
+
|
352
|
+
# Tries to convert the specified value to a serializer, raises an error if the conversion fails.
|
353
|
+
# @param serializer [Any] the value to convert
|
354
|
+
# @return [#dump&#load]
|
355
|
+
# @raise [Error] if the serializer has not implemented dump & load
|
356
|
+
def self.convert!(serializer)
|
357
|
+
return serializer if SerializerValidator.valid? serializer
|
358
|
+
|
359
|
+
if serializer.respond_to?(:call)
|
360
|
+
serializer = serializer.call
|
361
|
+
return serializer if SerializerValidator.valid? serializer
|
362
|
+
end
|
363
|
+
if serializer.respond_to?(:new)
|
364
|
+
serializer = serializer.new
|
365
|
+
return serializer if SerializerValidator.valid? serializer
|
366
|
+
end
|
367
|
+
|
368
|
+
raise Error, <<~MSG.squish
|
369
|
+
The configured serializer (#{serializer.class}) does not respond to #dump and #load
|
370
|
+
MSG
|
371
|
+
end
|
372
|
+
end
|
354
373
|
end
|
355
374
|
|
356
375
|
def self.config
|
@@ -358,16 +377,20 @@ module Exekutor
|
|
358
377
|
end
|
359
378
|
|
360
379
|
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?
|
380
|
+
raise ArgumentError, "either opts or a block must be given" unless opts || block
|
363
381
|
|
364
|
-
|
365
|
-
|
382
|
+
if opts
|
383
|
+
raise ArgumentError, "opts must be a Hash" unless opts.is_a?(Hash)
|
366
384
|
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
385
|
+
config.set(**opts)
|
386
|
+
end
|
387
|
+
if block
|
388
|
+
if block.arity.zero?
|
389
|
+
instance_eval(&block)
|
390
|
+
else
|
391
|
+
yield config
|
392
|
+
end
|
371
393
|
end
|
394
|
+
self
|
372
395
|
end
|
373
396
|
end
|
data/lib/exekutor/hook.rb
CHANGED
@@ -27,12 +27,12 @@ module Exekutor
|
|
27
27
|
before_enqueue around_enqueue after_enqueue before_job_execution around_job_execution after_job_execution
|
28
28
|
on_job_failure on_fatal_error before_startup after_startup before_shutdown after_shutdown
|
29
29
|
].freeze
|
30
|
-
private_constant
|
30
|
+
private_constant :CALLBACK_NAMES
|
31
31
|
|
32
32
|
included do
|
33
33
|
class_attribute :__callbacks, default: Hash.new { |h, k| h[k] = [] }
|
34
34
|
|
35
|
-
private_class_method :
|
35
|
+
private_class_method :_add_callback
|
36
36
|
end
|
37
37
|
|
38
38
|
# Gets the registered callbacks
|
@@ -53,7 +53,6 @@ module Exekutor
|
|
53
53
|
end
|
54
54
|
|
55
55
|
class_methods do
|
56
|
-
|
57
56
|
# @!method before_enqueue
|
58
57
|
# Registers a callback to be called before a job is enqueued.
|
59
58
|
# @param methods [Symbol] the method(s) to call
|
@@ -141,9 +140,9 @@ module Exekutor
|
|
141
140
|
|
142
141
|
CALLBACK_NAMES.each do |name|
|
143
142
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
144
|
-
def #{name}(*methods, &callback)
|
145
|
-
|
146
|
-
end
|
143
|
+
def #{name}(*methods, &callback) # def callback_name(*methods, &callback
|
144
|
+
_add_callback :#{name}, methods, callback # _add_callback :callback_name, methods, callback
|
145
|
+
end # end
|
147
146
|
RUBY
|
148
147
|
end
|
149
148
|
|
@@ -156,16 +155,21 @@ module Exekutor
|
|
156
155
|
raise Error, "Invalid callback type: #{type} (Expected one of: #{CALLBACK_NAMES.map(&:inspect).join(", ")}"
|
157
156
|
end
|
158
157
|
|
159
|
-
|
158
|
+
_add_callback type, methods, callback
|
160
159
|
true
|
161
160
|
end
|
162
161
|
|
163
|
-
|
164
|
-
|
162
|
+
# @!visibility private
|
163
|
+
def _add_callback(type, methods, callback)
|
165
164
|
raise Error, "Either a method or a callback block must be supplied" if methods.present? && callback.present?
|
166
165
|
|
167
|
-
methods
|
168
|
-
|
166
|
+
if methods.present?
|
167
|
+
methods.each { |method| __callbacks[type] << [method, nil] }
|
168
|
+
elsif callback.present?
|
169
|
+
__callbacks[type] << [nil, callback]
|
170
|
+
else
|
171
|
+
raise Error, "No method or callback block supplied"
|
172
|
+
end
|
169
173
|
end
|
170
174
|
end
|
171
175
|
end
|
data/lib/exekutor/info/worker.rb
CHANGED
@@ -12,9 +12,9 @@ module Exekutor
|
|
12
12
|
|
13
13
|
# Registers a heartbeat for this worker, if necessary
|
14
14
|
def heartbeat!
|
15
|
-
now = Time.
|
16
|
-
touch :last_heartbeat_at, time: now if
|
15
|
+
now = Time.current.change(sec: 0)
|
16
|
+
touch :last_heartbeat_at, time: now if last_heartbeat_at.nil? || now >= last_heartbeat_at + 1.minute
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
20
|
-
end
|
20
|
+
end
|