exel 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -4
  3. data/.rubocop_todo.yml +1 -13
  4. data/Gemfile +1 -0
  5. data/Guardfile +1 -0
  6. data/README.md +95 -30
  7. data/Rakefile +1 -0
  8. data/exel.gemspec +2 -2
  9. data/lib/exel.rb +6 -1
  10. data/lib/exel/ast_node.rb +4 -9
  11. data/lib/exel/context.rb +1 -0
  12. data/lib/exel/deferred_context_value.rb +1 -0
  13. data/lib/exel/error/job_termination.rb +9 -0
  14. data/lib/exel/events.rb +1 -0
  15. data/lib/exel/instruction.rb +3 -1
  16. data/lib/exel/instruction_node.rb +1 -0
  17. data/lib/exel/job.rb +1 -0
  18. data/lib/exel/listen_instruction.rb +1 -0
  19. data/lib/exel/logging.rb +22 -1
  20. data/lib/exel/logging/logger_wrapper.rb +28 -0
  21. data/lib/exel/logging_helper.rb +35 -0
  22. data/lib/exel/middleware/chain.rb +66 -0
  23. data/lib/exel/middleware/logging.rb +30 -0
  24. data/lib/exel/null_instruction.rb +1 -0
  25. data/lib/exel/processor_helper.rb +7 -0
  26. data/lib/exel/processors/async_processor.rb +2 -8
  27. data/lib/exel/processors/run_processor.rb +2 -7
  28. data/lib/exel/processors/split_processor.rb +5 -8
  29. data/lib/exel/providers/local_file_provider.rb +1 -0
  30. data/lib/exel/providers/threaded_async_provider.rb +1 -0
  31. data/lib/exel/sequence_node.rb +1 -0
  32. data/lib/exel/value.rb +1 -0
  33. data/lib/exel/version.rb +2 -1
  34. data/spec/exel/ast_node_spec.rb +24 -3
  35. data/spec/exel/context_spec.rb +1 -0
  36. data/spec/exel/deferred_context_value_spec.rb +1 -0
  37. data/spec/exel/events_spec.rb +1 -0
  38. data/spec/exel/instruction_node_spec.rb +1 -0
  39. data/spec/exel/instruction_spec.rb +6 -0
  40. data/spec/exel/job_spec.rb +1 -0
  41. data/spec/exel/listen_instruction_spec.rb +1 -0
  42. data/spec/exel/logging/logger_wrapper_spec.rb +95 -0
  43. data/spec/exel/logging_helper_spec.rb +25 -0
  44. data/spec/exel/logging_spec.rb +36 -3
  45. data/spec/exel/middleware/chain_spec.rb +67 -0
  46. data/spec/exel/middleware/logging_spec.rb +33 -0
  47. data/spec/exel/middleware_spec.rb +69 -0
  48. data/spec/exel/null_instruction_spec.rb +1 -0
  49. data/spec/exel/processors/async_processor_spec.rb +1 -0
  50. data/spec/exel/processors/run_processor_spec.rb +1 -0
  51. data/spec/exel/processors/split_processor_spec.rb +2 -7
  52. data/spec/exel/providers/local_file_provider_spec.rb +1 -0
  53. data/spec/exel/providers/threaded_async_provider_spec.rb +1 -0
  54. data/spec/exel/sequence_node_spec.rb +1 -0
  55. data/spec/exel/value_spec.rb +1 -0
  56. data/spec/exel_spec.rb +1 -0
  57. data/spec/integration/integration_spec.rb +1 -0
  58. data/spec/spec_helper.rb +1 -0
  59. data/spec/support/integration_test_classes.rb +1 -0
  60. metadata +18 -18
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module EXEL
3
+ # Logging related helper methods for processors
4
+ module LoggingHelper
5
+ # @return [Logger] Returns the EXEL logger
6
+ def logger
7
+ EXEL.logger
8
+ end
9
+
10
+ # Logs a message with DEBUG severity
11
+ def log_debug(message)
12
+ logger.debug(message)
13
+ end
14
+
15
+ # Logs a message with INFO severity
16
+ def log_info(message)
17
+ logger.info(message)
18
+ end
19
+
20
+ # Logs a message with WARN severity
21
+ def log_warn(message)
22
+ logger.warn(message)
23
+ end
24
+
25
+ # Logs a message with ERROR severity
26
+ def log_error(message)
27
+ logger.error(message)
28
+ end
29
+
30
+ # Logs a message with FATAL severity
31
+ def log_fatal(message)
32
+ logger.fatal(message)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ module EXEL
3
+ # Middleware is code configured to run around each processor execution. Custom middleware can be added as follows:
4
+ #
5
+ # EXEL.configure do |config|
6
+ # config.middleware.add(MyMiddleware)
7
+ # config.middleware.add(AnotherMiddleware, 'constructor arg')
8
+ # end
9
+ #
10
+ # Middleware can be any class that implements a +call+ method that includes a call to +yield+:
11
+ #
12
+ # class MyMiddleware
13
+ # def call(processor, context, args)
14
+ # puts 'before process'
15
+ # yield
16
+ # puts 'after process'
17
+ # end
18
+ # end
19
+ #
20
+ # The +call+ method will be passed the class of the processor that will be executed, the current context, and any args
21
+ # that were passed to the processor in the job definition.
22
+ module Middleware
23
+ # Chain of middleware to be invoked in sequence around each processor execution.
24
+ class Chain
25
+ attr_reader :entries
26
+
27
+ Entry = Struct.new(:klass, :args)
28
+
29
+ def initialize
30
+ @entries = []
31
+ end
32
+
33
+ # Adds a middleware class to the chain. If it is already in the chain it will be removed and added to the end.
34
+ # Any additional arguments will be passed to +new+ when the middleware is created.
35
+ def add(klass, *args)
36
+ remove(klass)
37
+ @entries << Entry.new(klass, args)
38
+ end
39
+
40
+ # Removes a middleware class from the chain.
41
+ def remove(klass)
42
+ @entries.delete_if { |entry| entry.klass == klass }
43
+ end
44
+
45
+ # Returns true if the given class is in the chain.
46
+ def include?(klass)
47
+ @entries.any? { |entry| entry.klass == klass }
48
+ end
49
+
50
+ # Calls each middleware in the chain.
51
+ def invoke(*args)
52
+ chain = @entries.map { |entry| entry.klass.new(*entry.args) }
53
+
54
+ traverse_chain = lambda do
55
+ if chain.empty?
56
+ yield
57
+ else
58
+ chain.shift.call(*args, &traverse_chain)
59
+ end
60
+ end
61
+
62
+ traverse_chain.call
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module EXEL
3
+ module Middleware
4
+ # Middleware to add a prefix to all messages logged during processor execution. The prefix is specified by the
5
+ # +:log_prefix+ key in the context. Also logs start, finish, and failure of the processor execution.
6
+ class Logging
7
+ def call(processor_class, context, _args, &block)
8
+ EXEL::Logging.with_prefix("#{context[:log_prefix]}[#{processor_class}] ") { log_process(&block) }
9
+ end
10
+
11
+ private
12
+
13
+ def log_process
14
+ start_time = Time.now
15
+ EXEL.logger.info 'Starting'
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,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  # An {Instruction} that does nothing when executed
3
4
  class NullInstruction
