exel 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.rubocop_todo.yml +10 -16
  4. data/Gemfile +2 -0
  5. data/README.md +5 -2
  6. data/exel.gemspec +2 -1
  7. data/lib/exel/ast_node.rb +2 -1
  8. data/lib/exel/context.rb +45 -42
  9. data/lib/exel/deferred_context_value.rb +35 -0
  10. data/lib/exel/error/job_termination.rb +3 -5
  11. data/lib/exel/events.rb +26 -0
  12. data/lib/exel/instruction.rb +2 -4
  13. data/lib/exel/instruction_node.rb +1 -0
  14. data/lib/exel/job.rb +32 -10
  15. data/lib/exel/listen_instruction.rb +17 -0
  16. data/lib/exel/null_instruction.rb +1 -0
  17. data/lib/exel/old_context.rb +109 -0
  18. data/lib/exel/processor_helper.rb +1 -4
  19. data/lib/exel/processors/async_processor.rb +1 -0
  20. data/lib/exel/processors/run_processor.rb +3 -0
  21. data/lib/exel/processors/split_processor.rb +12 -2
  22. data/lib/exel/providers/local_file_provider.rb +3 -1
  23. data/lib/exel/providers/threaded_async_provider.rb +1 -0
  24. data/lib/exel/sequence_node.rb +1 -0
  25. data/lib/exel/value.rb +1 -0
  26. data/lib/exel/version.rb +1 -1
  27. data/lib/exel.rb +20 -1
  28. data/spec/exel/ast_node_spec.rb +4 -4
  29. data/spec/exel/context_spec.rb +35 -109
  30. data/spec/exel/deferred_context_value_spec.rb +51 -7
  31. data/spec/exel/events_spec.rb +89 -0
  32. data/spec/exel/instruction_node_spec.rb +3 -3
  33. data/spec/exel/instruction_spec.rb +9 -9
  34. data/spec/exel/job_spec.rb +23 -13
  35. data/spec/exel/listen_instruction_spec.rb +14 -0
  36. data/spec/exel/logging_spec.rb +3 -3
  37. data/spec/exel/processors/split_processor_spec.rb +14 -6
  38. data/spec/exel/sequence_node_spec.rb +1 -1
  39. data/spec/exel_spec.rb +7 -0
  40. data/spec/fixtures/sample.csv +501 -0
  41. data/spec/integration/integration_spec.rb +51 -0
  42. data/spec/spec_helper.rb +17 -1
  43. data/spec/support/integration_test_classes.rb +44 -0
  44. metadata +32 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1c9430975944c61a336b74ec445fc85836c64827
4
- data.tar.gz: c0189c97975d51869f45300c7a08d50a6f41c0f3
3
+ metadata.gz: 6bb6f49bf87e5bdf9dab0634d6ba7ea975335aed
4
+ data.tar.gz: 661b46455bab58722f422f341f2470ded9519853
5
5
  SHA512:
6
- metadata.gz: 5391eead9f34042b4e4e4fb22c5d19871b69a51371691ccea5d869a664d0a2d6f2dca408cb787eb0b1342b21a97250079e4551cd4bca8579b63012f8a39d3ae2
7
- data.tar.gz: 46d654603de7860071251878716647dfcce4199ab858cb6d852156dc79f3258d4486e5202e52b543af53141d8dfda7e4e64e553ca9636a8648c7b60c2797ae96
6
+ metadata.gz: 359c314ec2a54d8e229738a44e0a243636c644fb451e82a013a26f04317bd3964ddefbe982781ae76abb40a3550ce34c457b1663a70f371b57e0a5183e01d195
7
+ data.tar.gz: c3a4b9799e38d4b4d8f2af6ab62141001b71377faaea6eac83caed5cbe647755f5cbd5c8a1322b1a576f4ab688ced6972c8cfbfe57bff3c39dfed8326d4c2c76
data/.rubocop.yml CHANGED
@@ -5,6 +5,7 @@ require:
5
5
  inherit_from: .rubocop_todo.yml
6
6
 
7
7
  AllCops:
8
+ DisplayCopNames: true
8
9
  DisplayStyleGuide: true
