contained_mr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
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
@@ -0,0 +1,8 @@
1
+ # Namespace.
2
+ module ContainedMr
3
+ end
4
+
5
+ require_relative 'contained_mr/cleaner.rb'
6
+ require_relative 'contained_mr/job.rb'
7
+ require_relative 'contained_mr/runner.rb'
8
+ require_relative 'contained_mr/template.rb'
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
@@ -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,4 @@
1
+ FROM contained_mrtests/base.hello
2
+ COPY input /usr/mrd/map-input
3
+ WORKDIR /usr/mrd
4
+ ENTRYPOINT ["/bin/sh","/usr/mrd/mapper.sh"]
@@ -0,0 +1,4 @@
1
+ FROM contained_mrtests/base.hello
2
+ COPY . /usr/mrd/
3
+ WORKDIR /
4
+ ENTRYPOINT ["/bin/sh","/usr/mrd/reducer.sh"]
@@ -0,0 +1,4 @@
1
+ FROM busybox
2
+
3
+ RUN mkdir -p /usr/mrd && chmod 777 /usr/mrd
4
+ COPY . /usr/mrd/
@@ -0,0 +1 @@
1
+ Goodbye, cruel world!
@@ -0,0 +1 @@
1
+ Hello world!
@@ -0,0 +1,9 @@
1
+ echo $ITEM > map-output
2
+ cat map-output map-input data/hello.txt
3
+ >&2 echo $ITEM $ITEMS
4
+ if [ "$ITEM" -eq "3" ]; then
5
+ exit 42
6
+ fi
7
+ if [ "$ITEM" -eq "1" ]; then
8
+ sleep 100
9
+ fi
@@ -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,10 @@
1
+ >&2 echo $ITEMS $PWD
2
+ cd /usr/mrd
3
+ cat 1.out 2.out 3.out > reduce-output
4
+ cat 1.stdout 2.stdout 3.stdout >> reduce-output
5
+ cat 1.stderr 2.stderr 3.stderr >> reduce-output
6
+ cat 1.json
7
+ echo
8
+ cat 2.json
9
+ echo
10
+ cat 3.json
@@ -0,0 +1 @@
1
+ mapper input file
@@ -0,0 +1,10 @@
1
+ {
2
+ "mapper": {
3
+ "wait_time": 2.5,
4
+ "ulimits": { "cpu": 3, "rss": 1000000 }
5
+ },
6
+ "reducer": {
7
+ "wait_time": 2,
8
+ "ulimits": { "cpu": 2, "rss": 100000 }
9
+ }
10
+ }
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: []