@@ -1,6 +1,13 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  # Helper methods useful to processors
4
+ # @deprecated Most functionality replaced by {EXEL::Middleware::Logging} middleware.
3
5
  module ProcessorHelper
6
+ def self.included(other)
7
+ warn "DEPRECATION WARNING: [#{other}] EXEL::ProcessorHelper will be removed. For process logging, please use "\
8
+ 'EXEL::Middleware::Logging instead'
9
+ end
10
+
4
11
  def tag(*tags)
5
12
  tags.map { |t| "[#{t}]" }.join('')
6
13
  end
@@ -1,24 +1,18 @@
1
- require_relative '../processor_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module EXEL
4
4
  module Processors
5
5
  # Implements the +async+ instruction by using the configured async provider to run a block asynchronously.
6
6
  class AsyncProcessor
7
- include EXEL::ProcessorHelper
8
7
  attr_reader :provider
9
8
 
10
9
  def initialize(context)
11
10
  @context = context
12
11
  @provider = EXEL.async_provider.new(context)
13
-
14
- log_prefix_with '[AsyncProcessor]'
15
12
  end
16
13
 
17
14
  def process(block)
18
- log_process do
19
- @provider.do_async(block)
20
- log_info 'call to async completed'
21
- end
15
+ @provider.do_async(block)
22
16
  end
