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.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +4 -4
  3. data/.gitignore +1 -2
  4. data/.rubocop.yml +23 -14
  5. data/.rubocop_airbnb.yml +2 -0
  6. data/.rubocop_todo.yml +1 -13
  7. data/.travis.yml +26 -0
  8. data/Gemfile +2 -2
  9. data/Gemfile.lock +118 -0
  10. data/Guardfile +1 -0
  11. data/README.md +96 -31
  12. data/Rakefile +2 -0
  13. data/exel.gemspec +7 -7
  14. data/lib/exel.rb +7 -1
  15. data/lib/exel/ast_node.rb +6 -10
  16. data/lib/exel/context.rb +4 -1
  17. data/lib/exel/deferred_context_value.rb +3 -1
  18. data/lib/exel/error/job_termination.rb +12 -0
  19. data/lib/exel/events.rb +6 -0
  20. data/lib/exel/instruction.rb +5 -2
  21. data/lib/exel/instruction_node.rb +2 -0
  22. data/lib/exel/job.rb +8 -4
  23. data/lib/exel/listen_instruction.rb +2 -0
  24. data/lib/exel/logging.rb +24 -1
  25. data/lib/exel/logging/logger_wrapper.rb +31 -0
  26. data/lib/exel/logging_helper.rb +36 -0
  27. data/lib/exel/middleware/chain.rb +67 -0
  28. data/lib/exel/middleware/logging.rb +30 -0
  29. data/lib/exel/null_instruction.rb +2 -0
  30. data/lib/exel/processor_helper.rb +9 -1
  31. data/lib/exel/processors/async_processor.rb +2 -8
  32. data/lib/exel/processors/run_processor.rb +2 -6
  33. data/lib/exel/processors/split_processor.rb +15 -10
  34. data/lib/exel/providers/local_file_provider.rb +9 -6
  35. data/lib/exel/providers/threaded_async_provider.rb +2 -0
  36. data/lib/exel/remote_value.rb +11 -0
  37. data/lib/exel/sequence_node.rb +2 -0
  38. data/lib/exel/value.rb +2 -0
  39. data/lib/exel/version.rb +3 -1
  40. data/spec/exel/ast_node_spec.rb +48 -27
  41. data/spec/exel/context_spec.rb +77 -77
  42. data/spec/exel/deferred_context_value_spec.rb +42 -42
  43. data/spec/exel/events_spec.rb +68 -59
  44. data/spec/exel/instruction_node_spec.rb +17 -16
  45. data/spec/exel/instruction_spec.rb +49 -42
  46. data/spec/exel/job_spec.rb +99 -84
  47. data/spec/exel/listen_instruction_spec.rb +11 -10
  48. data/spec/exel/logging/logger_wrapper_spec.rb +93 -0
  49. data/spec/exel/logging_helper_spec.rb +24 -0
  50. data/spec/exel/logging_spec.rb +69 -24
  51. data/spec/exel/middleware/chain_spec.rb +65 -0
  52. data/spec/exel/middleware/logging_spec.rb +31 -0
  53. data/spec/exel/middleware_spec.rb +68 -0
  54. data/spec/exel/null_instruction_spec.rb +4 -4
  55. data/spec/exel/processors/async_processor_spec.rb +17 -18
  56. data/spec/exel/processors/run_processor_spec.rb +10 -11
  57. data/spec/exel/processors/split_processor_spec.rb +99 -74
  58. data/spec/exel/providers/local_file_provider_spec.rb +26 -28
  59. data/spec/exel/providers/threaded_async_provider_spec.rb +37 -38
  60. data/spec/exel/sequence_node_spec.rb +12 -11
  61. data/spec/exel/value_spec.rb +33 -33
  62. data/spec/exel_spec.rb +9 -7
  63. data/spec/integration/integration_spec.rb +3 -1
  64. data/spec/spec_helper.rb +4 -2
  65. data/spec/support/integration_test_classes.rb +4 -3
  66. metadata +37 -48
@@ -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
@@ -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
- fail_silently { run(context) }
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
@@ -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) { |a, e| a[e] }
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
@@ -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)
@@ -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 = nil)
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).process(@subtree)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './ast_node'
2
4
 
3
5
  module EXEL
@@ -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
- (ast = parse(dsl_code_or_name)) ? ast.start(context) : raise(%(Job "#{dsl_code_or_name}" not found))
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, sub_tree, args = {})
94
- instruction = EXEL::Instruction.new(processor, args, sub_tree)
95
- node = sub_tree.nil? ? InstructionNode.new(instruction) : InstructionNode.new(instruction, [sub_tree])
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'events'
2
4
 
3
5
  module EXEL
@@ -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 || Logger.new('/dev/null')
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EXEL
2
4
  # An {Instruction} that does nothing when executed
3
5
  class NullInstruction
@@ -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 > 0
67
+ sleep(time_to_sleep) if time_to_sleep.positive?
60
68
  end
61
69
  end
62
70
  end