contained_mr 0.1.2 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +7 -0
- data/Gemfile +7 -6
- data/Gemfile.lock +9 -5
- data/{README.rdoc → README.md} +6 -3
- data/VERSION +1 -1
- data/contained_mr.gemspec +37 -17
- data/lib/contained_mr/job.rb +32 -122
- data/lib/contained_mr/job_logic.rb +126 -0
- data/lib/contained_mr/mock/job.rb +111 -0
- data/lib/contained_mr/mock/runner.rb +86 -0
- data/lib/contained_mr/mock/template.rb +62 -0
- data/lib/contained_mr/mock.rb +3 -0
- data/lib/contained_mr/namespace.rb +24 -0
- data/lib/contained_mr/runner.rb +31 -24
- data/lib/contained_mr/runner_logic.rb +38 -0
- data/lib/contained_mr/template.rb +12 -71
- data/lib/contained_mr/template_logic.rb +91 -0
- data/lib/contained_mr.rb +10 -3
- data/test/concerns/job_state_cases.rb +75 -0
- data/test/helper.rb +1 -0
- data/test/test_cleaner.rb +6 -6
- data/test/test_contained_mr.rb +15 -0
- data/test/test_job.rb +36 -47
- data/test/test_job_logic.rb +52 -0
- data/test/test_mock_job.rb +94 -0
- data/test/test_mock_runner.rb +99 -0
- data/test/test_mock_template.rb +39 -0
- data/test/test_runner.rb +21 -6
- data/test/test_runner_logic.rb +51 -0
- data/test/test_template.rb +15 -28
- data/test/test_template_logic.rb +47 -0
- metadata +52 -21
@@ -0,0 +1,86 @@
|
|
1
|
+
# @see {ContainedMr::Runner}
|
2
|
+
class ContainedMr::Mock::Runner
|
3
|
+
# @return {Hash<String, Object>} the options passed to the constructor
|
4
|
+
attr_reader :_container_options
|
5
|
+
# @return {Hash<String, Object>} the time limit passed to the constructor
|
6
|
+
attr_reader :_time_limit
|
7
|
+
# @return {Hash<String, Object>} the output path passed to the constructor
|
8
|
+
attr_reader :_output_path
|
9
|
+
|
10
|
+
# @return {Boolean} true if {#perform} was called
|
11
|
+
def performed?
|
12
|
+
@performed
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return {Boolean} true if {#destroy!} was called
|
16
|
+
def destroyed?
|
17
|
+
@destroyed
|
18
|
+
end
|
19
|
+
|
20
|
+
include ContainedMr::RunnerLogic
|
21
|
+
|
22
|
+
# @see {ContainedMr::Runner#initialize}
|
23
|
+
def initialize(container_options, time_limit, output_path)
|
24
|
+
@_container_options = container_options
|
25
|
+
@_time_limit = time_limit
|
26
|
+
@_output_path = output_path
|
27
|
+
|
28
|
+
@container_id = nil
|
29
|
+
@started_at = @ended_at = nil
|
30
|
+
@status_code = nil
|
31
|
+
@timed_out = nil
|
32
|
+
@stdout = @stderr = nil
|
33
|
+
@output = nil
|
34
|
+
|
35
|
+
@performed = false
|
36
|
+
@destroyed = false
|
37
|
+
end
|
38
|
+
|
39
|
+
# @see {ContainedMr::Runner#perform}
|
40
|
+
def perform
|
41
|
+
@performed = true
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# @see {ContainedMr::Runner#destroy!}
|
46
|
+
def destroy!
|
47
|
+
@destroyed = true
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sets the container execution data returned by the mock.
|
52
|
+
#
|
53
|
+
# @param {Hash<Symbol, Object>} attributes values describing the result of
|
54
|
+
# running the job
|
55
|
+
# @return {ContainedMr::Runner} self
|
56
|
+
def _mock_set(attributes)
|
57
|
+
@started_at = attributes[:started_at]
|
58
|
+
@ended_at = attributes[:ended_at]
|
59
|
+
@status_code = attributes[:status_code]
|
60
|
+
@timed_out = attributes[:timed_out]
|
61
|
+
@stdout = attributes[:stdout]
|
62
|
+
@stderr = attributes[:stderr]
|
63
|
+
@output = attributes[:output]
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
# Convenience method for looking up an ulimit in the container options.
|
68
|
+
#
|
69
|
+
# @param {String} name the ulimit's name, such as 'cpu' or 'rss'
|
70
|
+
# @return {Number} the ulimit's hard and soft value, or nil if the ulimit was
|
71
|
+
# not found
|
72
|
+
# @raise {RuntimeError} if the ulimit's hard and soft values don't match
|
73
|
+
def _ulimit(name)
|
74
|
+
return nil unless ulimits = @_container_options['Ulimits']
|
75
|
+
|
76
|
+
ulimits.each do |ulimit|
|
77
|
+
if ulimit['Name'] == name
|
78
|
+
if ulimit['Hard'] != ulimit['Soft']
|
79
|
+
raise RuntimeError, "Hard/soft ulimit mismatch for #{name}"
|
80
|
+
end
|
81
|
+
return ulimit['Hard']
|
82
|
+
end
|
83
|
+
end
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# @see {ContainedMr::Template}
|
2
|
+
class ContainedMr::Mock::Template
|
3
|
+
# @return {Hash<String, Object>} YAML-parsed mapreduced.yml
|
4
|
+
attr_reader :_definition
|
5
|
+
|
6
|
+
# @return {Hash<String, Symbol|String>} maps file names in the template .zip
|
7
|
+
# to their contents, and maps directory entries to the :directory symbol
|
8
|
+
attr_reader :_zip_contents
|
9
|
+
|
10
|
+
include ContainedMr::TemplateLogic
|
11
|
+
|
12
|
+
# @return {Boolean} true if {#destroy!} was called
|
13
|
+
def destroyed?
|
14
|
+
@destroyed
|
15
|
+
end
|
16
|
+
|
17
|
+
# @see {ContainedMr::Template#initialize}
|
18
|
+
def initialize(name_prefix, id, zip_io)
|
19
|
+
@name_prefix = name_prefix
|
20
|
+
@id = id
|
21
|
+
@image_id = 'mock-template-image-id'
|
22
|
+
@item_count = nil
|
23
|
+
@_definition = nil
|
24
|
+
|
25
|
+
@destroyed = false
|
26
|
+
@_zip_contents = {}
|
27
|
+
|
28
|
+
process_zip zip_io
|
29
|
+
end
|
30
|
+
|
31
|
+
# @see {ContainedMr::Template#destroy!}
|
32
|
+
def destroy!
|
33
|
+
@destroyed = true
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# @see {ContainedMr::Template#new_job}
|
38
|
+
def job_class
|
39
|
+
ContainedMr::Mock::Job
|
40
|
+
end
|
41
|
+
|
42
|
+
# Reads the template .zip and parses the definition.
|
43
|
+
def process_zip(zip_io)
|
44
|
+
# TODO(pwnall): zip_io.read -> zip_io after rubyzip releases 1.1.8
|
45
|
+
Zip::File.open_buffer zip_io.read do |zip|
|
46
|
+
zip.each do |zip_entry|
|
47
|
+
file_name = zip_entry.name
|
48
|
+
if zip_entry.directory?
|
49
|
+
@_zip_contents[file_name] = :directory
|
50
|
+
elsif zip_entry.file?
|
51
|
+
if file_name == 'mapreduced.yml'
|
52
|
+
read_definition zip_entry.get_input_stream
|
53
|
+
next
|
54
|
+
end
|
55
|
+
@_zip_contents[file_name] = zip_entry.get_input_stream.read
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
@_zip_contents.freeze
|
60
|
+
end
|
61
|
+
private :process_zip
|
62
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Namespace and factory for templates.
|
2
|
+
module ContainedMr
|
3
|
+
# Sets up the template and builds its Docker base image.
|
4
|
+
#
|
5
|
+
# This method should be used instead of calling {ContainedMr::Template.new}
|
6
|
+
# directly. This way, tests can stub {ContainedMr.template_class} to have it
|
7
|
+
# return {ContainedMr::Mock::Template}.
|
8
|
+
#
|
9
|
+
# @param {String} name_prefix prepended to Docker objects, for identification
|
10
|
+
# purposes
|
11
|
+
# @param {String} id the template's unique identifier
|
12
|
+
# @param {String} zip_io IO implementation that produces the template .zip
|
13
|
+
def self.new_template(name_prefix, id, zip_io)
|
14
|
+
template_class.new name_prefix, id, zip_io
|
15
|
+
end
|
16
|
+
|
17
|
+
# The class instantiated by {ContainedMr.new_template}.
|
18
|
+
#
|
19
|
+
# @return {Class} by default {ContainedMr::Template}; tests should stub this
|
20
|
+
# method and have it return {ContainedMr::Mock::Template}
|
21
|
+
def self.template_class
|
22
|
+
ContainedMr::Template
|
23
|
+
end
|
24
|
+
end
|
data/lib/contained_mr/runner.rb
CHANGED
@@ -5,15 +5,20 @@ require 'docker'
|
|
5
5
|
|
6
6
|
# Handles running a single mapper or reducer.
|
7
7
|
class ContainedMr::Runner
|
8
|
-
|
9
|
-
attr_reader :started_at, :ended_at, :status_code, :timed_out
|
10
|
-
attr_reader :stdout, :stderr, :output
|
8
|
+
include ContainedMr::RunnerLogic
|
11
9
|
|
12
|
-
#
|
10
|
+
# Initialize a runner.
|
11
|
+
#
|
12
|
+
# @param {Hash<String, Object>} container_options docker container creation
|
13
|
+
# options, passed to {Docker::Container.create} without modification
|
14
|
+
# @param {Number} time_limit maximum number of seconds that the runner's
|
15
|
+
# container is allowed to execute before being terminated
|
16
|
+
# @param {String} output_path the location of the file inside the container
|
17
|
+
# whose output will be saved
|
13
18
|
def initialize(container_options, time_limit, output_path)
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
19
|
+
@_container_options = container_options
|
20
|
+
@_time_limit = time_limit
|
21
|
+
@_output_path = output_path
|
17
22
|
|
18
23
|
@container_id = nil
|
19
24
|
@started_at = @ended_at = nil
|
@@ -23,8 +28,9 @@ class ContainedMr::Runner
|
|
23
28
|
@output = nil
|
24
29
|
end
|
25
30
|
|
26
|
-
|
27
31
|
# Performs a full mapper / reducer step.
|
32
|
+
#
|
33
|
+
# @return {ContainedMr::Runner} self
|
28
34
|
def perform
|
29
35
|
container = create
|
30
36
|
@container_id = container.id
|
@@ -32,20 +38,29 @@ class ContainedMr::Runner
|
|
32
38
|
execute container
|
33
39
|
fetch_console_output container
|
34
40
|
fetch_file_output container
|
35
|
-
destroy container
|
36
|
-
self
|
41
|
+
destroy! container
|
37
42
|
end
|
38
43
|
|
39
|
-
#
|
40
|
-
|
41
|
-
|
44
|
+
# Removes the container used to run a mapper / reducer.
|
45
|
+
#
|
46
|
+
# @param {Docker::Container} container the mapper / reducer's container; if
|
47
|
+
# not supplied, an extra Docker API query is performed to obtain the
|
48
|
+
# container object
|
49
|
+
# @return {ContainedMr::Runner} self
|
50
|
+
def destroy!(container = nil)
|
51
|
+
unless @container_id.nil?
|
52
|
+
container ||= Docker::Container.get @container_id
|
53
|
+
container.delete force: true
|
54
|
+
@container_id = nil
|
55
|
+
end
|
56
|
+
self
|
42
57
|
end
|
43
58
|
|
44
59
|
# Creates a container for running a mapper or reducer.
|
45
60
|
#
|
46
61
|
# @return {Docker::Container} newly created container
|
47
62
|
def create
|
48
|
-
Docker::Container.create @
|
63
|
+
Docker::Container.create @_container_options
|
49
64
|
end
|
50
65
|
private :create
|
51
66
|
|
@@ -56,7 +71,7 @@ class ContainedMr::Runner
|
|
56
71
|
container.start
|
57
72
|
@started_at = Time.now
|
58
73
|
begin
|
59
|
-
wait_status = container.wait @
|
74
|
+
wait_status = container.wait @_time_limit
|
60
75
|
@status_code = wait_status['StatusCode']
|
61
76
|
@timed_out = false
|
62
77
|
rescue Docker::Error::TimeoutError
|
@@ -107,19 +122,11 @@ class ContainedMr::Runner
|
|
107
122
|
# @return {IO} an IO implementation that sources the .tar data
|
108
123
|
def fetch_tar_output(container)
|
109
124
|
tar_buffer = StringIO.new
|
110
|
-
container.copy @
|
125
|
+
container.copy @_output_path do |data|
|
111
126
|
tar_buffer << data
|
112
127
|
end
|
113
128
|
tar_buffer.rewind
|
114
129
|
tar_buffer
|
115
130
|
end
|
116
131
|
private :fetch_tar_output
|
117
|
-
|
118
|
-
# Removes the container used to run a mapper / reducer.
|
119
|
-
#
|
120
|
-
# @param {Docker::Container} container the mapper / reducer's container
|
121
|
-
def destroy(container)
|
122
|
-
container.delete
|
123
|
-
@container_id = nil
|
124
|
-
end
|
125
132
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Logic shared by {ContainedMr::Runner} and {ContainedMr::Mock::Runner}.
|
2
|
+
module ContainedMr::RunnerLogic
|
3
|
+
# @return {Time} the time when the mapper or reducer starts running
|
4
|
+
attr_reader :started_at
|
5
|
+
# @return {Time} the time when the mapper or reducer stops running or is killed
|
6
|
+
attr_reader :ended_at
|
7
|
+
# @return {Number} the time
|
8
|
+
attr_reader :status_code
|
9
|
+
# @return {Boolean} true if the mapper or reducer was terminated due to
|
10
|
+
# running for too long
|
11
|
+
attr_reader :timed_out
|
12
|
+
|
13
|
+
# @return {String} the data written by the mapper or reducer to stdout
|
14
|
+
attr_reader :stdout
|
15
|
+
# @return {String} the data written by the mapper or reducer to stderr
|
16
|
+
attr_reader :stderr
|
17
|
+
# @return {String} the contents of the file
|
18
|
+
attr_reader :output
|
19
|
+
|
20
|
+
# @return {String} the unique ID of the Docker container used to run the
|
21
|
+
# mapper / reducer; this is nil
|
22
|
+
attr_reader :container_id
|
23
|
+
|
24
|
+
# @return {Number} the container's running time, in seconds
|
25
|
+
def ran_for
|
26
|
+
started_at && ended_at && (ended_at - started_at)
|
27
|
+
end
|
28
|
+
|
29
|
+
# The information written to the mapper status files given to the reducer.
|
30
|
+
#
|
31
|
+
# This is saved in files named 1.json, 2.json, ... provided to the reducer.
|
32
|
+
#
|
33
|
+
# @return {Hash<Symbol, Object>} JSON-compatible representation of the
|
34
|
+
# runner's information
|
35
|
+
def json_file
|
36
|
+
{ ran_for: ran_for, exit_code: status_code, timed_out: timed_out }
|
37
|
+
end
|
38
|
+
end
|
@@ -7,20 +7,15 @@ require 'zip'
|
|
7
7
|
|
8
8
|
# A template is used to spawn multiple Map-Reduce jobs.
|
9
9
|
class ContainedMr::Template
|
10
|
-
|
10
|
+
include ContainedMr::TemplateLogic
|
11
11
|
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# @param {String} name_prefix prepended to Docker objects, for identification
|
15
|
-
# purposes
|
16
|
-
# @param {String} id the job's unique identifier
|
17
|
-
# @param {String} zip_io IO implementation that produces the template .zip
|
12
|
+
# @see {ContainedMr.new_template}
|
18
13
|
def initialize(name_prefix, id, zip_io)
|
19
14
|
@name_prefix = name_prefix
|
20
15
|
@id = id
|
21
16
|
@image_id = nil
|
22
|
-
@definition = nil
|
23
17
|
@item_count = nil
|
18
|
+
@_definition = nil
|
24
19
|
|
25
20
|
tar_buffer = StringIO.new
|
26
21
|
process_zip zip_io, tar_buffer
|
@@ -31,6 +26,8 @@ class ContainedMr::Template
|
|
31
26
|
# Tears down the template's state.
|
32
27
|
#
|
33
28
|
# This removes the template's base Docker image.
|
29
|
+
#
|
30
|
+
# @return {ContainedMr::Template} self
|
34
31
|
def destroy!
|
35
32
|
unless @image_id.nil?
|
36
33
|
# HACK(pwnall): Trick docker-api into issuing a DELETE request by tag.
|
@@ -38,63 +35,17 @@ class ContainedMr::Template
|
|
38
35
|
image.remove
|
39
36
|
@image_id = nil
|
40
37
|
end
|
38
|
+
self
|
41
39
|
end
|
42
40
|
|
43
|
-
#
|
44
|
-
#
|
45
|
-
# @return {String} the Dockerfile
|
46
|
-
def mapper_dockerfile
|
47
|
-
job_dockerfile @definition['mapper'] || {}, 'input'
|
48
|
-
end
|
49
|
-
|
50
|
-
# Computes the Dockerfile used to build a job's reducer image.
|
51
|
-
#
|
52
|
-
# @return {String} the Dockerfile
|
53
|
-
def reducer_dockerfile
|
54
|
-
job_dockerfile @definition['reducer'] || {}, '.'
|
55
|
-
end
|
56
|
-
|
57
|
-
# @return {String} tag applied to the template's base Docker image
|
58
|
-
def image_tag
|
59
|
-
"#{@name_prefix}/base.#{@id}"
|
60
|
-
end
|
61
|
-
|
62
|
-
# Computes the environment variables to be set in a mapper container.
|
41
|
+
# The class instantiated by {ContainedMr::TemplateLogic#new_job}.
|
63
42
|
#
|
64
|
-
# @
|
65
|
-
#
|
66
|
-
def
|
67
|
-
|
43
|
+
# @return {Class} by default {ContainedMr::Job}; tests might want to stub
|
44
|
+
# this method and have it return {ContainedMr::Mock::Job}
|
45
|
+
def job_class
|
46
|
+
ContainedMr::Job
|
68
47
|
end
|
69
48
|
|
70
|
-
# Computes the environment variables to be set in the reducer container.
|
71
|
-
#
|
72
|
-
# @return {Array<String>} environment variables to be set in the mapper
|
73
|
-
def reducer_env
|
74
|
-
[ "ITEMS=#{@item_count.to_s}" ]
|
75
|
-
end
|
76
|
-
|
77
|
-
# @return {String} the map output's path in the mapper Docker container
|
78
|
-
def mapper_output_path
|
79
|
-
(@definition['mapper'] || {})['output'] || '/output'
|
80
|
-
end
|
81
|
-
|
82
|
-
# @return {String} the reducer output's path in the reducer Docker container
|
83
|
-
def reducer_output_path
|
84
|
-
(@definition['reducer'] || {})['output'] || '/output'
|
85
|
-
end
|
86
|
-
|
87
|
-
# @private common code from mapper_dockerfile and reducer_dockerfile
|
88
|
-
def job_dockerfile(job_definition, input_source)
|
89
|
-
<<DOCKER_END
|
90
|
-
FROM #{@image_id}
|
91
|
-
COPY #{input_source} #{job_definition['input'] || '/input'}
|
92
|
-
WORKDIR #{job_definition['chdir'] || '/'}
|
93
|
-
ENTRYPOINT #{JSON.dump(job_definition['cmd'] || ['/bin/sh'])}
|
94
|
-
DOCKER_END
|
95
|
-
end
|
96
|
-
private :job_dockerfile
|
97
|
-
|
98
49
|
# Reads the template .zip and parses the definition.
|
99
50
|
#
|
100
51
|
# @param {IO} zip_io IO implementation that produces the .zip file
|
@@ -120,17 +71,7 @@ DOCKER_END
|
|
120
71
|
end
|
121
72
|
end
|
122
73
|
end
|
123
|
-
|
124
|
-
# Reads the template's definition, using data at the given path.
|
125
|
-
#
|
126
|
-
# @param {IO} yaml_io IO implementation that produces the .yaml file
|
127
|
-
# containing the definition
|
128
|
-
def read_definition(yaml_io)
|
129
|
-
@definition = YAML.load yaml_io.read
|
130
|
-
|
131
|
-
@item_count = @definition['items'] || 1
|
132
|
-
end
|
133
|
-
private :read_definition
|
74
|
+
private :process_zip
|
134
75
|
|
135
76
|
# Builds the template's Docker image, using data at the given path.
|
136
77
|
#
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# Logic shared by {ContainedMr::Template} and {ContainedMr::Mock::Template}.
|
2
|
+
module ContainedMr::TemplateLogic
|
3
|
+
# @return {String} prepended to Docker objects, for identification purposes
|
4
|
+
attr_reader :name_prefix
|
5
|
+
|
6
|
+
# @return {String} the template's unique identifier
|
7
|
+
attr_reader :id
|
8
|
+
|
9
|
+
# @return {Number} the number of mapper jobs specified by this template
|
10
|
+
attr_reader :item_count
|
11
|
+
|
12
|
+
# @return {String} image_id the unique ID of the Docker image used as a base
|
13
|
+
# for images built by jobs derived from this template
|
14
|
+
attr_reader :image_id
|
15
|
+
|
16
|
+
# Creates a job using this template.
|
17
|
+
#
|
18
|
+
# @param {String} id the job's unique ID
|
19
|
+
# @param {Hash<String, Object>} json_options job options, extracted from JSON
|
20
|
+
# @return {ContainedMr::Job} a newly created job that uses this template
|
21
|
+
def new_job(id, json_options)
|
22
|
+
job_class.new self, id, json_options
|
23
|
+
end
|
24
|
+
|
25
|
+
# Computes the Dockerfile used to build a job's mapper image.
|
26
|
+
#
|
27
|
+
# @return {String} the Dockerfile
|
28
|
+
def mapper_dockerfile
|
29
|
+
job_dockerfile @_definition['mapper'] || {}, 'input'
|
30
|
+
end
|
31
|
+
|
32
|
+
# Computes the Dockerfile used to build a job's reducer image.
|
33
|
+
#
|
34
|
+
# @return {String} the Dockerfile
|
35
|
+
def reducer_dockerfile
|
36
|
+
job_dockerfile @_definition['reducer'] || {}, '.'
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return {String} tag applied to the template's base Docker image
|
40
|
+
def image_tag
|
41
|
+
"#{@name_prefix}/base.#{@id}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Computes the environment variables to be set in a mapper container.
|
45
|
+
#
|
46
|
+
# @param {Number} i the mapper number
|
47
|
+
# @return {Array<String>} environment variables to be set in the mapper
|
48
|
+
def mapper_env(i)
|
49
|
+
[ "ITEM=#{i}", "ITEMS=#{@item_count.to_s}" ]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Computes the environment variables to be set in the reducer container.
|
53
|
+
#
|
54
|
+
# @return {Array<String>} environment variables to be set in the mapper
|
55
|
+
def reducer_env
|
56
|
+
[ "ITEMS=#{@item_count.to_s}" ]
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return {String} the map output's path in the mapper Docker container
|
60
|
+
def mapper_output_path
|
61
|
+
(@_definition['mapper'] || {})['output'] || '/output'
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return {String} the reducer output's path in the reducer Docker container
|
65
|
+
def reducer_output_path
|
66
|
+
(@_definition['reducer'] || {})['output'] || '/output'
|
67
|
+
end
|
68
|
+
|
69
|
+
# @private common code from mapper_dockerfile and reducer_dockerfile
|
70
|
+
def job_dockerfile(job_definition, input_source)
|
71
|
+
<<DOCKER_END
|
72
|
+
FROM #{@image_id}
|
73
|
+
COPY #{input_source} #{job_definition['input'] || '/input'}
|
74
|
+
WORKDIR #{job_definition['chdir'] || '/'}
|
75
|
+
ENTRYPOINT #{JSON.dump(job_definition['cmd'] || ['/bin/sh'])}
|
76
|
+
DOCKER_END
|
77
|
+
end
|
78
|
+
private :job_dockerfile
|
79
|
+
|
80
|
+
# Reads the template's definition, using data at the given path.
|
81
|
+
#
|
82
|
+
# @param {IO} yaml_io IO implementation that produces the .yaml file
|
83
|
+
# containing the definition
|
84
|
+
def read_definition(yaml_io)
|
85
|
+
@_definition = YAML.load yaml_io.read
|
86
|
+
@_definition.freeze
|
87
|
+
|
88
|
+
@item_count = @_definition['items'] || 1
|
89
|
+
end
|
90
|
+
private :read_definition
|
91
|
+
end
|
data/lib/contained_mr.rb
CHANGED
@@ -1,8 +1,15 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require_relative 'contained_mr/namespace.rb'
|
2
|
+
|
3
|
+
require_relative 'contained_mr/job_logic.rb'
|
4
|
+
require_relative 'contained_mr/runner_logic.rb'
|
5
|
+
require_relative 'contained_mr/template_logic.rb'
|
4
6
|
|
5
7
|
require_relative 'contained_mr/cleaner.rb'
|
6
8
|
require_relative 'contained_mr/job.rb'
|
7
9
|
require_relative 'contained_mr/runner.rb'
|
8
10
|
require_relative 'contained_mr/template.rb'
|
11
|
+
|
12
|
+
require_relative 'contained_mr/mock.rb'
|
13
|
+
require_relative 'contained_mr/mock/job.rb'
|
14
|
+
require_relative 'contained_mr/mock/runner.rb'
|
15
|
+
require_relative 'contained_mr/mock/template.rb'
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module JobStateCases
|
2
|
+
def test_build_mapper_image_twice
|
3
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
4
|
+
begin
|
5
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
6
|
+
flunk 'No exception thrown'
|
7
|
+
rescue RuntimeError => e
|
8
|
+
assert_instance_of RuntimeError, e
|
9
|
+
assert_equal 'Mapper image already exists', e.message
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_run_mapper_without_image
|
14
|
+
begin
|
15
|
+
@job.run_mapper 1
|
16
|
+
flunk 'No exception thrown'
|
17
|
+
rescue RuntimeError => e
|
18
|
+
assert_instance_of RuntimeError, e
|
19
|
+
assert_equal 'Mapper image does not exist', e.message
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_run_invalid_mapper
|
24
|
+
begin
|
25
|
+
@job.run_mapper 4
|
26
|
+
flunk 'No exception thrown'
|
27
|
+
rescue ArgumentError => e
|
28
|
+
assert_instance_of ArgumentError, e
|
29
|
+
assert_equal 'Invalid mapper number 4', e.message
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_invalid_mapper_runner
|
34
|
+
begin
|
35
|
+
@job.mapper_runner 4
|
36
|
+
flunk 'No exception thrown'
|
37
|
+
rescue ArgumentError => e
|
38
|
+
assert_instance_of ArgumentError, e
|
39
|
+
assert_equal 'Invalid mapper number 4', e.message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_build_reducer_image_without_mapper_results
|
44
|
+
begin
|
45
|
+
@job.build_reducer_image
|
46
|
+
flunk 'No exception thrown'
|
47
|
+
rescue RuntimeError => e
|
48
|
+
assert_instance_of RuntimeError, e
|
49
|
+
assert_equal 'Not all mappers ran', e.message
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_build_reducer_image_twice
|
54
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
55
|
+
1.upto(3) { |i| @job.run_mapper i }
|
56
|
+
@job.build_reducer_image
|
57
|
+
begin
|
58
|
+
@job.build_reducer_image
|
59
|
+
flunk 'No exception thrown'
|
60
|
+
rescue RuntimeError => e
|
61
|
+
assert_instance_of RuntimeError, e
|
62
|
+
assert_equal 'Reducer image already exists', e.message
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_run_reducer_without_image
|
67
|
+
begin
|
68
|
+
@job.run_reducer
|
69
|
+
flunk 'No exception thrown'
|
70
|
+
rescue RuntimeError => e
|
71
|
+
assert_instance_of RuntimeError, e
|
72
|
+
assert_equal 'Reducer image does not exist', e.message
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/test/helper.rb
CHANGED
data/test/test_cleaner.rb
CHANGED
@@ -2,10 +2,10 @@ require 'helper'
|
|
2
2
|
|
3
3
|
class TestCleaner < MiniTest::Test
|
4
4
|
def setup
|
5
|
-
@template = ContainedMr
|
5
|
+
@template = ContainedMr.new_template 'contained_mrtests', 'hello',
|
6
6
|
StringIO.new(File.binread('testdata/hello.zip'))
|
7
|
-
@job =
|
8
|
-
|
7
|
+
@job = @template.new_job 'testjob',
|
8
|
+
JSON.load(File.read('testdata/job.hello'))
|
9
9
|
@job.build_mapper_image File.read('testdata/input.hello')
|
10
10
|
|
11
11
|
@cleaner = ContainedMr::Cleaner.new 'contained_mrtests'
|
@@ -22,10 +22,10 @@ class TestCleaner < MiniTest::Test
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def test_destroy_all_with_duplicates
|
25
|
-
template2 = ContainedMr
|
25
|
+
template2 = ContainedMr.new_template 'contained_mrtests', 'hello2',
|
26
26
|
StringIO.new(File.binread('testdata/hello.zip'))
|
27
|
-
job2 =
|
28
|
-
|
27
|
+
job2 = template2.new_job 'testjob2',
|
28
|
+
JSON.load(File.read('testdata/job.hello'))
|
29
29
|
job2.build_mapper_image File.read('testdata/input.hello')
|
30
30
|
@cleaner.destroy_all!
|
31
31
|
assert_raises Docker::Error::NotFoundError do
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestContainedMr < MiniTest::Test
|
4
|
+
def test_template_class
|
5
|
+
assert_equal ContainedMr::Template, ContainedMr.template_class
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_new_template
|
9
|
+
ContainedMr.stubs(:template_class).returns ContainedMr::Mock::Template
|
10
|
+
|
11
|
+
template = ContainedMr.new_template 'contained_mrtests', 'hello',
|
12
|
+
StringIO.new(File.binread('testdata/hello.zip'))
|
13
|
+
assert_instance_of ContainedMr::Mock::Template, template
|
14
|
+
end
|
15
|
+
end
|