exel 1.2.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.codeclimate.yml +4 -4
- data/.gitignore +1 -2
- data/.rubocop.yml +23 -14
- data/.rubocop_airbnb.yml +2 -0
- data/.rubocop_todo.yml +1 -13
- data/.travis.yml +26 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +118 -0
- data/Guardfile +1 -0
- data/README.md +96 -31
- data/Rakefile +2 -0
- data/exel.gemspec +7 -7
- data/lib/exel.rb +7 -1
- data/lib/exel/ast_node.rb +6 -10
- data/lib/exel/context.rb +4 -1
- data/lib/exel/deferred_context_value.rb +3 -1
- data/lib/exel/error/job_termination.rb +12 -0
- data/lib/exel/events.rb +6 -0
- data/lib/exel/instruction.rb +5 -2
- data/lib/exel/instruction_node.rb +2 -0
- data/lib/exel/job.rb +8 -4
- data/lib/exel/listen_instruction.rb +2 -0
- data/lib/exel/logging.rb +24 -1
- data/lib/exel/logging/logger_wrapper.rb +31 -0
- data/lib/exel/logging_helper.rb +36 -0
- data/lib/exel/middleware/chain.rb +67 -0
- data/lib/exel/middleware/logging.rb +30 -0
- data/lib/exel/null_instruction.rb +2 -0
- data/lib/exel/processor_helper.rb +9 -1
- data/lib/exel/processors/async_processor.rb +2 -8
- data/lib/exel/processors/run_processor.rb +2 -6
- data/lib/exel/processors/split_processor.rb +15 -10
- data/lib/exel/providers/local_file_provider.rb +9 -6
- data/lib/exel/providers/threaded_async_provider.rb +2 -0
- data/lib/exel/remote_value.rb +11 -0
- data/lib/exel/sequence_node.rb +2 -0
- data/lib/exel/value.rb +2 -0
- data/lib/exel/version.rb +3 -1
- data/spec/exel/ast_node_spec.rb +48 -27
- data/spec/exel/context_spec.rb +77 -77
- data/spec/exel/deferred_context_value_spec.rb +42 -42
- data/spec/exel/events_spec.rb +68 -59
- data/spec/exel/instruction_node_spec.rb +17 -16
- data/spec/exel/instruction_spec.rb +49 -42
- data/spec/exel/job_spec.rb +99 -84
- data/spec/exel/listen_instruction_spec.rb +11 -10
- data/spec/exel/logging/logger_wrapper_spec.rb +93 -0
- data/spec/exel/logging_helper_spec.rb +24 -0
- data/spec/exel/logging_spec.rb +69 -24
- data/spec/exel/middleware/chain_spec.rb +65 -0
- data/spec/exel/middleware/logging_spec.rb +31 -0
- data/spec/exel/middleware_spec.rb +68 -0
- data/spec/exel/null_instruction_spec.rb +4 -4
- data/spec/exel/processors/async_processor_spec.rb +17 -18
- data/spec/exel/processors/run_processor_spec.rb +10 -11
- data/spec/exel/processors/split_processor_spec.rb +99 -74
- data/spec/exel/providers/local_file_provider_spec.rb +26 -28
- data/spec/exel/providers/threaded_async_provider_spec.rb +37 -38
- data/spec/exel/sequence_node_spec.rb +12 -11
- data/spec/exel/value_spec.rb +33 -33
- data/spec/exel_spec.rb +9 -7
- data/spec/integration/integration_spec.rb +3 -1
- data/spec/spec_helper.rb +4 -2
- data/spec/support/integration_test_classes.rb +4 -3
- metadata +37 -48
data/lib/exel.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'exel/version'
|
2
4
|
require 'exel/logging'
|
3
5
|
require 'ostruct'
|
@@ -18,7 +20,7 @@ module EXEL
|
|
18
20
|
|
19
21
|
# @return The current configuration
|
20
22
|
def self.configuration
|
21
|
-
@config ||= OpenStruct.new
|
23
|
+
@config ||= OpenStruct.new(middleware: Middleware::Chain.new)
|
22
24
|
end
|
23
25
|
|
24
26
|
# Yields the configuration object to the given block. Configuration can include:
|
@@ -46,6 +48,10 @@ module EXEL
|
|
46
48
|
configuration.remote_provider || Providers::LocalFileProvider
|
47
49
|
end
|
48
50
|
|
51
|
+
def self.middleware
|
52
|
+
configuration.middleware
|
53
|
+
end
|
54
|
+
|
49
55
|
root = File.expand_path('../..', __FILE__)
|
50
56
|
Dir[File.join(root, 'lib/exel/**/*.rb')].each { |file| require file }
|
51
57
|
end
|
data/lib/exel/ast_node.rb
CHANGED
@@ -1,15 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EXEL
|
2
4
|
# An abstract class that serves as the parent class of nodes in the AST
|
3
5
|
class ASTNode
|
4
6
|
attr_reader :instruction, :children
|
5
7
|
|
6
|
-
def initialize(instruction, children
|
8
|
+
def initialize(instruction, children: [])
|
7
9
|
@instruction = instruction
|
8
10
|
@children = children
|
9
11
|
end
|
10
12
|
|
11
13
|
def start(context)
|
12
|
-
|
14
|
+
run(context)
|
15
|
+
rescue EXEL::Error::JobTermination => e
|
16
|
+
EXEL.logger.send(e.cmd, "JobTerminationError: #{e.message.chomp}")
|
13
17
|
end
|
14
18
|
|
15
19
|
def run(_context)
|
@@ -19,13 +23,5 @@ module EXEL
|
|
19
23
|
def add_child(node)
|
20
24
|
@children << node
|
21
25
|
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def fail_silently(&_block)
|
26
|
-
yield if block_given?
|
27
|
-
rescue EXEL::Error::JobTermination => e
|
28
|
-
EXEL.logger.error "JobTerminationError: #{e.message.chomp}"
|
29
|
-
end
|
30
26
|
end
|
31
27
|
end
|
data/lib/exel/context.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'tempfile'
|
4
|
+
require 'securerandom'
|
2
5
|
|
3
6
|
module EXEL
|
4
7
|
# The +Context+ is the shared memory of a running job. It acts as the source of input to processors and the place for
|
@@ -32,7 +35,7 @@ module EXEL
|
|
32
35
|
file = EXEL::Value.localize(uri)
|
33
36
|
|
34
37
|
begin
|
35
|
-
context = Marshal.load(file.read)
|
38
|
+
context = Marshal.load(file.read) # rubocop:disable Airbnb/UnsafeYamlMarshal
|
36
39
|
ensure
|
37
40
|
file.close
|
38
41
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EXEL
|
2
4
|
# When +context+ is referenced in a job definition, an instance of +DeferredContextValue+ will be put in its place.
|
3
5
|
# At runtime, the first time a +DeferredContextValue+ is read via {EXEL::Context#[]}, it will be replaced by the value
|
@@ -47,7 +49,7 @@ module EXEL
|
|
47
49
|
|
48
50
|
# Given a context, returns the value that this instance was acting as a placeholder for.
|
49
51
|
def get(context)
|
50
|
-
keys.reduce(context) { |
|
52
|
+
keys.reduce(context) { |acc, elem| acc[elem] }
|
51
53
|
end
|
52
54
|
end
|
53
55
|
end
|
@@ -1,8 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Lint/InheritException
|
1
4
|
module EXEL
|
2
5
|
module Error
|
3
6
|
# If a processor raises a JobTermination exception, the job will immediately stop running without raising anything.
|
4
7
|
# This is useful if you want to stop a job without triggering any kind of retry mechanism, for example.
|
5
8
|
class JobTermination < Exception # Inherit from Exception so it won't be rescued and can propagate to ASTNode#start
|
9
|
+
attr_reader :cmd
|
10
|
+
|
11
|
+
CMDS = [:info, :warn, :error].freeze
|
12
|
+
|
13
|
+
def initialize(message = nil, cmd = :error)
|
14
|
+
super(message)
|
15
|
+
@cmd = CMDS.include?(cmd) ? cmd : :error
|
16
|
+
end
|
6
17
|
end
|
7
18
|
end
|
8
19
|
end
|
20
|
+
# rubocop:enable Lint/InheritException
|
data/lib/exel/events.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EXEL
|
2
4
|
# Provides methods for registering and triggering event listeners
|
3
5
|
module Events
|
@@ -11,6 +13,10 @@ module EXEL
|
|
11
13
|
listeners_for_event(event, context).each { |listener| listener.send(event, context, data) }
|
12
14
|
end
|
13
15
|
|
16
|
+
def self.included(other)
|
17
|
+
other.class_eval { attr_reader :context }
|
18
|
+
end
|
19
|
+
|
14
20
|
private
|
15
21
|
|
16
22
|
def listeners_for_event(event, context)
|
data/lib/exel/instruction.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EXEL
|
2
4
|
# Represents one step to be executed in the processing of a job
|
3
5
|
class Instruction
|
4
|
-
def initialize(processor_class, args, subtree
|
6
|
+
def initialize(processor_class, args, subtree: nil)
|
5
7
|
@processor_class = processor_class
|
6
8
|
@args = args || {}
|
7
9
|
@subtree = subtree
|
@@ -9,7 +11,8 @@ module EXEL
|
|
9
11
|
|
10
12
|
def execute(context)
|
11
13
|
context.merge!(@args)
|
12
|
-
@processor_class.new(context)
|
14
|
+
processor = @processor_class.new(context)
|
15
|
+
EXEL.middleware.invoke(@processor_class, context, @args) { processor.process(@subtree) }
|
13
16
|
end
|
14
17
|
end
|
15
18
|
end
|
data/lib/exel/job.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EXEL
|
2
4
|
# The +Job+ module provides the main interface for defining and running EXEL jobs
|
3
5
|
module Job
|
@@ -25,7 +27,9 @@ module EXEL
|
|
25
27
|
# @raise If no job has been registered with the given name
|
26
28
|
def run(dsl_code_or_name, context = {})
|
27
29
|
context = EXEL::Context.new(context) if context.instance_of?(Hash)
|
28
|
-
|
30
|
+
ast = parse(dsl_code_or_name)
|
31
|
+
ast ? ast.start(context) : raise(%(Job "#{dsl_code_or_name}" not found))
|
32
|
+
context
|
29
33
|
end
|
30
34
|
|
31
35
|
private
|
@@ -90,9 +94,9 @@ module EXEL
|
|
90
94
|
block.nil? ? nil : Parser.parse(block)
|
91
95
|
end
|
92
96
|
|
93
|
-
def add_instruction_node(processor,
|
94
|
-
instruction = EXEL::Instruction.new(processor, args,
|
95
|
-
node =
|
97
|
+
def add_instruction_node(processor, subtree, args = {})
|
98
|
+
instruction = EXEL::Instruction.new(processor, args, subtree: subtree)
|
99
|
+
node = subtree.nil? ? InstructionNode.new(instruction) : InstructionNode.new(instruction, children: [subtree])
|
96
100
|
@ast.add_child(node)
|
97
101
|
end
|
98
102
|
end
|
data/lib/exel/logging.rb
CHANGED
@@ -1,9 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'logger'
|
2
4
|
|
3
5
|
module EXEL
|
4
6
|
module Logging
|
5
7
|
DEFAULT_LEVEL = :info
|
6
8
|
|
9
|
+
# Formats log messages with timestamp, severity and Logging prefix (if set via {Logging.with_prefix})
|
10
|
+
class PrefixFormatter < Logger::Formatter
|
11
|
+
def call(severity, time, _program_name, message)
|
12
|
+
"#{time.utc} severity=#{severity}, #{Logging.prefix}#{message}\n"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
7
16
|
def self.logger
|
8
17
|
@logger || initialize_logger
|
9
18
|
end
|
@@ -11,6 +20,7 @@ module EXEL
|
|
11
20
|
def self.initialize_logger
|
12
21
|
@logger = Logger.new(log_filename)
|
13
22
|
@logger.level = log_level
|
23
|
+
@logger.formatter = PrefixFormatter.new
|
14
24
|
@logger
|
15
25
|
end
|
16
26
|
|
@@ -24,7 +34,20 @@ module EXEL
|
|
24
34
|
end
|
25
35
|
|
26
36
|
def self.logger=(logger)
|
27
|
-
@logger = logger
|
37
|
+
@logger = logger ? LoggerWrapper.new(logger) : Logger.new('/dev/null')
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets a prefix to be added to any messages sent to the EXEL logger in the given block.
|
41
|
+
def self.with_prefix(prefix)
|
42
|
+
Thread.current[:exel_log_prefix] ||= []
|
43
|
+
Thread.current[:exel_log_prefix].push(prefix)
|
44
|
+
yield
|
45
|
+
ensure
|
46
|
+
Thread.current[:exel_log_prefix].pop
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.prefix
|
50
|
+
Thread.current[:exel_log_prefix]&.last
|
28
51
|
end
|
29
52
|
end
|
30
53
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EXEL
|
4
|
+
module Logging
|
5
|
+
# Wraps calls to a logger to add {Logging} prefix to log messages
|
6
|
+
class LoggerWrapper < SimpleDelegator
|
7
|
+
LOG_LEVELS = %i(debug info warn error fatal unknown).freeze
|
8
|
+
|
9
|
+
LOG_LEVELS.each do |level|
|
10
|
+
define_method level do |progname = nil, &block|
|
11
|
+
prefix_block = nil
|
12
|
+
|
13
|
+
if block
|
14
|
+
prefix_block = proc { "#{Logging.prefix}#{block.call}" }
|
15
|
+
else
|
16
|
+
progname = "#{Logging.prefix}#{progname}"
|
17
|
+
end
|
18
|
+
|
19
|
+
__getobj__.send(level, progname, &prefix_block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def add(severity, message = nil, progname = nil)
|
24
|
+
if message.nil? && block_given?
|
25
|
+
message = yield
|
26
|
+
end
|
27
|
+
__getobj__.add(severity, "#{Logging.prefix}#{message}", progname)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EXEL
|
4
|
+
# Logging related helper methods for processors
|
5
|
+
module LoggingHelper
|
6
|
+
# @return [Logger] Returns the EXEL logger
|
7
|
+
def logger
|
8
|
+
EXEL.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
# Logs a message with DEBUG severity
|
12
|
+
def log_debug(message)
|
13
|
+
logger.debug(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Logs a message with INFO severity
|
17
|
+
def log_info(message)
|
18
|
+
logger.info(message)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Logs a message with WARN severity
|
22
|
+
def log_warn(message)
|
23
|
+
logger.warn(message)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Logs a message with ERROR severity
|
27
|
+
def log_error(message)
|
28
|
+
logger.error(message)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Logs a message with FATAL severity
|
32
|
+
def log_fatal(message)
|
33
|
+
logger.fatal(message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EXEL
|
4
|
+
# Middleware is code configured to run around each processor execution. Custom middleware can be added as follows:
|
5
|
+
#
|
6
|
+
# EXEL.configure do |config|
|
7
|
+
# config.middleware.add(MyMiddleware)
|
8
|
+
# config.middleware.add(AnotherMiddleware, 'constructor arg')
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# Middleware can be any class that implements a +call+ method that includes a call to +yield+:
|
12
|
+
#
|
13
|
+
# class MyMiddleware
|
14
|
+
# def call(processor, context, args)
|
15
|
+
# puts 'before process'
|
16
|
+
# yield
|
17
|
+
# puts 'after process'
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# The +call+ method will be passed the class of the processor that will be executed, the current context, and any args
|
22
|
+
# that were passed to the processor in the job definition.
|
23
|
+
module Middleware
|
24
|
+
# Chain of middleware to be invoked in sequence around each processor execution.
|
25
|
+
class Chain
|
26
|
+
attr_reader :entries
|
27
|
+
|
28
|
+
Entry = Struct.new(:klass, :args)
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@entries = []
|
32
|
+
end
|
33
|
+
|
34
|
+
# Adds a middleware class to the chain. If it is already in the chain it will be removed and added to the end.
|
35
|
+
# Any additional arguments will be passed to +new+ when the middleware is created.
|
36
|
+
def add(klass, *args)
|
37
|
+
remove(klass)
|
38
|
+
@entries << Entry.new(klass, args)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Removes a middleware class from the chain.
|
42
|
+
def remove(klass)
|
43
|
+
@entries.delete_if { |entry| entry.klass == klass }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns true if the given class is in the chain.
|
47
|
+
def include?(klass)
|
48
|
+
@entries.any? { |entry| entry.klass == klass }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Calls each middleware in the chain.
|
52
|
+
def invoke(*args)
|
53
|
+
chain = @entries.map { |entry| entry.klass.new(*entry.args) }
|
54
|
+
|
55
|
+
traverse_chain = lambda do
|
56
|
+
if chain.empty?
|
57
|
+
yield
|
58
|
+
else
|
59
|
+
chain.shift.call(*args, &traverse_chain)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
traverse_chain.call
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EXEL
|
4
|
+
module Middleware
|
5
|
+
# Middleware to add a prefix to all messages logged during processor execution. The prefix is specified by the
|
6
|
+
# +:log_prefix+ key in the context. Also logs start, finish, and failure of the processor execution.
|
7
|
+
class Logging
|
8
|
+
def call(processor_class, context, _args, &block)
|
9
|
+
EXEL::Logging.with_prefix("#{context[:log_prefix]}[#{processor_class}] ") { log_process(&block) }
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def log_process
|
15
|
+
start_time = Time.now
|
16
|
+
|
17
|
+
yield
|
18
|
+
|
19
|
+
EXEL.logger.info "Finished in #{duration(start_time)} seconds"
|
20
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
21
|
+
EXEL.logger.info "Failed in #{duration(start_time)} seconds"
|
22
|
+
raise
|
23
|
+
end
|
24
|
+
|
25
|
+
def duration(start_time)
|
26
|
+
(Time.now - start_time).round(3)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,6 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EXEL
|
2
4
|
# Helper methods useful to processors
|
5
|
+
# @deprecated Most functionality replaced by {EXEL::Middleware::Logging} middleware.
|
3
6
|
module ProcessorHelper
|
7
|
+
def self.included(other)
|
8
|
+
warn "DEPRECATION WARNING: [#{other}] EXEL::ProcessorHelper will be removed. For process logging, please use "\
|
9
|
+
'EXEL::Middleware::Logging instead'
|
10
|
+
end
|
11
|
+
|
4
12
|
def tag(*tags)
|
5
13
|
tags.map { |t| "[#{t}]" }.join('')
|
6
14
|
end
|
@@ -56,7 +64,7 @@ module EXEL
|
|
56
64
|
def ensure_transaction_duration(duration, start_time)
|
57
65
|
elapsed_time = Time.now.to_f - start_time.to_f
|
58
66
|
time_to_sleep = duration.second.to_f - elapsed_time
|
59
|
-
sleep(time_to_sleep) if time_to_sleep
|
67
|
+
sleep(time_to_sleep) if time_to_sleep.positive?
|
60
68
|
end
|
61
69
|
end
|
62
70
|
end
|