23
17
  end
24
18
  end
@@ -1,11 +1,8 @@
1
- require_relative '../processor_helper'
2
-
1
+ # frozen_string_literal: true
3
2
  module EXEL
4
3
  module Processors
5
4
  # Implements the +run+ instruction.
6
5
  class RunProcessor
7
- include EXEL::ProcessorHelper
8
-
9
6
  # Requires +context[:job]+ to contain the name of the job to be run.
10
7
  def initialize(context)
11
8
  @context = context
@@ -13,9 +10,7 @@ module EXEL
13
10
 
14
11
  # Runs the specified job with the current context
15
12
  def process(_block = nil)
16
- log_process "running job #{@context[:job]}" do
17
- EXEL::Job.run(@context[:job], @context)
18
- end
13
+ EXEL::Job.run(@context[:job], @context)
19
14
  end
20
15
  end
21
16
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
1
2
  require 'csv'
2
3
  require 'tempfile'
3
- require_relative '../processor_helper'
4
+ require_relative '../logging_helper'
4
5
 
5
6
  module EXEL
6
7
  module Processors
@@ -12,7 +13,7 @@ module EXEL
12
13
  # be deleted when splitting is complete
13
14
  # * +:chunk_size+ Set to specify the number of lines that each chunk should contain
14
15
  class SplitProcessor
15
- include EXEL::ProcessorHelper
16
+ include EXEL::LoggingHelper
16
17
 
17
18
  attr_accessor :file_name, :block
18
19
 
@@ -26,15 +27,11 @@ module EXEL
26
27
  @context = context
27
28
  @file = context[:resource]
28
29
  @context[:delete_resource] = true if @context[:delete_resource].nil?
29
-
30
- log_prefix_with '[SplitProcessor]'
31
30
  end
32
31
 
33
32
  def process(callback)
34
- log_process do
35
- process_file(callback)
36
- finish(callback)
37
- end
33
+ process_file(callback)
34
+ finish(callback)
38
35
  end
39
36
 
40
37
  def process_line(line, callback)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  module Providers
3
4
  # The default remote provider. Doesn't actually upload and download files to and from remote storage, but rather
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  module Providers
3
4
  # The default remote provider. Provides async execution by running the given EXEL block in a new Thread
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require_relative './ast_node'
2
3
 
3
4
  module EXEL
data/lib/exel/value.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  # Contains methods to handle remote and local values. Used for {Context} serialization
3
4
  module Value
data/lib/exel/version.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
- VERSION = '1.2.1'.freeze
3
+ VERSION = '1.3.0'
3
4
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe ASTNode do
3
4
  let(:context) { instance_double(EXEL::Context) }
@@ -9,13 +10,33 @@ module EXEL
9
10
  TestNode = Class.new(ASTNode)
10
11
 
11
12
  describe '#start' do
12
- context 'when an JobTermination error bubbles up' do
13
- it 'ensures the process fails silently' do
14
- node = TestNode.new(instruction)
13
+ context 'when a JobTermination error bubbles up' do
14
+ let(:node) { TestNode.new(instruction) }
15
+
16
+ before do
15
17
  allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination, 'Error')
18
+ end
19
+
20
+ it 'ensures the process fails silently' do
16
21
  expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
17
22
  expect { node.start(context) }.not_to raise_error
18
23
  end
24
+
25
+ it 'logs the error by default' do
26
+ expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
27
+ node.start(context)
28
+ end
29
+
30
+ context 'given a log instruction' do
31
+ before do
32
+ allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination.new('Error', :warn))
33
+ end
34
+
35
+ it 'logs the error with the given cmd' do
36
+ expect(EXEL.logger).to receive(:warn).with('JobTerminationError: Error')
37
+ node.start(context)
38
+ end
39
+ end
19
40
  end