9
10
 
10
11
  Metrics/LineLength:
data/.rubocop_todo.yml CHANGED
@@ -1,32 +1,26 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2015-12-13 18:06:06 -0500 using RuboCop version 0.35.1.
3
+ # on 2016-03-09 23:07:44 -0500 using RuboCop version 0.37.2.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 65
10
- # Configuration parameters: CustomTransform, IgnoredWords.
11
- RSpec/ExampleWording:
9
+ # Offense count: 5
10
+ RSpec/AnyInstance:
12
11
  Exclude:
13
- - 'spec/exel/ast_node_spec.rb'
14
- - 'spec/exel/context_spec.rb'
15
- - 'spec/exel/deferred_context_value_spec.rb'
16
- - 'spec/exel/handlers/s3_handler_spec.rb'
17
- - 'spec/exel/instruction_node_spec.rb'
18
- - 'spec/exel/instruction_spec.rb'
19
- - 'spec/exel/logging_spec.rb'
20
12
  - 'spec/exel/processors/split_processor_spec.rb'
21
- - 'spec/exel/resource_spec.rb'
22
- - 'spec/exel/sequence_node_spec.rb'
13
+ - 'spec/exel/value_spec.rb'
14
+ - 'spec/integration/integration_spec.rb'
23
15
 
24
16
  # Offense count: 1
25
17
  RSpec/InstanceVariable:
26
18
  Exclude:
27
19
  - 'spec/exel/logging_spec.rb'
28
20
 
29
- # Offense count: 18
30
- # Configuration parameters: Exclude.
21
+ # Offense count: 1
31
22
  Style/Documentation:
32
- Enabled: false
23
+ Exclude:
24
+ - 'spec/**/*'
25
+ - 'test/**/*'
26
+ - 'lib/exel/logging.rb'
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in exel.gemspec
4
4
  gemspec
