exel 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +10 -16
- data/Gemfile +2 -0
- data/README.md +5 -2
- data/exel.gemspec +2 -1
- data/lib/exel/ast_node.rb +2 -1
- data/lib/exel/context.rb +45 -42
- data/lib/exel/deferred_context_value.rb +35 -0
- data/lib/exel/error/job_termination.rb +3 -5
- data/lib/exel/events.rb +26 -0
- data/lib/exel/instruction.rb +2 -4
- data/lib/exel/instruction_node.rb +1 -0
- data/lib/exel/job.rb +32 -10
- data/lib/exel/listen_instruction.rb +17 -0
- data/lib/exel/null_instruction.rb +1 -0
- data/lib/exel/old_context.rb +109 -0
- data/lib/exel/processor_helper.rb +1 -4
- data/lib/exel/processors/async_processor.rb +1 -0
- data/lib/exel/processors/run_processor.rb +3 -0
- data/lib/exel/processors/split_processor.rb +12 -2
- data/lib/exel/providers/local_file_provider.rb +3 -1
- data/lib/exel/providers/threaded_async_provider.rb +1 -0
- data/lib/exel/sequence_node.rb +1 -0
- data/lib/exel/value.rb +1 -0
- data/lib/exel/version.rb +1 -1
- data/lib/exel.rb +20 -1
- data/spec/exel/ast_node_spec.rb +4 -4
- data/spec/exel/context_spec.rb +35 -109
- data/spec/exel/deferred_context_value_spec.rb +51 -7
- data/spec/exel/events_spec.rb +89 -0
- data/spec/exel/instruction_node_spec.rb +3 -3
- data/spec/exel/instruction_spec.rb +9 -9
- data/spec/exel/job_spec.rb +23 -13
- data/spec/exel/listen_instruction_spec.rb +14 -0
- data/spec/exel/logging_spec.rb +3 -3
- data/spec/exel/processors/split_processor_spec.rb +14 -6
- data/spec/exel/sequence_node_spec.rb +1 -1
- data/spec/exel_spec.rb +7 -0
- data/spec/fixtures/sample.csv +501 -0
- data/spec/integration/integration_spec.rb +51 -0
- data/spec/spec_helper.rb +17 -1
- data/spec/support/integration_test_classes.rb +44 -0
- metadata +32 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6bb6f49bf87e5bdf9dab0634d6ba7ea975335aed
|
4
|
+
data.tar.gz: 661b46455bab58722f422f341f2470ded9519853
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 359c314ec2a54d8e229738a44e0a243636c644fb451e82a013a26f04317bd3964ddefbe982781ae76abb40a3550ce34c457b1663a70f371b57e0a5183e01d195
|
7
|
+
data.tar.gz: c3a4b9799e38d4b4d8f2af6ab62141001b71377faaea6eac83caed5cbe647755f5cbd5c8a1322b1a576f4ab688ced6972c8cfbfe57bff3c39dfed8326d4c2c76
|
data/.rubocop.yml
CHANGED
data/.rubocop_todo.yml
CHANGED
@@ -1,32 +1,26 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on
|
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:
|
10
|
-
|
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/
|
22
|
-
- 'spec/
|
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:
|
30
|
-
# Configuration parameters: Exclude.
|
21
|
+
# Offense count: 1
|
31
22
|
Style/Documentation:
|
32
|
-
|
23
|
+
Exclude:
|
24
|
+
- 'spec/**/*'
|
25
|
+
- 'test/**/*'
|
26
|
+
- 'lib/exel/logging.rb'
|
data/Gemfile
CHANGED
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```
|
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
|
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
|
-
|
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
|
-
|
5
|
-
|
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
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
value
|
31
|
-
end
|
35
|
+
begin
|
36
|
+
context = Marshal.load(file.read)
|
37
|
+
rescue
|
38
|
+
# temporarily in place for backwards compatibility
|
32
39
|
|
33
|
-
|
34
|
-
@table[key] = value
|
35
|
-
end
|
40
|
+
dir = File.expand_path('..', __FILE__)
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
42
|
+
EXEL.send(:remove_const, :Context)
|
43
|
+
load File.join(dir, 'old_context.rb')
|
44
|
+
|
45
|
+
context = Context.deserialize(uri)
|
41
46
|
|
42
|
-
|
43
|
-
|
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
|
-
|
47
|
-
|
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
|
51
|
-
|
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
|
-
|
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
|
80
|
-
value.
|
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
|
-
#
|
4
|
-
#
|
5
|
-
#
|
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
|
data/lib/exel/events.rb
ADDED
@@ -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
|
data/lib/exel/instruction.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
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.
|
15
|
-
(ast = parse(dsl_code_or_name)) ? ast.start(context) :
|
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(
|
63
|
+
add_instruction_node(processor_class, parse(block), options)
|
50
64
|
end
|
51
65
|
|
52
66
|
def async(options = {}, &block)
|
53
|
-
add_instruction_node(
|
67
|
+
add_instruction_node(Processors::AsyncProcessor, parse(block), options)
|
54
68
|
end
|
55
69
|
|
56
70
|
def split(options = {}, &block)
|
57
|
-
add_instruction_node(
|
71
|
+
add_instruction_node(Processors::SplitProcessor, parse(block), options)
|
58
72
|
end
|
59
73
|
|
60
74
|
def run(options = {}, &block)
|
61
|
-
add_instruction_node(
|
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
|
71
|
-
|
72
|
-
|
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
|
@@ -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,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)
|