exel 1.2.1 → 1.5.2

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