contained_mr 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +76 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/lib/contained_mr/cleaner.rb +47 -0
- data/lib/contained_mr/job.rb +236 -0
- data/lib/contained_mr/runner.rb +125 -0
- data/lib/contained_mr/template.rb +138 -0
- data/lib/contained_mr.rb +8 -0
- data/test/helper.rb +45 -0
- data/test/test_job.rb +153 -0
- data/test/test_runner.rb +56 -0
- data/test/test_template.rb +50 -0
- data/testdata/Dockerfile.hello.mapper +4 -0
- data/testdata/Dockerfile.hello.reducer +4 -0
- data/testdata/hello/Dockerfile +4 -0
- data/testdata/hello/data/goodbye.txt +1 -0
- data/testdata/hello/data/hello.txt +1 -0
- data/testdata/hello/mapper.sh +9 -0
- data/testdata/hello/mapreduced.yml +18 -0
- data/testdata/hello/reducer.sh +10 -0
- data/testdata/input.hello +1 -0
- data/testdata/job.hello +10 -0
- metadata +183 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 92996839ff48bb11fd7dd367fe661163a7b33c25
|
4
|
+
data.tar.gz: f44046ae3b61c5c10fc9dfbe0311a48b725e26fd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b570c230d4c6c5a2a74bffbda9e88667bfe8f2a420e6b992b8bc4368d94e4fecc12b5541b6090d5dba5e6376cf1abffb342ed0dd0da1ef254fe5f3ee97f261d0
|
7
|
+
data.tar.gz: c296cc24c02982f2018f968f6338db78af78a32c9140a0c885c2ca2f1fd711a62b8c39166539021ffee870de044a26b45aa3337967b8f371bcb23af5200a92cb
|
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'docker-api', '>= 1.22.3', require: 'docker'
|
4
|
+
gem 'rubyzip', '>= 1.1.7', require: 'zip'
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "minitest", ">= 0"
|
10
|
+
gem "yard", ">= 0.7"
|
11
|
+
gem "rdoc", ">= 3.12"
|
12
|
+
gem "bundler", ">= 1.6.1"
|
13
|
+
gem "jeweler", ">= 2.0.1"
|
14
|
+
gem "simplecov", ">= 0"
|
15
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.3.8)
|
5
|
+
builder (3.2.2)
|
6
|
+
descendants_tracker (0.0.4)
|
7
|
+
thread_safe (~> 0.3, >= 0.3.1)
|
8
|
+
docile (1.1.5)
|
9
|
+
docker-api (1.22.3)
|
10
|
+
excon (>= 0.38.0)
|
11
|
+
json
|
12
|
+
excon (0.45.4)
|
13
|
+
faraday (0.9.1)
|
14
|
+
multipart-post (>= 1.2, < 3)
|
15
|
+
git (1.2.9.1)
|
16
|
+
github_api (0.12.4)
|
17
|
+
addressable (~> 2.3)
|
18
|
+
descendants_tracker (~> 0.0.4)
|
19
|
+
faraday (~> 0.8, < 0.10)
|
20
|
+
hashie (>= 3.4)
|
21
|
+
multi_json (>= 1.7.5, < 2.0)
|
22
|
+
nokogiri (~> 1.6.6)
|
23
|
+
oauth2
|
24
|
+
hashie (3.4.2)
|
25
|
+
highline (1.7.3)
|
26
|
+
jeweler (2.0.1)
|
27
|
+
builder
|
28
|
+
bundler (>= 1.0)
|
29
|
+
git (>= 1.2.5)
|
30
|
+
github_api
|
31
|
+
highline (>= 1.6.15)
|
32
|
+
nokogiri (>= 1.5.10)
|
33
|
+
rake
|
34
|
+
rdoc
|
35
|
+
json (1.8.3)
|
36
|
+
jwt (1.5.1)
|
37
|
+
mini_portile (0.6.2)
|
38
|
+
minitest (5.8.0)
|
39
|
+
multi_json (1.11.2)
|
40
|
+
multi_xml (0.5.5)
|
41
|
+
multipart-post (2.0.0)
|
42
|
+
nokogiri (1.6.6.2)
|
43
|
+
mini_portile (~> 0.6.0)
|
44
|
+
oauth2 (1.0.0)
|
45
|
+
faraday (>= 0.8, < 0.10)
|
46
|
+
jwt (~> 1.0)
|
47
|
+
multi_json (~> 1.3)
|
48
|
+
multi_xml (~> 0.5)
|
49
|
+
rack (~> 1.2)
|
50
|
+
rack (1.6.4)
|
51
|
+
rake (10.4.2)
|
52
|
+
rdoc (4.2.0)
|
53
|
+
rubyzip (1.1.7)
|
54
|
+
simplecov (0.10.0)
|
55
|
+
docile (~> 1.1.0)
|
56
|
+
json (~> 1.8)
|
57
|
+
simplecov-html (~> 0.10.0)
|
58
|
+
simplecov-html (0.10.0)
|
59
|
+
thread_safe (0.3.5)
|
60
|
+
yard (0.8.7.6)
|
61
|
+
|
62
|
+
PLATFORMS
|
63
|
+
ruby
|
64
|
+
|
65
|
+
DEPENDENCIES
|
66
|
+
bundler (>= 1.6.1)
|
67
|
+
docker-api (>= 1.22.3)
|
68
|
+
jeweler (>= 2.0.1)
|
69
|
+
minitest
|
70
|
+
rdoc (>= 3.12)
|
71
|
+
rubyzip (>= 1.1.7)
|
72
|
+
simplecov
|
73
|
+
yard (>= 0.7)
|
74
|
+
|
75
|
+
BUNDLED WITH
|
76
|
+
1.10.6
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2015 Victor Costan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= contained_mr
|
2
|
+
|
3
|
+
Map-Reduce where both the mappers and the reducer run inside Docker containers.
|
4
|
+
|
5
|
+
== Contributing to contained_mr
|
6
|
+
|
7
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
8
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
9
|
+
* Fork the project.
|
10
|
+
* Start a feature/bugfix branch.
|
11
|
+
* Commit and push until you are happy with your contribution.
|
12
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
13
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2015 Victor Costan. See LICENSE.txt for further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
17
|
+
gem.name = "contained_mr"
|
18
|
+
gem.homepage = "http://github.com/pwnall/contained_mr"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Map-Reduce with Docker containers}
|
21
|
+
gem.description = %Q{Plumbing for running mappers and reducers inside Docker containers}
|
22
|
+
gem.email = "victor@costan.us"
|
23
|
+
gem.authors = ["Victor Costan"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
Rake::TestTask.new(:test) do |test|
|
30
|
+
test.libs << 'lib' << 'test'
|
31
|
+
test.pattern = 'test/**/test_*.rb'
|
32
|
+
test.verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Code coverage detail"
|
36
|
+
task :simplecov do
|
37
|
+
ENV['COVERAGE'] = "true"
|
38
|
+
Rake::Task['test'].execute
|
39
|
+
end
|
40
|
+
|
41
|
+
task :default => :test
|
42
|
+
|
43
|
+
require 'yard'
|
44
|
+
YARD::Rake::YardocTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# Cleans up left over Docker images and containers.
|
4
|
+
class ContainedMr::Cleaner
|
5
|
+
attr_reader :name_prefix
|
6
|
+
|
7
|
+
# Sets up a cleaner.
|
8
|
+
#
|
9
|
+
# @param {String} name_prefix should match the value given to Template
|
10
|
+
# instances
|
11
|
+
def initialize(name_prefix)
|
12
|
+
@name_prefix = name_prefix
|
13
|
+
@label_value = name_prefix
|
14
|
+
end
|
15
|
+
|
16
|
+
# Removes all images and containers matching this cleaner's name prefix.
|
17
|
+
def destroy_all!
|
18
|
+
destroy_all_containers!
|
19
|
+
destroy_all_images!
|
20
|
+
end
|
21
|
+
|
22
|
+
def destroy_all_containers!
|
23
|
+
containers = Docker::Container.all all: true,
|
24
|
+
filters: container_filters.to_json
|
25
|
+
containers.each do |container|
|
26
|
+
container.delete force: false, volumes: true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
private :destroy_all_containers!
|
30
|
+
|
31
|
+
def destroy_all_images!
|
32
|
+
tag_prefix = "#{@name_prefix}/"
|
33
|
+
images = Docker::Image.all
|
34
|
+
images.each do |image|
|
35
|
+
image_tags = image.info['RepoTags'] || []
|
36
|
+
next unless image_tags.any? { |tag| tag.start_with? tag_prefix }
|
37
|
+
image.delete
|
38
|
+
end
|
39
|
+
end
|
40
|
+
private :destroy_all_images!
|
41
|
+
|
42
|
+
# @return { Hash<Symbol, Array<String>> } filters used to identify Docker
|
43
|
+
# containers started by this controller
|
44
|
+
def container_filters
|
45
|
+
{ label: [ "contained_mr.ctl=#{@label_value}" ] }
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'rubygems' # For tar operations.
|
3
|
+
require 'stringio'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'docker'
|
7
|
+
|
8
|
+
# A map-reduce job.
|
9
|
+
class ContainedMr::Job
|
10
|
+
attr_reader :id, :item_count, :mapper_image_id, :reducer_image_id
|
11
|
+
|
12
|
+
# Sets up the job.
|
13
|
+
#
|
14
|
+
# @param {Template} template data used to spawn this job
|
15
|
+
# @param {String} id the job's unique ID
|
16
|
+
# @param {Hash<String, Object>} json_options job options, extracted from JSON
|
17
|
+
def initialize(template, id, json_options)
|
18
|
+
@id = id
|
19
|
+
@template = template
|
20
|
+
@name_prefix = template.name_prefix
|
21
|
+
@item_count = template.item_count
|
22
|
+
|
23
|
+
@mapper_image_id = nil
|
24
|
+
@reducer_image_id = nil
|
25
|
+
|
26
|
+
@mappers = Array.new @item_count
|
27
|
+
@reducer = nil
|
28
|
+
@mapper_options = nil
|
29
|
+
@reducer_options = nil
|
30
|
+
parse_options json_options
|
31
|
+
end
|
32
|
+
|
33
|
+
# Tears down the job's state.
|
34
|
+
#
|
35
|
+
# This removes the job's containers, as well as the mapper and reducer Docker
|
36
|
+
# images, if they still exist.
|
37
|
+
def destroy!
|
38
|
+
@mappers.each do |runner|
|
39
|
+
next if runner.nil? or runner.container_id.nil?
|
40
|
+
container = Docker::Container.get runner.container_id
|
41
|
+
container.delete force: true
|
42
|
+
end
|
43
|
+
|
44
|
+
unless @reducer.nil? or @reducer.container_id.nil?
|
45
|
+
container = Docker::Container.get @reducer.container_id
|
46
|
+
container.delete force: true
|
47
|
+
end
|
48
|
+
|
49
|
+
unless @mapper_image_id.nil?
|
50
|
+
image = Docker::Image.get @mapper_image_id
|
51
|
+
image.remove
|
52
|
+
@mapper_image_id = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
unless @reducer_image_id.nil?
|
56
|
+
image = Docker::Image.get @reducer_image_id
|
57
|
+
image.remove
|
58
|
+
@reducer_image_id = nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the runner used for a mapper.
|
63
|
+
#
|
64
|
+
# @param {Number} i the mapper number
|
65
|
+
# @return {ContainedMr::Runner} the runner used for the given mapper; nil if
|
66
|
+
# the given mapper was not started
|
67
|
+
def mapper_runner(i)
|
68
|
+
@mappers[i - 1]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the runner used for the reducer.
|
72
|
+
#
|
73
|
+
# @return {ContainedMr::Runner} the runner used for reducer; nil if the
|
74
|
+
# reducer was not started
|
75
|
+
def reducer_runner
|
76
|
+
@reducer
|
77
|
+
end
|
78
|
+
|
79
|
+
# Builds the Docker image used to run this job's mappers.
|
80
|
+
#
|
81
|
+
# @param {String} mapper_input data passed to the mappers
|
82
|
+
# @return {String} the newly built Docker image's ID
|
83
|
+
def build_mapper_image(mapper_input)
|
84
|
+
tar_io = mapper_tar_context mapper_input
|
85
|
+
image = Docker::Image.build_from_tar tar_io, t: mapper_image_tag
|
86
|
+
@mapper_image_id = image.id
|
87
|
+
end
|
88
|
+
|
89
|
+
# Builds the Docker image used to run this job's reducer.
|
90
|
+
#
|
91
|
+
# @return {String} the newly built Docker image's ID
|
92
|
+
def build_reducer_image
|
93
|
+
tar_io = reducer_tar_context
|
94
|
+
image = Docker::Image.build_from_tar tar_io, t: reducer_image_tag
|
95
|
+
@reducer_image_id = image.id
|
96
|
+
end
|
97
|
+
|
98
|
+
# Runs one of the job's mappers.
|
99
|
+
#
|
100
|
+
# @param {Number} i the mapper to run
|
101
|
+
# @return {ContainedMr::Runner} the runner used by the mapper
|
102
|
+
def run_mapper(i)
|
103
|
+
mapper = ContainedMr::Runner.new mapper_container_options(i),
|
104
|
+
@mapper_options[:wait_time], @template.mapper_output_path
|
105
|
+
@mappers[i - 1] = mapper
|
106
|
+
mapper.perform
|
107
|
+
end
|
108
|
+
|
109
|
+
# Runs one the job's reducer.
|
110
|
+
#
|
111
|
+
# @return {ContainedMr::Runner} the runner used by the reducer
|
112
|
+
def run_reducer
|
113
|
+
reducer = ContainedMr::Runner.new reducer_container_options,
|
114
|
+
@reducer_options[:wait_time], @template.reducer_output_path
|
115
|
+
@reducer = reducer
|
116
|
+
@reducer.perform
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return {String} tag applied to the Docker image used by the job's mappers
|
120
|
+
def mapper_image_tag
|
121
|
+
"#{@name_prefix}/mapper.#{@id}"
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return {String} tag applied to the Docker image used by the job's reducers
|
125
|
+
def reducer_image_tag
|
126
|
+
"#{@name_prefix}/reducer.#{@id}"
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return {Hash<String, Object>} params used to create a mapper container
|
130
|
+
def mapper_container_options(i)
|
131
|
+
ulimits = @mapper_options[:ulimits].map do |k, v|
|
132
|
+
{ "Name" => k.to_s, "Soft" => v, "Hard" => v }
|
133
|
+
end
|
134
|
+
|
135
|
+
{
|
136
|
+
'name' => "#{@name_prefix}_mapper.#{@id}.#{i}",
|
137
|
+
'Image' => @mapper_image_id,
|
138
|
+
'Hostname' => "#{i}.mapper", 'Domainname' => '',
|
139
|
+
'Labels' => { 'contained_mr.ctl' => @name_prefix },
|
140
|
+
'Env' => @template.mapper_env(i), 'Ulimits' => ulimits,
|
141
|
+
'NetworkDisabled' => true, 'ExposedPorts' => {},
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
# @return {Hash<String, Object>} params used to create a reducer container
|
146
|
+
def reducer_container_options
|
147
|
+
ulimits = @reducer_options[:ulimits].map do |k, v|
|
148
|
+
{ "Name" => k.to_s, "Soft" => v, "Hard" => v }
|
149
|
+
end
|
150
|
+
|
151
|
+
{
|
152
|
+
'name' => "#{@name_prefix}_reducer.#{@id}",
|
153
|
+
'Image' => @reducer_image_id,
|
154
|
+
'Hostname' => 'reducer', 'Domainname' => '',
|
155
|
+
'Labels' => { 'contained_mr.ctl' => @name_prefix },
|
156
|
+
'Env' => @template.reducer_env, 'Ulimits' => ulimits,
|
157
|
+
'NetworkDisabled' => true, 'ExposedPorts' => {},
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
# Reads in JSON options and sets defaults.
|
162
|
+
def parse_options(json_options)
|
163
|
+
mapper = json_options['mapper'] || {}
|
164
|
+
mapper_ulimits = mapper['ulimits'] || {}
|
165
|
+
@mapper_options = {
|
166
|
+
wait_time: mapper['wait_time'] || 60,
|
167
|
+
ulimits: {
|
168
|
+
cpu: mapper_ulimits['cpu'] || 60, # seconds
|
169
|
+
rss: mapper_ulimits['rss'] || 500_000, # pages
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
reducer = json_options['reducer'] || {}
|
174
|
+
reducer_ulimits = reducer['ulimits'] || {}
|
175
|
+
@reducer_options = {
|
176
|
+
wait_time: reducer['wait_time'] || 60,
|
177
|
+
ulimits: {
|
178
|
+
cpu: reducer_ulimits['cpu'] || 60,
|
179
|
+
rss: reducer_ulimits['rss'] || 500_000,
|
180
|
+
}
|
181
|
+
}
|
182
|
+
end
|
183
|
+
private :parse_options
|
184
|
+
|
185
|
+
# Builds the .tar context used to create the mapper's Docker image.
|
186
|
+
#
|
187
|
+
# @param {String} mapper_input data passed to the mappers
|
188
|
+
# @return {IO} an IO implementation that sources the .tar data
|
189
|
+
def mapper_tar_context(mapper_input)
|
190
|
+
tar_buffer = StringIO.new
|
191
|
+
Gem::Package::TarWriter.new tar_buffer do |tar|
|
192
|
+
tar.add_file 'Dockerfile', 0644 do |docker_io|
|
193
|
+
docker_io.write @template.mapper_dockerfile
|
194
|
+
end
|
195
|
+
tar.add_file 'input', 0644 do |input_io|
|
196
|
+
input_io.write mapper_input
|
197
|
+
end
|
198
|
+
end
|
199
|
+
tar_buffer.rewind
|
200
|
+
tar_buffer
|
201
|
+
end
|
202
|
+
private :mapper_tar_context
|
203
|
+
|
204
|
+
# Builds the .tar context used to create the mapper's Docker image.
|
205
|
+
#
|
206
|
+
# @return {IO} an IO implementation that sources the .tar file
|
207
|
+
def reducer_tar_context
|
208
|
+
tar_buffer = StringIO.new
|
209
|
+
Gem::Package::TarWriter.new tar_buffer do |tar|
|
210
|
+
tar.add_file 'Dockerfile', 0644 do |docker_io|
|
211
|
+
docker_io.write @template.reducer_dockerfile
|
212
|
+
end
|
213
|
+
@mappers.each_with_index do |mapper, index|
|
214
|
+
i = index + 1
|
215
|
+
|
216
|
+
if mapper.output
|
217
|
+
tar.add_file "#{i}.out", 0644 do |io|
|
218
|
+
io.write mapper.output
|
219
|
+
end
|
220
|
+
end
|
221
|
+
tar.add_file("#{i}.stdout", 0644) { |io| io.write mapper.stdout }
|
222
|
+
tar.add_file("#{i}.stderr", 0644) { |io| io.write mapper.stderr }
|
223
|
+
|
224
|
+
status = {
|
225
|
+
ran_for: mapper.ran_for,
|
226
|
+
exit_code: mapper.status_code,
|
227
|
+
timed_out: mapper.timed_out,
|
228
|
+
}
|
229
|
+
tar.add_file("#{i}.json", 0644) { |io| io.write status.to_json }
|
230
|
+
end
|
231
|
+
end
|
232
|
+
tar_buffer.rewind
|
233
|
+
tar_buffer
|
234
|
+
end
|
235
|
+
private :mapper_tar_context
|
236
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'rubygems' # For tar operations.
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
require 'docker'
|
5
|
+
|
6
|
+
# Handles running a single mapper or reducer.
|
7
|
+
class ContainedMr::Runner
|
8
|
+
attr_reader :container_id
|
9
|
+
attr_reader :started_at, :ended_at, :status_code, :timed_out
|
10
|
+
attr_reader :stdout, :stderr, :output
|
11
|
+
|
12
|
+
# C
|
13
|
+
def initialize(container_options, time_limit, output_path)
|
14
|
+
@container_options = container_options
|
15
|
+
@time_limit = time_limit
|
16
|
+
@output_path = output_path
|
17
|
+
|
18
|
+
@container_id = nil
|
19
|
+
@started_at = @ended_at = nil
|
20
|
+
@status_code = nil
|
21
|
+
@timed_out = nil
|
22
|
+
@stdout = @stderr = nil
|
23
|
+
@output = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# Performs a full mapper / reducer step.
|
28
|
+
def perform
|
29
|
+
container = create
|
30
|
+
@container_id = container.id
|
31
|
+
|
32
|
+
execute container
|
33
|
+
fetch_console_output container
|
34
|
+
fetch_file_output container
|
35
|
+
destroy container
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return {Number} the container's running time, in seconds
|
40
|
+
def ran_for
|
41
|
+
@started_at && @ended_at && (@ended_at - @started_at)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Creates a container for running a mapper or reducer.
|
45
|
+
#
|
46
|
+
# @return {Docker::Container} newly created container
|
47
|
+
def create
|
48
|
+
Docker::Container.create @container_options
|
49
|
+
end
|
50
|
+
private :create
|
51
|
+
|
52
|
+
# Runs the process inside the container, kills it if takes too long.
|
53
|
+
#
|
54
|
+
# @param {Docker::Container} container the container that holds the process
|
55
|
+
def execute(container)
|
56
|
+
container.start
|
57
|
+
@started_at = Time.now
|
58
|
+
begin
|
59
|
+
wait_status = container.wait @time_limit
|
60
|
+
@status_code = wait_status['StatusCode']
|
61
|
+
@timed_out = false
|
62
|
+
rescue Docker::Error::TimeoutError
|
63
|
+
@status_code = false
|
64
|
+
@timed_out = true
|
65
|
+
container.kill
|
66
|
+
end
|
67
|
+
@ended_at = Time.now
|
68
|
+
end
|
69
|
+
private :execute
|
70
|
+
|
71
|
+
# Extracts console output from a container.
|
72
|
+
#
|
73
|
+
# @param {Docker::Container} container the mapper / reducer's container
|
74
|
+
def fetch_console_output(container)
|
75
|
+
messages = container.attach stream: false, logs: true, stdin: nil,
|
76
|
+
stdout: true, stderr: true
|
77
|
+
@stdout = messages[0].join ''
|
78
|
+
@stderr = messages[1].join ''
|
79
|
+
end
|
80
|
+
private :fetch_console_output
|
81
|
+
|
82
|
+
# Extracts the mapper / reducer's output file from a container.
|
83
|
+
#
|
84
|
+
# @param {Docker::Container} container the mapper / reducer's container
|
85
|
+
def fetch_file_output(container)
|
86
|
+
begin
|
87
|
+
tar_buffer = fetch_tar_output container
|
88
|
+
rescue Docker::Error::ServerError
|
89
|
+
@output = false
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
Gem::Package::TarReader.new tar_buffer do |tar|
|
94
|
+
tar.each do |entry|
|
95
|
+
next unless entry.file?
|
96
|
+
@output = entry.read
|
97
|
+
return
|
98
|
+
end
|
99
|
+
end
|
100
|
+
@output = false
|
101
|
+
end
|
102
|
+
private :fetch_file_output
|
103
|
+
|
104
|
+
# Extracts the mapper / reducer's output, as a .tar, from a container.
|
105
|
+
#
|
106
|
+
# @param {Docker::Container} container the mapper / reducer's container
|
107
|
+
# @return {IO} an IO implementation that sources the .tar data
|
108
|
+
def fetch_tar_output(container)
|
109
|
+
tar_buffer = StringIO.new
|
110
|
+
container.copy @output_path do |data|
|
111
|
+
tar_buffer << data
|
112
|
+
end
|
113
|
+
tar_buffer.rewind
|
114
|
+
tar_buffer
|
115
|
+
end
|
116
|
+
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
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'rubygems' # For tar operations.
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require 'docker'
|
6
|
+
require 'zip'
|
7
|
+
|
8
|
+
# A template is used to spawn multiple Map-Reduce jobs.
|
9
|
+
class ContainedMr::Template
|
10
|
+
attr_reader :name_prefix, :item_count, :image_id
|
11
|
+
|
12
|
+
# Sets up the template and builds its Docker base image.
|
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
|
18
|
+
def initialize(name_prefix, id, zip_io)
|
19
|
+
@name_prefix = name_prefix
|
20
|
+
@id = id
|
21
|
+
@image_id = nil
|
22
|
+
@definition = nil
|
23
|
+
@item_count = nil
|
24
|
+
|
25
|
+
tar_buffer = StringIO.new
|
26
|
+
process_zip zip_io, tar_buffer
|
27
|
+
tar_buffer.rewind
|
28
|
+
build_image tar_buffer
|
29
|
+
end
|
30
|
+
|
31
|
+
# Tears down the template's state.
|
32
|
+
#
|
33
|
+
# This removes the template's base Docker image.
|
34
|
+
def destroy!
|
35
|
+
unless @image_id.nil?
|
36
|
+
image = Docker::Image.get @image_id
|
37
|
+
image.remove
|
38
|
+
@image_id = nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Computes the Dockerfile used to build a job's mapper image.
|
43
|
+
#
|
44
|
+
# @return {String} the Dockerfile
|
45
|
+
def mapper_dockerfile
|
46
|
+
job_dockerfile @definition['mapper'] || {}, 'input'
|
47
|
+
end
|
48
|
+
|
49
|
+
# Computes the Dockerfile used to build a job's reducer image.
|
50
|
+
#
|
51
|
+
# @return {String} the Dockerfile
|
52
|
+
def reducer_dockerfile
|
53
|
+
job_dockerfile @definition['reducer'] || {}, '.'
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return {String} tag applied to the template's base Docker image
|
57
|
+
def image_tag
|
58
|
+
"#{@name_prefix}/base.#{@id}"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Computes the environment variables to be set in a mapper container.
|
62
|
+
#
|
63
|
+
# @param {Number} i the mapper number
|
64
|
+
# @return {Array<String>} environment variables to be set in the mapper
|
65
|
+
def mapper_env(i)
|
66
|
+
[ "ITEM=#{i}", "ITEMS=#{@item_count.to_s}" ]
|
67
|
+
end
|
68
|
+
|
69
|
+
# Computes the environment variables to be set in the reducer container.
|
70
|
+
#
|
71
|
+
# @return {Array<String>} environment variables to be set in the mapper
|
72
|
+
def reducer_env
|
73
|
+
[ "ITEMS=#{@item_count.to_s}" ]
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return {String} the map output's path in the mapper Docker container
|
77
|
+
def mapper_output_path
|
78
|
+
(@definition['mapper'] || {})['output'] || '/output'
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return {String} the reducer output's path in the reducer Docker container
|
82
|
+
def reducer_output_path
|
83
|
+
(@definition['reducer'] || {})['output'] || '/output'
|
84
|
+
end
|
85
|
+
|
86
|
+
# @private common code from mapper_dockerfile and reducer_dockerfile
|
87
|
+
def job_dockerfile(job_definition, input_source)
|
88
|
+
<<DOCKER_END
|
89
|
+
FROM #{@image_id}
|
90
|
+
COPY #{input_source} #{job_definition['input'] || '/input'}
|
91
|
+
WORKDIR #{job_definition['chdir'] || '/'}
|
92
|
+
ENTRYPOINT #{JSON.dump(job_definition['cmd'] || ['/bin/sh'])}
|
93
|
+
DOCKER_END
|
94
|
+
end
|
95
|
+
private :job_dockerfile
|
96
|
+
|
97
|
+
# Reads the template .zip and parses the definition.
|
98
|
+
#
|
99
|
+
# @param {IO} zip_io IO implementation that produces the .zip file
|
100
|
+
# @param {IO} tar_io IO implementation that will receive the .tar file
|
101
|
+
def process_zip(zip_io, tar_io)
|
102
|
+
Gem::Package::TarWriter.new tar_io do |tar|
|
103
|
+
# TODO(pwnall): zip_io.read -> zip_io after rubyzip releases 1.1.8
|
104
|
+
Zip::File.open_buffer zip_io.read do |zip|
|
105
|
+
zip.each do |zip_entry|
|
106
|
+
file_name = zip_entry.name
|
107
|
+
if file_name == 'mapreduced.yml'
|
108
|
+
read_definition zip_entry.get_input_stream
|
109
|
+
next
|
110
|
+
end
|
111
|
+
tar.add_file file_name, 0644 do |tar_file_io|
|
112
|
+
IO.copy_stream zip_entry.get_input_stream, tar_file_io
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Reads the template's definition, using data at the given path.
|
120
|
+
#
|
121
|
+
# @param {IO} yaml_io IO implementation that produces the .yaml file
|
122
|
+
# containing the definition
|
123
|
+
def read_definition(yaml_io)
|
124
|
+
@definition = YAML.load yaml_io.read
|
125
|
+
|
126
|
+
@item_count = @definition['items'] || 1
|
127
|
+
end
|
128
|
+
private :read_definition
|
129
|
+
|
130
|
+
# Builds the template's Docker image, using data at the given path.
|
131
|
+
#
|
132
|
+
# @param {IO} tar_io IO implementation that produces the image's .tar file
|
133
|
+
def build_image(tar_io)
|
134
|
+
image = Docker::Image.build_from_tar tar_io, t: image_tag
|
135
|
+
@image_id = image.id
|
136
|
+
end
|
137
|
+
private :build_image
|
138
|
+
end
|
data/lib/contained_mr.rb
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
module SimpleCov::Configuration
|
4
|
+
def clean_filters
|
5
|
+
@filters = []
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
SimpleCov.configure do
|
10
|
+
clean_filters
|
11
|
+
load_profile 'test_frameworks'
|
12
|
+
end
|
13
|
+
|
14
|
+
ENV["COVERAGE"] && SimpleCov.start do
|
15
|
+
add_filter "/.rvm/"
|
16
|
+
end
|
17
|
+
require 'rubygems'
|
18
|
+
require 'bundler'
|
19
|
+
begin
|
20
|
+
Bundler.setup(:default, :development)
|
21
|
+
rescue Bundler::BundlerError => e
|
22
|
+
$stderr.puts e.message
|
23
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
24
|
+
exit e.status_code
|
25
|
+
end
|
26
|
+
require 'minitest/autorun'
|
27
|
+
|
28
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
29
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
30
|
+
require 'contained_mr'
|
31
|
+
|
32
|
+
class MiniTest::Test
|
33
|
+
end
|
34
|
+
|
35
|
+
File.unlink 'testdata/hello.zip' if File.file?('testdata/hello.zip')
|
36
|
+
Zip::File.open('testdata/hello.zip', Zip::File::CREATE) do |zip|
|
37
|
+
files = Dir.chdir('testdata/hello') { Dir.glob('**/*') }.sort
|
38
|
+
files.each do |file|
|
39
|
+
path = File.join 'testdata/hello', file
|
40
|
+
next unless File.file?(path)
|
41
|
+
zip.add file, path
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
MiniTest.autorun
|
data/test/test_job.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestJob < MiniTest::Test
|
4
|
+
def setup
|
5
|
+
@template = ContainedMr::Template.new 'contained_mrtests', 'hello',
|
6
|
+
StringIO.new(File.binread('testdata/hello.zip'))
|
7
|
+
@job = ContainedMr::Job.new @template, 'testjob',
|
8
|
+
JSON.load(File.read('testdata/job.hello'))
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
@job.destroy!
|
13
|
+
@template.destroy!
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_mapper_container_options
|
17
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
18
|
+
|
19
|
+
golden = {
|
20
|
+
'name' => 'contained_mrtests_mapper.testjob.2',
|
21
|
+
'Image' => @job.mapper_image_id,
|
22
|
+
'Hostname' => '2.mapper',
|
23
|
+
'Domainname' => '',
|
24
|
+
'Labels' => { 'contained_mr.ctl' => 'contained_mrtests' },
|
25
|
+
'Env' => [ 'ITEM=2', 'ITEMS=3' ],
|
26
|
+
'Ulimits' => [
|
27
|
+
{ 'Name' => 'cpu', 'Hard' => 3, 'Soft' => 3 },
|
28
|
+
{ 'Name' => 'rss', 'Hard' => 1000000, 'Soft' => 1000000 },
|
29
|
+
],
|
30
|
+
'NetworkDisabled' => true, 'ExposedPorts' => {},
|
31
|
+
}
|
32
|
+
assert_equal golden, @job.mapper_container_options(2)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_reducer_container_options
|
36
|
+
golden = {
|
37
|
+
'name' => 'contained_mrtests_reducer.testjob',
|
38
|
+
'Image' => @job.mapper_image_id,
|
39
|
+
'Hostname' => 'reducer',
|
40
|
+
'Domainname' => '',
|
41
|
+
'Labels' => { 'contained_mr.ctl' => 'contained_mrtests' },
|
42
|
+
'Env' => [ 'ITEMS=3' ],
|
43
|
+
'Ulimits' => [
|
44
|
+
{ 'Name' => 'cpu', 'Hard' => 2, 'Soft' => 2 },
|
45
|
+
{ 'Name' => 'rss', 'Hard' => 100000, 'Soft' => 100000 },
|
46
|
+
],
|
47
|
+
'NetworkDisabled' => true, 'ExposedPorts' => {},
|
48
|
+
}
|
49
|
+
assert_equal golden, @job.reducer_container_options
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_build_mapper_image
|
53
|
+
assert_equal 'contained_mrtests/mapper.testjob', @job.mapper_image_tag
|
54
|
+
|
55
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
56
|
+
image = Docker::Image.get @job.mapper_image_tag
|
57
|
+
assert image, 'Docker::Image'
|
58
|
+
assert_operator image.id, :start_with?, @job.mapper_image_id
|
59
|
+
|
60
|
+
1.upto 3 do |i|
|
61
|
+
assert_nil @job.mapper_runner(i), "Mapper #{i} started prematurely"
|
62
|
+
end
|
63
|
+
assert_nil @job.reducer_runner, "Reducer started prematurely"
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_run_mapper_stderr
|
67
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
68
|
+
@job.run_mapper 2
|
69
|
+
|
70
|
+
assert_nil @job.mapper_runner(1), 'Mapper 1 started prematurely'
|
71
|
+
assert_nil @job.mapper_runner(3), 'Mapper 3 started prematurely'
|
72
|
+
assert_nil @job.reducer_runner, 'Reducer started prematurely'
|
73
|
+
|
74
|
+
mapper = @job.mapper_runner 2
|
75
|
+
assert mapper, 'Mapper 2 not started'
|
76
|
+
assert_equal nil, mapper.container_id, 'Mapper container still running'
|
77
|
+
assert_equal "2 3\n", mapper.stderr, 'Stderr: $ITEM + $ITEMS'
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_run_mapper_stdout
|
81
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
82
|
+
mapper = @job.run_mapper 2
|
83
|
+
|
84
|
+
assert_equal nil, mapper.container_id, 'Mapper container still running'
|
85
|
+
assert_equal "2\nmapper input file\nHello world!\n", mapper.stdout,
|
86
|
+
'Stdout: $ITEM + mapper input file + data file'
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_run_mapper_output
|
90
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
91
|
+
mapper = @job.run_mapper 2
|
92
|
+
|
93
|
+
assert_equal nil, mapper.container_id, 'Mapper container still running'
|
94
|
+
assert_equal "2\n", mapper.output, 'Output: ITEM env variable'
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_build_reducer_image
|
98
|
+
assert_equal 'contained_mrtests/reducer.testjob', @job.reducer_image_tag
|
99
|
+
|
100
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
101
|
+
1.upto(3) { |i| @job.run_mapper i }
|
102
|
+
|
103
|
+
@job.build_reducer_image
|
104
|
+
image = Docker::Image.get @job.reducer_image_tag
|
105
|
+
assert image, 'Docker::Image'
|
106
|
+
assert_operator image.id, :start_with?, @job.reducer_image_id
|
107
|
+
|
108
|
+
assert_nil @job.reducer_runner, "Reducer started prematurely"
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_run_reducer
|
112
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
113
|
+
1.upto(3) { |i| @job.run_mapper i }
|
114
|
+
@job.build_reducer_image
|
115
|
+
reducer = @job.run_reducer
|
116
|
+
|
117
|
+
assert @job.reducer_runner, 'reducer_runner return'
|
118
|
+
assert_equal "3 /\n", reducer.stderr, 'Stderr: $ITEMS + $PWD'
|
119
|
+
|
120
|
+
output_gold = "1\n2\n3\n" +
|
121
|
+
"1\nmapper input file\nHello world!\n" +
|
122
|
+
"2\nmapper input file\nHello world!\n" +
|
123
|
+
"3\nmapper input file\nHello world!\n" +
|
124
|
+
"1 3\n2 3\n3 3\n"
|
125
|
+
assert_equal output_gold, reducer.output, 'Stderr: mappers out/stdout/err'
|
126
|
+
|
127
|
+
json_texts = reducer.stdout.split("\n")
|
128
|
+
assert_equal 3, json_texts.length
|
129
|
+
|
130
|
+
jsons = json_texts.map { |t| JSON.load t }
|
131
|
+
assert_equal [false, 0, 42], jsons.map { |j| j['exit_code'] }
|
132
|
+
assert_equal [true, false, false], jsons.map { |j| j['timed_out'] }
|
133
|
+
assert_operator jsons[0]['ran_for'], :>, 2, 'ran_for in mapper 0'
|
134
|
+
assert_operator jsons[1]['ran_for'], :<, 2, 'ran_for in mapper 1'
|
135
|
+
assert_operator jsons[2]['ran_for'], :<, 2, 'ran_for in mapper 2'
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_destroy
|
139
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
140
|
+
1.upto(3) { |i| @job.run_mapper i }
|
141
|
+
@job.build_reducer_image
|
142
|
+
|
143
|
+
@job.destroy!
|
144
|
+
|
145
|
+
assert_raises Docker::Error::NotFoundError do
|
146
|
+
Docker::Image.get @job.mapper_image_tag
|
147
|
+
end
|
148
|
+
|
149
|
+
assert_raises Docker::Error::NotFoundError do
|
150
|
+
Docker::Image.get @job.reducer_image_tag
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/test/test_runner.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestRunner < MiniTest::Test
|
4
|
+
def setup
|
5
|
+
@template = ContainedMr::Template.new 'contained_mrtests', 'hello',
|
6
|
+
StringIO.new(File.binread('testdata/hello.zip'))
|
7
|
+
@job = ContainedMr::Job.new @template, 'testjob',
|
8
|
+
JSON.load(File.read('testdata/job.hello'))
|
9
|
+
@job.build_mapper_image File.read('testdata/input.hello')
|
10
|
+
end
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
@job.destroy!
|
14
|
+
@template.destroy!
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_perform_happy_path
|
18
|
+
runner = ContainedMr::Runner.new @job.mapper_container_options(2), 2.5,
|
19
|
+
@template.mapper_output_path
|
20
|
+
runner.perform
|
21
|
+
|
22
|
+
assert_equal nil, runner.container_id, 'container still running'
|
23
|
+
assert_operator runner.ended_at - runner.started_at, :<, 1, 'running time'
|
24
|
+
assert_equal 0, runner.status_code, 'status code'
|
25
|
+
assert_equal false, runner.timed_out, 'timed out'
|
26
|
+
assert_equal "2 3\n", runner.stderr, 'Stderr: $ITEM + $ITEMS'
|
27
|
+
assert_equal "2\nmapper input file\nHello world!\n", runner.stdout,
|
28
|
+
'Stdout: $ITEM + mapper input file + data file'
|
29
|
+
assert_equal "2\n", runner.output, 'Output: ITEM env variable'
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_perform_exit_code
|
33
|
+
runner = ContainedMr::Runner.new @job.mapper_container_options(3), 2.5,
|
34
|
+
@template.mapper_output_path
|
35
|
+
runner.perform
|
36
|
+
|
37
|
+
assert_equal nil, runner.container_id, 'container still running'
|
38
|
+
assert_operator runner.ended_at - runner.started_at, :<, 1, 'running time'
|
39
|
+
assert_equal 42, runner.status_code, 'status code'
|
40
|
+
assert_equal false, runner.timed_out, 'timed out'
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_perform_timeout
|
44
|
+
runner = ContainedMr::Runner.new @job.mapper_container_options(1), 2.5,
|
45
|
+
@template.mapper_output_path
|
46
|
+
runner.perform
|
47
|
+
|
48
|
+
assert_equal nil, runner.container_id, 'container still running'
|
49
|
+
assert_equal false, runner.status_code, 'status code'
|
50
|
+
assert_equal true, runner.timed_out, 'timed out'
|
51
|
+
assert_operator runner.ended_at - runner.started_at, :>, 2.2,
|
52
|
+
'running time'
|
53
|
+
assert_operator runner.ended_at - runner.started_at, :<, 2.8,
|
54
|
+
'running time'
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestTemplate < MiniTest::Test
|
4
|
+
def setup
|
5
|
+
@template = ContainedMr::Template.new 'contained_mrtests', 'hello',
|
6
|
+
StringIO.new(File.binread('testdata/hello.zip'))
|
7
|
+
end
|
8
|
+
|
9
|
+
def teardown
|
10
|
+
@template.destroy!
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_image_id_matches_created_image
|
14
|
+
image = Docker::Image.get @template.image_tag
|
15
|
+
assert image, 'Docker::Image'
|
16
|
+
assert_operator image.id, :start_with?, @template.image_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_image_tag
|
20
|
+
assert_equal 'contained_mrtests/base.hello', @template.image_tag
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_dockerfiles
|
24
|
+
golden = File.read 'testdata/Dockerfile.hello.mapper'
|
25
|
+
golden.sub! 'contained_mrtests/base.hello', @template.image_id
|
26
|
+
assert_equal golden, @template.mapper_dockerfile, 'mapper Dockerfile'
|
27
|
+
|
28
|
+
golden = File.read 'testdata/Dockerfile.hello.reducer'
|
29
|
+
golden.sub! 'contained_mrtests/base.hello', @template.image_id
|
30
|
+
assert_equal golden, @template.reducer_dockerfile, 'reducer Dockerfile'
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_paths
|
34
|
+
assert_equal '/usr/mrd/map-output', @template.mapper_output_path
|
35
|
+
assert_equal '/usr/mrd/reduce-output', @template.reducer_output_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_envs
|
39
|
+
assert_equal 3, @template.item_count
|
40
|
+
assert_equal ['ITEM=2', 'ITEMS=3'], @template.mapper_env(2)
|
41
|
+
assert_equal ['ITEMS=3'], @template.reducer_env
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_destroy
|
45
|
+
@template.destroy!
|
46
|
+
assert_raises Docker::Error::NotFoundError do
|
47
|
+
Docker::Image.get @template.image_tag
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Goodbye, cruel world!
|
@@ -0,0 +1 @@
|
|
1
|
+
Hello world!
|
@@ -0,0 +1,18 @@
|
|
1
|
+
---
|
2
|
+
items: 3
|
3
|
+
mapper:
|
4
|
+
input: /usr/mrd/map-input
|
5
|
+
output: /usr/mrd/map-output
|
6
|
+
chdir: /usr/mrd
|
7
|
+
env: ITEM
|
8
|
+
cmd:
|
9
|
+
- /bin/sh
|
10
|
+
- /usr/mrd/mapper.sh
|
11
|
+
reducer:
|
12
|
+
input: /usr/mrd/
|
13
|
+
output: /usr/mrd/reduce-output
|
14
|
+
chdir: /
|
15
|
+
env: ITEMS
|
16
|
+
cmd:
|
17
|
+
- /bin/sh
|
18
|
+
- /usr/mrd/reducer.sh
|
@@ -0,0 +1 @@
|
|
1
|
+
mapper input file
|
data/testdata/job.hello
ADDED
metadata
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: contained_mr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Victor Costan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-09-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: docker-api
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.22.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.22.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubyzip
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.1.7
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.1.7
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rdoc
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.12'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.12'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bundler
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.6.1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.6.1
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: jeweler
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 2.0.1
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.0.1
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: simplecov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Plumbing for running mappers and reducers inside Docker containers
|
126
|
+
email: victor@costan.us
|
127
|
+
executables: []
|
128
|
+
extensions: []
|
129
|
+
extra_rdoc_files:
|
130
|
+
- LICENSE
|
131
|
+
- README.rdoc
|
132
|
+
files:
|
133
|
+
- ".document"
|
134
|
+
- Gemfile
|
135
|
+
- Gemfile.lock
|
136
|
+
- LICENSE
|
137
|
+
- README.rdoc
|
138
|
+
- Rakefile
|
139
|
+
- VERSION
|
140
|
+
- lib/contained_mr.rb
|
141
|
+
- lib/contained_mr/cleaner.rb
|
142
|
+
- lib/contained_mr/job.rb
|
143
|
+
- lib/contained_mr/runner.rb
|
144
|
+
- lib/contained_mr/template.rb
|
145
|
+
- test/helper.rb
|
146
|
+
- test/test_job.rb
|
147
|
+
- test/test_runner.rb
|
148
|
+
- test/test_template.rb
|
149
|
+
- testdata/Dockerfile.hello.mapper
|
150
|
+
- testdata/Dockerfile.hello.reducer
|
151
|
+
- testdata/hello/Dockerfile
|
152
|
+
- testdata/hello/data/goodbye.txt
|
153
|
+
- testdata/hello/data/hello.txt
|
154
|
+
- testdata/hello/mapper.sh
|
155
|
+
- testdata/hello/mapreduced.yml
|
156
|
+
- testdata/hello/reducer.sh
|
157
|
+
- testdata/input.hello
|
158
|
+
- testdata/job.hello
|
159
|
+
homepage: http://github.com/pwnall/contained_mr
|
160
|
+
licenses:
|
161
|
+
- MIT
|
162
|
+
metadata: {}
|
163
|
+
post_install_message:
|
164
|
+
rdoc_options: []
|
165
|
+
require_paths:
|
166
|
+
- lib
|
167
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - ">="
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
requirements: []
|
178
|
+
rubyforge_project:
|
179
|
+
rubygems_version: 2.4.5
|
180
|
+
signing_key:
|
181
|
+
specification_version: 4
|
182
|
+
summary: Map-Reduce with Docker containers
|
183
|
+
test_files: []
|