exekutor 0.1.0 → 0.1.1
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 +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
|