20
41
  end
21
42
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe Context do
3
4
  subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe DeferredContextValue do
3
4
  subject(:deferred_value) { DeferredContextValue.new }
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe Events do
3
4
  class EventTest
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe InstructionNode do
3
4
  let(:context) { {} }
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe Instruction do
3
4
  subject(:instruction) { EXEL::Instruction.new(processor_class, args) }
@@ -14,6 +15,11 @@ module EXEL
14
15
  instruction.execute(context)
15
16
  end
16
17
 
18
+ it 'invokes the middleware chain' do
19
+ expect(EXEL.middleware).to receive(:invoke).with(processor_class, context, args)
20
+ instruction.execute(context)
21
+ end
22
+
17
23
  it 'does not pass a copy of the context' do
18
24
  allow(processor_class).to receive(:new) do |context_arg|
19
25
  expect(context_arg).to be(context)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe Job do
3
4
  describe '.define' do
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe ListenInstruction do
3
4
  subject(:instruction) { EXEL::ListenInstruction.new(:event, listener) }
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ module EXEL
3
+ module Logging
4
+ describe LoggerWrapper do
5
+ subject(:wrapper) { LoggerWrapper.new(logger) }
6
+ let(:logger) { instance_double(Logger) }
7
+
8
+ it { is_expected.to be_a(SimpleDelegator) }
9
+
10
+ LOG_LEVELS = %i(debug info warn error fatal unknown).freeze
11
+
12
+ context 'without a Logging prefix' do
13
+ LOG_LEVELS.each do |level|
14
+ describe "##{level}" do
15
+ context 'when passed a message string' do
16
+ it 'passes the message to its wrapped logger' do
17
+ expect(logger).to receive(level).with('message')
18
+ wrapper.send(level, 'message')
19
+ end
20
+ end
21
+
22
+ context 'when passed a message block' do
23
+ it 'passes the block to its wrapped logger' do
24
+ block = proc {}
25
+ expect(logger).to receive(level).with(nil, &block)
26
+
27
+ wrapper.send(level, &block)
28
+ end
29
+
30
+ context 'and a progname' do
31
+ it 'passes the block and progname to its wrapped logger' do
32
+ block = proc {}
33
+ expect(logger).to receive(level).with('test', &block)
34
+
35
+ wrapper.send(level, 'test', &block)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '#add' do
43
+ it 'passes the message to its wrapped logger' do
44
+ expect(logger).to receive(:add).with(Logger::FATAL, 'message', 'progname')
45
+ wrapper.add(Logger::FATAL, 'message', 'progname')
46
+ end
47
+ end
48
+ end
49
+
50
+ context 'with a Logging prefix' do
51
+ before { allow(Logging).to receive(:prefix).and_return('prefix: ') }
52
+
53
+ LOG_LEVELS.each do |level|
54
+ describe "##{level}" do
55
+ context 'when passed a message string' do
56
+ it 'passes the prefixed message to its wrapped logger' do
57
+ expect(logger).to receive(level).with('prefix: message')
58
+ wrapper.send(level, 'message')
59
+ end
60
+ end
61
+
62
+ context 'when passed a message block' do
63
+ it 'passes the prefixed block to its wrapped logger' do
64
+ expect(logger).to receive(level) do |progname, &block|
65
+ expect(progname).to be_nil
66
+ expect(block.call).to eq('prefix: message')
67
+ end
68
+
69
+ wrapper.send(level) { 'message' }
70
+ end
71
+
72
+ context 'and a progname' do
73
+ it 'passes the prefixed block and progname to its wrapped logger' do
74
+ expect(logger).to receive(level) do |progname, &block|
75
+ expect(progname).to eq('test')
76
+ expect(block.call).to eq('prefix: message')
77
+ end
78
+
79
+ wrapper.send(level, 'test') { 'message' }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#add' do
87
+ it 'passes the prefixed message to its wrapped logger' do
88
+ expect(logger).to receive(:add).with(Logger::FATAL, 'prefix: message', 'progname')
89
+ wrapper.add(Logger::FATAL, 'message', 'progname')
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end