exel 1.2.1 → 1.5.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|