5
+
6
+ gem 'codeclimate-test-reporter', group: :test, require: nil
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # EXEL
2
2
  [![Gem Version](https://badge.fury.io/rb/exel.svg)](https://badge.fury.io/rb/exel)
3
3
  [![Code Climate](https://codeclimate.com/github/47colborne/exel/badges/gpa.svg)](https://codeclimate.com/github/47colborne/exel)
4
+ [![Test Coverage](https://codeclimate.com/github/47colborne/exel/badges/coverage.svg)](https://codeclimate.com/github/47colborne/exel/coverage)
4
5
  [![Build Status](https://snap-ci.com/47colborne/exel/branch/master/build_image)](https://snap-ci.com/47colborne/exel/branch/master)
6
+ [![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/github/47colborne/exel/master)
5
7
 
6
8
  EXEL is the Elastic eXEcution Language, a simple Ruby DSL for creating processing jobs that can be run on a single machine, or scaled up to run on dozens of machines with no changes to the job itself. To run a job on more than one machine, simply install EXEL async and remote provider gems to integrate with your preferred platforms. The currently implemented providers so far are:
7
9
 
@@ -53,13 +55,14 @@ The ```Context``` class has a Hash-like interface and acts as shared storage for
53
55
  * Arguments passed to processors in the job DSL
54
56
  * Outputs assigned by processors during processing
55
57
 
56
- If you use EXEL with an async provider, such as [exel-sidekiq](https://github.com/47colborne/exel-sidekiq), and a remote provider, such as [exel-s3](https://github.com/47colborne/exel-s3), a context switch will occur when the ```async``` command is executed. Context shifts involve serializing the context and uploading it via the remote provider, then downloading and deserializing it when the async block is eventually run. This allows the processors to pass the results of their process through the sequence of processors in the job, without having to be concerned with when, where, or how those processors will be run.
58
+ If you use EXEL with an async provider, such as [exel-sidekiq](https://github.com/47colborne/exel-sidekiq), and a remote provider, such as [exel-s3](https://github.com/47colborne/exel-s3), a context switch will occur when the ```async``` instruction is executed. Context shifts involve serializing the context and uploading it via the remote provider, then downloading and deserializing it when the async block is eventually run. This allows the processors to pass the results of their process through the sequence of processors in the job, without having to be concerned with when, where, or how those processors will be run.
57
59
 
58
- ### Supported Commands
60
+ ### Supported Instructions
59
61
 
60
62
  * ```process``` Execute the given processor class (specified by the ```:with``` option), given the current context and any additional arguments provided
61
63
  * ```split``` Split the input data into 1000 line chunks and run the given block for each chunk. Assumes that the input data is a CSV formatted file referenced by ```context[:resource]```. When each block is run, ```context[:resource]``` will reference to the chunk file.
62
64
  * ```async``` Asynchronously run the given block. Uses the configured async provider to execute the block.
65
+ * ```run``` Runs the job specified by the ```:job``` option. The job will run using the current context.
63
66
 
64
67
  ### Example job
65
68
 
data/exel.gemspec CHANGED
@@ -26,7 +26,8 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency 'guard-rubocop', '~> 1'
27
27
  spec.add_development_dependency 'terminal-notifier', '~> 1'
28
28
  spec.add_development_dependency 'terminal-notifier-guard', '~> 1'
29
- spec.add_development_dependency 'rubocop', '~> 0'
29
+ spec.add_development_dependency 'rubocop', '~> 0.37.0'
30
30
  spec.add_development_dependency 'rubocop-rspec', '~> 1'
31
31
  spec.add_development_dependency 'rubocop-rspec-focused', '~> 0'
32
+ spec.add_development_dependency 'pry-byebug'
32
33
  end
data/lib/exel/ast_node.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  module EXEL
2
+ # An abstract class that serves as the parent class of nodes in the AST
2
3
  class ASTNode
3
4
  attr_reader :instruction, :children
4
5
 
@@ -12,7 +13,7 @@ module EXEL
12
13
  end
13
14
 
14
15
  def run(_context)
15
- fail "#{self.class} does not implement #process"
16
+ raise "#{self.class} does not implement #process"
16
17
  end
17
18
 
18
19
  def add_child(node)
data/lib/exel/context.rb CHANGED
@@ -1,54 +1,67 @@
1
1
  require 'tempfile'
2
2
 
3
3
  module EXEL
4
- class Context
5
- attr_reader :table
6
-
4
+ # The +Context+ is the shared memory of a running job. It acts as the source of input to processors and the place for
5
+ # them to store their outputs. It can be serialized and deserialized to support remote execution.
6
+ class Context < Hash
7
+ # Accepts an optional hash of keys and values to initialize the context with.
7
8
  def initialize(initial_context = {})
8
- @table = initial_context
9
+ super()
10
+ merge!(initial_context)
9
11
  end
10
12
 
13
+ # Returns a deep copy of this context. The copy and the original will have no shared object references.
14
+ #
15
+ # @return [Context]
11
16
  def deep_dup
12
17
  Context.deserialize(serialize)
13
18
  end
14
19
 
20
+ # Serializes this instance to a local file and uses the remote provider to upload it. Returns a URI indicating where
21
+ # the serialized context can be downloaded.
22
+ #
23
+ # @return [String] A URI such as +s3://bucket/file+, +file:///path/to/file+, etc.
15
24
  def serialize
16
25
  EXEL::Value.remotize(serialized_context)
17
26
  end
18
27
 
28
+ # Given a string representing the URI to a serialized context, downloads and returns the deserialized context
29
+ #
30
+ # @return [Context]
31
+ # rubocop:disable Metrics/MethodLength
19
32
  def self.deserialize(uri)
20
33
  file = EXEL::Value.localize(uri)
21
- context = Marshal.load(file.read)
22
- file.close
23
- context
24
- end
25
34
 
26
- def [](key)
27
- value = EXEL::Value.localize(@table[key])
28
- value = get_deferred(value)
29
- @table[key] = value
30
- value
31
- end
35
+ begin
36
+ context = Marshal.load(file.read)
37
+ rescue
38
+ # temporarily in place for backwards compatibility
32
39
 
33
- def []=(key, value)
34
- @table[key] = value
35
- end
40
+ dir = File.expand_path('..', __FILE__)
36
41
 
37
- def merge!(hash)
38
- @table.merge!(hash)
39
- self
40
- end
42
+ EXEL.send(:remove_const, :Context)
43
+ load File.join(dir, 'old_context.rb')
44
+
45
+ context = Context.deserialize(uri)
41
46
 
42
- def delete(key)
43
- @table.delete(key)
47
+ EXEL.send(:remove_const, :Context)
48
+ load File.join(dir, 'context.rb')
49
+ ensure
50
+ file.close
51
+ end
52
+
53
+ context
44
54
  end
55
+ # rubocop:enable Metrics/MethodLength
45
56
 
46
- def ==(other)
47
- other.is_a?(EXEL::Context) && table == other.table
57
+ # Returns the value referenced by the given key. If it is a remote value, it will be converted to a local value and
58
+ # the local value will be returned.
59
+ def [](key)
60
+ convert_value!(key, super(key))
48
61
  end
49
62
 
50
- def include?(values)
51
- @table.merge(values) == @table
63
+ def fetch(key)
64
+ convert_value!(key, super(key))
52
65
  end
53
66
 
54
67
  private
@@ -61,23 +74,13 @@ module EXEL
61
74
  end
62
75
 
63
76
  def remotized_table
64
- @table.each_with_object({}) { |(key, value), acc| acc[key] = EXEL::Value.remotize(value) }
65
- end
66
-
67
- def get_deferred(value)
68
- if deferred?(value)
69
- value = value.get(self)
70
- elsif value.is_a?(Array)
71
- value.map! { |v| get_deferred(v) }
72
- elsif value.is_a?(Hash)
73
- value.each { |k, v| value[k] = get_deferred(v) }
74
- end
75
-
76
- value
77
+ each_with_object({}) { |(key, value), acc| acc[key] = EXEL::Value.remotize(value) }
77
78
  end
78
79
 
79
- def deferred?(value)
80
- value.is_a?(DeferredContextValue)
80
+ def convert_value!(key, value)
81
+ value = EXEL::Value.localize(value)
82
+ value = DeferredContextValue.resolve(value, self)
83
+ self[key] = value
81
84
  end
82
85
  end
83
86
  end
@@ -1,16 +1,51 @@
1
1
  module EXEL
2
+ # When +context+ is referenced in a job definition, an instance of +DeferredContextValue+ will be put in its place.
3
+ # At runtime, the first time a +DeferredContextValue+ is read via {EXEL::Context#[]}, it will be replaced by the value
4
+ # it was referring to.
5
+ #
6
+ # Example:
7
+ # process with: MyProcessor, foo: context[:bar]
2
8
  class DeferredContextValue
3
9
  attr_reader :keys
4
10
 
11
+ class << self
12
+ # If +value+ is an instance of +DeferredContextValue+, it will be resolved to its actual value in the context. If
13
+ # it is an +Array+ or +Hash+ all +DeferredContextValue+ instances within it will be resolved. If it is anything
14
+ # else, it will just be returned.
15
+ #
16
+ # @return value, with all +DeferredContextValue+ instances resolved
17
+ def resolve(value, context)
18
+ if deferred?(value)
19
+ value = value.get(context)
20
+ elsif value.is_a?(Array)
21
+ value.map! { |v| resolve(v, context) }
22
+ elsif value.is_a?(Hash)
23
+ value.each { |k, v| value[k] = resolve(v, context) }
24
+ end
25
+
26
+ value
27
+ end
28
+
29
+ private
30
+
31
+ def deferred?(value)
32
+ value.is_a?(DeferredContextValue)
33
+ end
34
+ end
35
+
5
36
  def initialize
6
37
  @keys = []
7
38
  end
8
39
 
40
+ # Records the keys that will be used to lookup the value from the context at runtime. Supports nested hashes
41
+ # such as:
42
+ # context[:hash1][:hash2][:key]
9
43
  def [](key)
10
44
  keys << key
11
45
  self
12
46
  end
13
47
 
48
+ # Given a context, returns the value that this instance was acting as a placeholder for.
14
49
  def get(context)
15
50
  keys.reduce(context) { |a, e| a[e] }
16
51
  end
@@ -1,10 +1,8 @@
1
1
  module EXEL
2
2
  module Error
3
- # Inherit from Exception rather then StandardError
4
- # because rescue => e will only catch StandardError
5
- # and allow the Exception to propagate to the root
6
- # of the job
7
- class JobTermination < Exception
3
+ # If a processor raises a JobTermination exception, the job will immediately stop running without raising anything.
4
+ # This is useful if you want to stop a job without triggering any kind of retry mechanism, for example.
5
+ class JobTermination < Exception # Inherit from Exception so it won't be rescued and can propagate to ASTNode#start
8
6
  end
9
7
  end
10
8
  end
@@ -0,0 +1,26 @@
1
+ module EXEL
2
+ # Provides methods for registering and triggering event listeners
3
+ module Events
4
+ LISTENERS_KEY = :_listeners
5
+
6
+ def register_listener(context, event, listener)
7
+ listeners_for_event(event, context) << listener
8
+ end
9
+
10
+ def trigger(event, data = {})
11
+ listeners_for_event(event, context).each { |listener| listener.send(event, context, data) }
12
+ end
13
+
14
+ private
15
+
16
+ def listeners_for_event(event, context)
17
+ listeners(context).fetch(event)
18
+ rescue KeyError
19
+ listeners(context)[event] = []
20
+ end
21
+
22
+ def listeners(context)
23
+ context[LISTENERS_KEY] ||= Hash.new([])
24
+ end
25
+ end
26
+ end
@@ -1,9 +1,7 @@
1
1
  module EXEL
2
+ # Represents one step to be executed in the processing of a job
2
3
  class Instruction
3
- attr_reader :name
4
-
5
- def initialize(name, processor_class, args, subtree = nil)
6
- @name = name
4
+ def initialize(processor_class, args, subtree = nil)
7
5
  @processor_class = processor_class
8
6
  @args = args || {}
9
7
  @subtree = subtree
@@ -1,6 +1,7 @@
1
1
  require_relative './ast_node'
2
2
 
3
3
  module EXEL
4
+ # A leaf node in the AST that contains an instruction ({Instruction}, {ListenInstruction}) to be executed
4
5
  class InstructionNode < ASTNode
5
6
  def run(context)
6
7
  @instruction.execute(context)
data/lib/exel/job.rb CHANGED
@@ -1,18 +1,31 @@
1
1
  module EXEL
2
+ # The +Job+ module provides the main interface for defining and running EXEL jobs
2
3
  module Job
3
4
  class << self
5
+ # Registers a new job
6
+ #
7
+ # @param job_name [Symbol] A symbol to set as the name of this job. Used to run it later.
8
+ # @param block A block of code that calls the EXEL DSL methods
4
9
  def define(job_name, &block)
5
- fail "Job #{job_name.inspect} is already defined" unless registry[job_name].nil?
10
+ raise "Job #{job_name.inspect} is already defined" unless registry[job_name].nil?
6
11
  registry[job_name] = block
7
12
  end
8
13
 
14
+ # @return [Hash] A hash of all the defined jobs
9
15
  def registry
10
16
  @registry ||= {}
11
17
  end
12
18
 
19
+ # If given a symbol as the first parameter, it attempts to run a previously registered job using that name.
20
+ # Alternatively, a string of code can be passed to be parsed and run directly.
21
+ #
22
+ # @param dsl_code_or_name [String, Symbol] As a symbol, the name of a registered job. As a string, the EXEL code
23
+ # to be run.
24
+ # @param context [Context, Hash] (Optional) The initial {Context} to be passed to the job.
25
+ # @raise If no job has been registered with the given name
13
26
  def run(dsl_code_or_name, context = {})
14
- context = EXEL::Context.new(context) if context.is_a?(Hash)
15
- (ast = parse(dsl_code_or_name)) ? ast.start(context) : fail(%(Job "#{dsl_code_or_name}" not found))
27
+ 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))
16
29
  end
17
30
 
18
31
  private
@@ -27,6 +40,7 @@ module EXEL
27
40
  end
28
41
  end
29
42
 
43
+ # Defines the EXEL DSL methods and is used to convert a block of Ruby code into an abstract syntax tree (AST)
30
44
  class Parser
31
45
  attr_reader :ast
32
46
 
@@ -46,19 +60,24 @@ module EXEL
46
60
 
47
61
  def process(options, &block)
48
62
  processor_class = options.delete(:with)
49
- add_instruction_node('process', processor_class, block, options)
63
+ add_instruction_node(processor_class, parse(block), options)
50
64
  end
51
65
 
52
66
  def async(options = {}, &block)
53
- add_instruction_node('async', Processors::AsyncProcessor, block, options)
67
+ add_instruction_node(Processors::AsyncProcessor, parse(block), options)
54
68
  end
55
69
 
56
70
  def split(options = {}, &block)
57
- add_instruction_node('split', Processors::SplitProcessor, block, options)
71
+ add_instruction_node(Processors::SplitProcessor, parse(block), options)
58
72
  end
59
73
 
60
74
  def run(options = {}, &block)
61
- add_instruction_node('run', Processors::RunProcessor, block, options)
75
+ add_instruction_node(Processors::RunProcessor, parse(block), options)
76
+ end
77
+
78
+ def listen(options)
79
+ instruction = ListenInstruction.new(options.fetch(:for), options.fetch(:with))
80
+ @ast.add_child(InstructionNode.new(instruction))
62
81
  end
63
82
 
64
83
  def context
@@ -67,9 +86,12 @@ module EXEL
67
86
 
68
87
  private
69
88
 
70
- def add_instruction_node(name, processor, block, args = {})
71
- sub_tree = block.nil? ? nil : Parser.parse(block)
72
- instruction = EXEL::Instruction.new(name, processor, args, sub_tree)
89
+ def parse(block)
90
+ block.nil? ? nil : Parser.parse(block)
91
+ end
92
+
93
+ def add_instruction_node(processor, sub_tree, args = {})
94
+ instruction = EXEL::Instruction.new(processor, args, sub_tree)
73
95
  node = sub_tree.nil? ? InstructionNode.new(instruction) : InstructionNode.new(instruction, [sub_tree])
74
96
  @ast.add_child(node)
75
97
  end
@@ -0,0 +1,17 @@
1
+ require_relative 'events'
2
+
3
+ module EXEL
4
+ # Registers an event listener
5
+ class ListenInstruction
6
+ include EXEL::Events
7
+
8
+ def initialize(event, listener)
9
+ @event = event
10
+ @listener = listener
11
+ end
12
+
13
+ def execute(context)
14
+ register_listener(context, @event, @listener)
15
+ end
16
+ end
17
+ end
@@ -1,4 +1,5 @@
1
1
  module EXEL
2
+ # An {Instruction} that does nothing when executed
2
3
  class NullInstruction
3
4
  def execute(context)
4
5
  end
@@ -0,0 +1,109 @@
1
+ require 'tempfile'
2
+
3
+ module EXEL
4
+ # This is here for one version for backwards compatibility with already serialized contexts, which can't
5
+ # be deserialized with the new +Context+ class
6
+ class Context
7
+ # Internal hash of keys/values in the context. Use {#[]} and {#[]=} to get and set values instead of this.
8
+ attr_reader :table
9
+
10
+ # Accepts an optional hash of keys and values to initialize the context with.
11
+ def initialize(initial_context = {})
12
+ @table = initial_context
13
+ end
14
+
15
+ # Returns a deep copy of this context. The copy and the original will have no shared object references.
16
+ #
17
+ # @return [Context]
18
+ def deep_dup
19
+ Context.deserialize(serialize)
20
+ end
21
+
22
+ # Serializes this instance to a local file and uses the remote provider to upload it. Returns a URI indicating where
23
+ # the serialized context can be downloaded.
24
+ #
25
+ # @return [String] A URI such as +s3://bucket/file+, +file:///path/to/file+, etc.
26
+ def serialize
27
+ EXEL::Value.remotize(serialized_context)
28
+ end
29
+
30
+ # Given a string representing the URI to a serialized context, downloads and returns the deserialized context
31
+ #
32
+ # @return [Context]
33
+ def self.deserialize(uri)
34
+ file = EXEL::Value.localize(uri)
35
+ context = Marshal.load(file.read)
36
+ file.close
37
+ context
38
+ end
39
+
40
+ def foobar
41
+ puts 'hi'
42
+ end
43
+
44
+ # Returns the value referenced by the given key
45
+ def [](key)
46
+ value = EXEL::Value.localize(@table[key])
47
+ value = get_deferred(value)
48
+ @table[key] = value
49
+ value
50
+ end
51
+
52
+ # Stores the given key/value pair
53
+ def []=(key, value)
54
+ @table[key] = value
55
+ end
56
+
57
+ # Adds the given key/value pairs to the context, overriding any keys that are already present.
58
+ #
59
+ # @return [Context]
60
+ def merge!(hash)
61
+ @table.merge!(hash)
62
+ self
63
+ end
64
+
65
+ # Removes the value referenced by +key+ from the context
66
+ def delete(key)
67
+ @table.delete(key)
68
+ end
69
+
70
+ # Two Contexts are equal if they contain the same key/value pairs
71
+ def ==(other)
72
+ other.is_a?(EXEL::Context) && table == other.table
73
+ end
74
+
75
+ # Returns true if this instance contains all of the given key/value pairs
76
+ def include?(hash)
77
+ @table.merge(hash) == @table
78
+ end
79
+
80
+ private
81
+
82
+ def serialized_context
83
+ file = Tempfile.new(SecureRandom.uuid, encoding: 'ascii-8bit')
84
+ file.write(Marshal.dump(Context.new(remotized_table)))
85
+ file.rewind
86
+ file
87
+ end
88
+
89
+ def remotized_table
90
+ @table.each_with_object({}) { |(key, value), acc| acc[key] = EXEL::Value.remotize(value) }
91
+ end
92
+
93
+ def get_deferred(value)
94
+ if deferred?(value)
95
+ value = value.get(self)
96
+ elsif value.is_a?(Array)
97
+ value.map! { |v| get_deferred(v) }
98
+ elsif value.is_a?(Hash)
99
+ value.each { |k, v| value[k] = get_deferred(v) }
100
+ end
101
+
102
+ value
103
+ end
104
+
105
+ def deferred?(value)
106
+ value.is_a?(DeferredContextValue)
107
+ end
108
+ end
109
+ end
@@ -1,7 +1,6 @@
1
1
  module EXEL
2
+ # Helper methods useful to processors
2
3
  module ProcessorHelper
3
- # Helper Methods
4
-
5
4
  def tag(*tags)
6
5
  tags.map { |t| "[#{t}]" }.join('')
7
6
  end
@@ -14,8 +13,6 @@ module EXEL
14
13
  format('%.2f MB', file.size.to_f / 1_024_000)
15
14
  end
16
15
 
17
- # Logging Helpers
18
-
19
16
  def log_prefix_with(prefix)
20
17
  @log_prefix = (@context[:log_prefix] || '') + prefix
21
18
  end
@@ -2,6 +2,7 @@ require_relative '../processor_helper'
2
2
 
3
3
  module EXEL
4
4
  module Processors
5
+ # Implements the +async+ instruction by using the configured async provider to run a block asynchronously.
5
6
  class AsyncProcessor
6
7
  include EXEL::ProcessorHelper
7
8
  attr_reader :provider
@@ -2,13 +2,16 @@ require_relative '../processor_helper'
2
2
 
3
3
  module EXEL
4
4
  module Processors
5
+ # Implements the +run+ instruction.
5
6
  class RunProcessor
6
7
  include EXEL::ProcessorHelper
7
8
 
9
+ # Requires +context[:job]+ to contain the name of the job to be run.
8
10
  def initialize(context)
9
11
  @context = context
10
12
  end
11
13
 
14
+ # Runs the specified job with the current context
12
15
  def process(_block = nil)
13
16
  log_process "running job #{@context[:job]}" do
14
17
  EXEL::Job.run(@context[:job], @context)