costan-tem_mr_search 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +5 -0
- data/LICENSE +21 -0
- data/Manifest +32 -0
- data/README +4 -0
- data/Rakefile +23 -0
- data/bin/tem_mr_search_server +13 -0
- data/lib/tem_mr_search/client.rb +52 -0
- data/lib/tem_mr_search/client_query.rb +21 -0
- data/lib/tem_mr_search/db.rb +26 -0
- data/lib/tem_mr_search/map_reduce_executor.rb +115 -0
- data/lib/tem_mr_search/map_reduce_job.rb +108 -0
- data/lib/tem_mr_search/map_reduce_planner.rb +169 -0
- data/lib/tem_mr_search/query_builder.rb +167 -0
- data/lib/tem_mr_search/server.rb +90 -0
- data/lib/tem_mr_search/web_client_query_builder.rb +50 -0
- data/lib/tem_mr_search.rb +20 -0
- data/tem_mr_search.gemspec +39 -0
- data/test/mr_test_case.rb +36 -0
- data/test/test_client_server.rb +57 -0
- data/test/test_db.rb +16 -0
- data/test/test_map_reduce_executor.rb +40 -0
- data/test/test_map_reduce_job.rb +65 -0
- data/test/test_map_reduce_planner.rb +55 -0
- data/test/test_query_builders.rb +40 -0
- data/testdata/cluster.yml +3 -0
- data/testdata/empty_cluster.yml +2 -0
- data/testdata/fares.yml +57 -0
- data/testdata/parallel_plan_431.yml +52 -0
- data/testdata/parallel_plan_740.yml +87 -0
- data/testdata/serial_plan_410.yml +36 -0
- data/testdata/serial_plan_431.yml +56 -0
- data/testdata/serial_plan_740.yml +93 -0
- metadata +126 -0
@@ -0,0 +1,167 @@
|
|
1
|
+
# :nodoc: namespace
|
2
|
+
module Tem::Mr::Search
|
3
|
+
|
4
|
+
class QueryBuilder
|
5
|
+
# Build a Query.
|
6
|
+
def self.query
|
7
|
+
builder = self.new
|
8
|
+
yield builder
|
9
|
+
builder.query
|
10
|
+
end
|
11
|
+
|
12
|
+
# Defines the object attributes imported into the map method.
|
13
|
+
def attributes(attributes)
|
14
|
+
@attributes = attributes.to_a.map do |k, v|
|
15
|
+
{ :name => k,
|
16
|
+
:type => v,
|
17
|
+
:length => Tem::Abi.send(:"#{v}_length")
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Defines the object attribute that's used as an object ID.
|
23
|
+
def id_attribute(id_attribute)
|
24
|
+
@id_attribute = id_attribute.to_sym
|
25
|
+
end
|
26
|
+
|
27
|
+
# Defines the query's map procedure.
|
28
|
+
def map
|
29
|
+
@map_secpack = Tem::Assembler.assemble do |s|
|
30
|
+
s.label :_secret
|
31
|
+
s.label :_key
|
32
|
+
s.zeros :tem_ubyte, 16
|
33
|
+
s.label :_check_bytes
|
34
|
+
s.data :tem_ubyte, @check_bytes
|
35
|
+
|
36
|
+
# User-provided ranking procedure (secret).
|
37
|
+
s.label :_ranking
|
38
|
+
yield s
|
39
|
+
s.ret
|
40
|
+
|
41
|
+
s.entry
|
42
|
+
s.ldbc 16
|
43
|
+
s.outnew
|
44
|
+
s.call :_ranking
|
45
|
+
s.ldbc 3
|
46
|
+
s.ldwc :_nonce
|
47
|
+
s.rnd
|
48
|
+
s.mcfxb :from => :_check_bytes, :to => :_check, :size => 3
|
49
|
+
# TODO(costan): encryption instead of plain dump
|
50
|
+
s.outfxb :from => :_id, :size => 16
|
51
|
+
s.halt
|
52
|
+
|
53
|
+
s.label :_plain
|
54
|
+
|
55
|
+
# Make room for query attributes.
|
56
|
+
@attributes.each do |attribute|
|
57
|
+
s.label attribute[:name]
|
58
|
+
s.zeros attribute[:type], 1
|
59
|
+
end
|
60
|
+
# Object ID.
|
61
|
+
s.label :_id
|
62
|
+
s.zeros :tem_ubyte, 8
|
63
|
+
# Object score.
|
64
|
+
s.label :score
|
65
|
+
s.zeros :tem_short, 1
|
66
|
+
# Random nonce to prevent matching map outputs.
|
67
|
+
s.label :_nonce
|
68
|
+
s.zeros :tem_ubyte, 3
|
69
|
+
# Check bytes to prevent malicious input corruption.
|
70
|
+
s.label :_check
|
71
|
+
s.zeros :tem_ubyte, 3
|
72
|
+
|
73
|
+
s.stack 64
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Defines the query's reduce procedure.
|
78
|
+
def reduce
|
79
|
+
@reduce_secpack = Tem::Assembler.assemble do |s|
|
80
|
+
s.label :_secret
|
81
|
+
s.label :_key
|
82
|
+
s.zeros :tem_ubyte, 16
|
83
|
+
s.label :_check
|
84
|
+
s.data :tem_ubyte, @check_bytes
|
85
|
+
|
86
|
+
s.label :_signed
|
87
|
+
# User-provided comparison procedure (signed).
|
88
|
+
s.label :_comparison_proc
|
89
|
+
yield s
|
90
|
+
s.ret
|
91
|
+
|
92
|
+
s.entry
|
93
|
+
s.ldbc 16
|
94
|
+
s.outnew
|
95
|
+
# Decode inputs.
|
96
|
+
[1, 2].each do |i|
|
97
|
+
# TODO(costan): decrypt instead of copying
|
98
|
+
s.mcfxb :from => :"_output#{i}", :to => :"_id#{i}", :size => 16
|
99
|
+
|
100
|
+
# Compare the check bytes and abort if the inputs were tampered with.
|
101
|
+
s.mcmpfxb :op1 => :"_check#{i}", :op2 => :"_check", :size => 3
|
102
|
+
s.jz :"_check_#{i}_ok"
|
103
|
+
s.halt
|
104
|
+
s.label :"_check_#{i}_ok"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Compare and output.
|
108
|
+
s.call :_comparison_proc
|
109
|
+
s.ldw :comparison
|
110
|
+
s.jae :_output1_wins
|
111
|
+
s.mcfxb :from => :_id2, :to => :_id1, :size => 16
|
112
|
+
s.jmp :_output
|
113
|
+
s.label :_output1_wins
|
114
|
+
# Still do a memcpy, to prevent timing attacks.
|
115
|
+
s.mcfxb :from => :_id2, :to => :_id2, :size => 16
|
116
|
+
s.jmp :_output
|
117
|
+
# Refresh the nonce to prevent learning about the comparison criteria.
|
118
|
+
s.label :_output
|
119
|
+
s.ldbc 3
|
120
|
+
s.ldwc :_nonce1
|
121
|
+
s.rnd
|
122
|
+
# TODO(costan): encrypt instead of copying
|
123
|
+
s.outfxb :from => :_id1, :size => 16
|
124
|
+
s.halt
|
125
|
+
|
126
|
+
s.label :_plain
|
127
|
+
# The comparison result produced by the user comparison procedure.
|
128
|
+
s.label :comparison
|
129
|
+
s.zeros :tem_short, 1
|
130
|
+
|
131
|
+
# The two inputs to reduce.
|
132
|
+
[1, 2].each do |i|
|
133
|
+
# Encrypted map/reduce output.
|
134
|
+
s.label :"_output#{i}"
|
135
|
+
s.zeros :tem_ubyte, 16
|
136
|
+
# Unencrypted input (decrypted inside TEM).
|
137
|
+
s.label :"_id#{i}"
|
138
|
+
s.zeros :tem_ubyte, 8
|
139
|
+
s.label :"score#{i}"
|
140
|
+
s.zeros :tem_short, 1
|
141
|
+
s.label :"_nonce#{i}"
|
142
|
+
s.zeros :tem_ubyte, 3
|
143
|
+
s.label :"_check#{i}"
|
144
|
+
s.zeros :tem_ubyte, 3
|
145
|
+
end
|
146
|
+
s.stack 8
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def query
|
151
|
+
raise "Map procedure not specified" unless @map_secpack
|
152
|
+
raise "Reduce procedure not specified" unless @reduce_secpack
|
153
|
+
raise "ID attribute not specified" unless @id_attribute
|
154
|
+
|
155
|
+
ClientQuery.new :key => @query_key, :attributes => @attributes,
|
156
|
+
:map => @map_secpack, :reduce => @reduce_secpack,
|
157
|
+
:id_attribute => @id_attribute
|
158
|
+
end
|
159
|
+
|
160
|
+
def initialize
|
161
|
+
@check_bytes = [1, 2, 3]
|
162
|
+
# TODO(costan): generate query key
|
163
|
+
@query_key = nil
|
164
|
+
end
|
165
|
+
end # class QueryBuilder
|
166
|
+
|
167
|
+
end # namespace Tem::Mr::Search
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
# :nodoc: namespace
|
5
|
+
module Tem::Mr::Search
|
6
|
+
|
7
|
+
class Server
|
8
|
+
DEFAULT_PORT = 9052
|
9
|
+
|
10
|
+
OP = Zerg::Support::Protocols::ObjectProtocol
|
11
|
+
OPAdapter = Zerg::Support::Sockets::ProtocolAdapter.adapter_module OP
|
12
|
+
|
13
|
+
# Creates a new Map-Reduce server (master).
|
14
|
+
def initialize(db_file, cluster_file, port)
|
15
|
+
@logger = Logger.new STDERR
|
16
|
+
@db = Db.new db_file
|
17
|
+
@cluster_file = cluster_file
|
18
|
+
@tems = []
|
19
|
+
refresh_tems!
|
20
|
+
@port = port || DEFAULT_PORT
|
21
|
+
end
|
22
|
+
|
23
|
+
# Reinitializes the TEM cluster connections.
|
24
|
+
#
|
25
|
+
# This should be called reasonably often to be able to respond to cluster
|
26
|
+
# configuration changes.
|
27
|
+
def refresh_tems!
|
28
|
+
@tems.each { |tem| tem.disconnect }
|
29
|
+
@tems = Server.tems_from_cluster_file @cluster_file
|
30
|
+
end
|
31
|
+
|
32
|
+
# This server's loop.
|
33
|
+
def serve_loop
|
34
|
+
listen_socket = Zerg::Support::SocketFactory.socket :in_port => @port
|
35
|
+
listen_socket.listen
|
36
|
+
shutdown_received = false
|
37
|
+
until shutdown_received
|
38
|
+
begin
|
39
|
+
client_socket, client_addr = listen_socket.accept
|
40
|
+
client_socket.extend OPAdapter
|
41
|
+
request = client_socket.recv_object
|
42
|
+
begin
|
43
|
+
response = process_request request
|
44
|
+
rescue Exception => e
|
45
|
+
@logger.error e
|
46
|
+
response = nil
|
47
|
+
end
|
48
|
+
client_socket.send_object response if response
|
49
|
+
shutdown_received = true if response == :shutdown
|
50
|
+
rescue Exception => e
|
51
|
+
@logger.error e
|
52
|
+
end
|
53
|
+
client_socket.close
|
54
|
+
end
|
55
|
+
listen_socket.close
|
56
|
+
end
|
57
|
+
|
58
|
+
# Computes the response of a single request.
|
59
|
+
def process_request(request)
|
60
|
+
case request[:type]
|
61
|
+
when :search
|
62
|
+
refresh_tems!
|
63
|
+
job = MapReduceJob.new request[:map_reduce]
|
64
|
+
root_tem = request[:root_tem]
|
65
|
+
executor = MapReduceExecutor.new job, @db, @tems, root_tem
|
66
|
+
return executor.execute
|
67
|
+
when :fetch
|
68
|
+
return @db.item_by_id(request[:id]) || :not_found
|
69
|
+
when :shutdown
|
70
|
+
return :shutdown
|
71
|
+
when :db_dump
|
72
|
+
return (0...@db.length).map { |i| @db.item(i) }
|
73
|
+
else
|
74
|
+
return :unknown
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Creates sessions to all the TEMs in a cluster.
|
79
|
+
def self.tems_from_cluster_file(cluster_file)
|
80
|
+
cluster_hosts = File.open(cluster_file, 'r') { |f| YAML.load f }
|
81
|
+
cluster_configs = cluster_hosts.map { |host|
|
82
|
+
Tem::MultiProxy::Client.query_tems host
|
83
|
+
}.flatten
|
84
|
+
cluster_configs.reject { |config| config.nil? }.map do |config|
|
85
|
+
Tem::Session.new Tem::Transport::AutoConfigurator.try_transport(config)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end # namespace Tem::Mr::Search
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# :nodoc: namespace
|
2
|
+
module Tem::Mr::Search
|
3
|
+
|
4
|
+
class WebClientQueryBuilder < MapReduceJob
|
5
|
+
# Builds a client query covering preferences expressed in the Web UI.
|
6
|
+
#
|
7
|
+
# The supported (and required) options are:
|
8
|
+
# layovers_cost:: the cost of each layover
|
9
|
+
# start_time_cost:: the cost of the flight's departure time, in minutes
|
10
|
+
# duration_cost:: the cost of each minute of flying
|
11
|
+
def self.query(options)
|
12
|
+
QueryBuilder.query { |q|
|
13
|
+
q.attributes :price => :tem_short, :start_time => :tem_short,
|
14
|
+
:end_time => :tem_short, :layovers => :tem_short
|
15
|
+
q.id_attribute :flight
|
16
|
+
|
17
|
+
# Score: 20000 - price - layover_cost * layovers -
|
18
|
+
# start_time * start_time_cost -
|
19
|
+
# (end_time - start_time) * duration_cost
|
20
|
+
q.map { |s|
|
21
|
+
s.ldwc 20000
|
22
|
+
s.ldw :price
|
23
|
+
s.sub
|
24
|
+
s.ldw :end_time
|
25
|
+
s.ldw :start_time
|
26
|
+
s.sub
|
27
|
+
s.ldwc options[:duration_cost]
|
28
|
+
s.mul
|
29
|
+
s.sub
|
30
|
+
[:start_time, :layovers].each do |factor|
|
31
|
+
s.ldw factor
|
32
|
+
s.ldwc options[:"#{factor}_cost"]
|
33
|
+
s.mul
|
34
|
+
s.sub
|
35
|
+
end
|
36
|
+
s.stw :score
|
37
|
+
}
|
38
|
+
|
39
|
+
# The greater score wins.
|
40
|
+
q.reduce { |s|
|
41
|
+
s.ldw :score1
|
42
|
+
s.ldw :score2
|
43
|
+
s.cmp
|
44
|
+
s.stw :comparison
|
45
|
+
}
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end # namespace Tem::Mr::search
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'tem_multi_proxy'
|
3
|
+
require 'tem_ruby'
|
4
|
+
|
5
|
+
# :nodoc: namespace
|
6
|
+
module Tem::Mr
|
7
|
+
end
|
8
|
+
# :nodoc: namespace
|
9
|
+
module Tem::Mr::Search
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'tem_mr_search/client.rb'
|
13
|
+
require 'tem_mr_search/db.rb'
|
14
|
+
require 'tem_mr_search/map_reduce_executor.rb'
|
15
|
+
require 'tem_mr_search/map_reduce_job.rb'
|
16
|
+
require 'tem_mr_search/map_reduce_planner.rb'
|
17
|
+
require 'tem_mr_search/query_builder.rb'
|
18
|
+
require 'tem_mr_search/client_query.rb'
|
19
|
+
require 'tem_mr_search/server.rb'
|
20
|
+
require 'tem_mr_search/web_client_query_builder.rb'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{tem_mr_search}
|
5
|
+
s.version = "0.2.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Victor Costan"]
|
9
|
+
s.date = %q{2009-06-04}
|
10
|
+
s.default_executable = %q{tem_mr_search_server}
|
11
|
+
s.description = %q{Tem Map-Reduce proof of concept: database search.}
|
12
|
+
s.email = %q{victor@costan.us}
|
13
|
+
s.executables = ["tem_mr_search_server"]
|
14
|
+
s.extra_rdoc_files = ["bin/tem_mr_search_server", "CHANGELOG", "lib/tem_mr_search/client.rb", "lib/tem_mr_search/client_query.rb", "lib/tem_mr_search/db.rb", "lib/tem_mr_search/map_reduce_executor.rb", "lib/tem_mr_search/map_reduce_job.rb", "lib/tem_mr_search/map_reduce_planner.rb", "lib/tem_mr_search/query_builder.rb", "lib/tem_mr_search/server.rb", "lib/tem_mr_search/web_client_query_builder.rb", "lib/tem_mr_search.rb", "LICENSE", "README"]
|
15
|
+
s.files = ["bin/tem_mr_search_server", "CHANGELOG", "lib/tem_mr_search/client.rb", "lib/tem_mr_search/client_query.rb", "lib/tem_mr_search/db.rb", "lib/tem_mr_search/map_reduce_executor.rb", "lib/tem_mr_search/map_reduce_job.rb", "lib/tem_mr_search/map_reduce_planner.rb", "lib/tem_mr_search/query_builder.rb", "lib/tem_mr_search/server.rb", "lib/tem_mr_search/web_client_query_builder.rb", "lib/tem_mr_search.rb", "LICENSE", "Manifest", "Rakefile", "README", "tem_mr_search.gemspec", "test/mr_test_case.rb", "test/test_client_server.rb", "test/test_db.rb", "test/test_map_reduce_executor.rb", "test/test_map_reduce_job.rb", "test/test_map_reduce_planner.rb", "test/test_query_builders.rb", "testdata/cluster.yml", "testdata/empty_cluster.yml", "testdata/fares.yml", "testdata/parallel_plan_431.yml", "testdata/parallel_plan_740.yml", "testdata/serial_plan_410.yml", "testdata/serial_plan_431.yml", "testdata/serial_plan_740.yml"]
|
16
|
+
s.homepage = %q{http://tem.rubyforge.org}
|
17
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Tem_mr_search", "--main", "README"]
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
s.rubyforge_project = %q{tem}
|
20
|
+
s.rubygems_version = %q{1.3.4}
|
21
|
+
s.summary = %q{Tem Map-Reduce proof of concept: database search.}
|
22
|
+
s.test_files = ["test/test_client_server.rb", "test/test_db.rb", "test/test_map_reduce_executor.rb", "test/test_map_reduce_job.rb", "test/test_map_reduce_planner.rb", "test/test_query_builders.rb"]
|
23
|
+
|
24
|
+
if s.respond_to? :specification_version then
|
25
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
26
|
+
s.specification_version = 3
|
27
|
+
|
28
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
29
|
+
s.add_runtime_dependency(%q<tem_ruby>, [">= 0.11.2"])
|
30
|
+
s.add_runtime_dependency(%q<tem_multi_proxy>, [">= 0.2"])
|
31
|
+
else
|
32
|
+
s.add_dependency(%q<tem_ruby>, [">= 0.11.2"])
|
33
|
+
s.add_dependency(%q<tem_multi_proxy>, [">= 0.2"])
|
34
|
+
end
|
35
|
+
else
|
36
|
+
s.add_dependency(%q<tem_ruby>, [">= 0.11.2"])
|
37
|
+
s.add_dependency(%q<tem_multi_proxy>, [">= 0.2"])
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'tem_mr_search'
|
3
|
+
|
4
|
+
class MrTestCase < Test::Unit::TestCase
|
5
|
+
include Tem::Mr::Search
|
6
|
+
|
7
|
+
def setup
|
8
|
+
super
|
9
|
+
|
10
|
+
Thread.abort_on_exception = true
|
11
|
+
|
12
|
+
@db_path = File.join File.dirname(__FILE__), "..", "testdata", "fares.yml"
|
13
|
+
@cluster_file = File.join File.dirname(__FILE__), "..", "testdata",
|
14
|
+
"cluster.yml"
|
15
|
+
@empty_cluster_file = File.join File.dirname(__FILE__), "..", "testdata",
|
16
|
+
"empty_cluster.yml"
|
17
|
+
@db = Db.new @db_path
|
18
|
+
|
19
|
+
@client_query = WebClientQueryBuilder.query :layovers_cost => 1000,
|
20
|
+
:start_time_cost => -1,
|
21
|
+
:duration_cost => 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def fare_score(fare)
|
25
|
+
20000 + fare['start_time'] - fare['price'] - (fare['end_time'] -
|
26
|
+
fare['start_time']) - fare['layovers'] * 1000
|
27
|
+
end
|
28
|
+
|
29
|
+
def fare_id(fare)
|
30
|
+
fare['flight']
|
31
|
+
end
|
32
|
+
|
33
|
+
# Ensures that everything has loaded.
|
34
|
+
def test_smoke
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'test/mr_test_case'
|
2
|
+
require 'flexmock/test_unit'
|
3
|
+
|
4
|
+
|
5
|
+
class ClientServerTest < MrTestCase
|
6
|
+
Client = Tem::Mr::Search::Client
|
7
|
+
Server = Tem::Mr::Search::Server
|
8
|
+
|
9
|
+
def setup
|
10
|
+
super
|
11
|
+
@server_port = 29550
|
12
|
+
end
|
13
|
+
|
14
|
+
def _test_request
|
15
|
+
Thread.new do
|
16
|
+
Server.new(@db_path, @empty_cluster_file, @server_port).serve_loop
|
17
|
+
end
|
18
|
+
sleep 0.1
|
19
|
+
yield "localhost:#{@server_port}"
|
20
|
+
Client.shutdown_server "localhost:#{@server_port}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_fetch_item
|
24
|
+
_test_request do |server_addr|
|
25
|
+
fetched_item = Client.fetch_item server_addr, fare_id(@db.item(3))
|
26
|
+
assert_equal @db.item(3), fetched_item, 'Fetch fail'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_dump_database
|
31
|
+
@server_port = 29557
|
32
|
+
_test_request do |server_addr|
|
33
|
+
items = Client.dump_database server_addr
|
34
|
+
assert_equal @db.length, items.length, 'Wrong number of items'
|
35
|
+
items.each_with_index do |item, i|
|
36
|
+
assert_equal @db.item(i), item, "Discrepancy in item #{i}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_query
|
42
|
+
@server_port = 29551
|
43
|
+
flexmock(Server).should_receive(:tems_from_cluster_file).
|
44
|
+
with(@empty_cluster_file).and_return do |file|
|
45
|
+
Tem.auto_conf
|
46
|
+
[$tem]
|
47
|
+
end
|
48
|
+
_test_request do |server_addr|
|
49
|
+
result = Client.search server_addr, @client_query
|
50
|
+
gold_item = @db.item 5
|
51
|
+
assert_equal fare_id(gold_item), result[:id],
|
52
|
+
'Incorrect Map-Reduce result (ID)'
|
53
|
+
assert_equal fare_score(gold_item), result[:score],
|
54
|
+
'Incorrect Map-Reduce result (score)'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/test/test_db.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test/mr_test_case'
|
2
|
+
|
3
|
+
class DbTest < MrTestCase
|
4
|
+
def test_loading
|
5
|
+
assert_equal 8, @db.length, 'Number of items in the database'
|
6
|
+
gold_item = {"price" => 2500, "from" => "BOS", "to" => "TPE",
|
7
|
+
"flight" => 15, "layovers"=>2, "end_time"=>2100,
|
8
|
+
"start_time"=>900}
|
9
|
+
assert_equal gold_item, @db.item(0), 'First database item'
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_by_id
|
13
|
+
assert_equal 18, @db.item_by_id(18)['flight'], 'Finding existing item by ID'
|
14
|
+
assert_equal nil, @db.item_by_id(5), 'Finding non-existing item'
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test/mr_test_case'
|
2
|
+
|
3
|
+
class MapReduceExecutorTest < MrTestCase
|
4
|
+
MRExecutor = Tem::Mr::Search::MapReduceExecutor
|
5
|
+
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
Tem.auto_conf
|
9
|
+
$tem.activate
|
10
|
+
$tem.emit
|
11
|
+
end
|
12
|
+
|
13
|
+
def teardown
|
14
|
+
$tem.disconnect
|
15
|
+
end
|
16
|
+
|
17
|
+
def _test_executor(tems, root_tem)
|
18
|
+
executor = MRExecutor.new @client_query, @db, tems, root_tem
|
19
|
+
packed_output = executor.execute
|
20
|
+
result = @client_query.unpack_output packed_output
|
21
|
+
gold_item = @db.item 5
|
22
|
+
assert_equal fare_id(gold_item), result[:id],
|
23
|
+
'Incorrect Map-Reduce result (ID)'
|
24
|
+
assert_equal fare_score(gold_item), result[:score],
|
25
|
+
'Incorrect Map-Reduce result (score)'
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_executor_with_autoconf
|
29
|
+
_test_executor [$tem], 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_executor_with_cluster
|
33
|
+
tems = Tem::Mr::Search::Server.tems_from_cluster_file @cluster_file
|
34
|
+
assert_equal 8, tems.length, 'Incorrect cluster setup'
|
35
|
+
|
36
|
+
tems.each { |tem| tem.activate; tem.emit }
|
37
|
+
|
38
|
+
_test_executor tems, 0
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'test/mr_test_case'
|
2
|
+
|
3
|
+
class MapReduceJobTest < MrTestCase
|
4
|
+
MapReduceJob = Tem::Mr::Search::MapReduceJob
|
5
|
+
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
|
9
|
+
@obj1 = @db.item 0
|
10
|
+
@obj2 = @db.item 1
|
11
|
+
@output1 = (1..16).to_a
|
12
|
+
@output2 = (17..32).to_a
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_map_for_object
|
16
|
+
obj = @obj1.merge 'flight' => 0x12345678
|
17
|
+
secpack = @client_query.mapper.map_for_object obj
|
18
|
+
|
19
|
+
assert_equal [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78],
|
20
|
+
secpack.get_bytes(:_id, 8), 'Object ID embedded incorrectly'
|
21
|
+
assert_equal @obj1['price'], secpack.get_value(:price, :tem_short),
|
22
|
+
'Price embedded incorrectly'
|
23
|
+
assert_equal @obj1['layovers'], secpack.get_value(:layovers, :tem_short),
|
24
|
+
'Layover count embedded incorrectly'
|
25
|
+
assert_equal @obj1['start_time'],
|
26
|
+
secpack.get_value(:start_time, :tem_short),
|
27
|
+
'Starting time embedded incorrectly'
|
28
|
+
assert_equal @obj1['end_time'],
|
29
|
+
secpack.get_value(:end_time, :tem_short),
|
30
|
+
'Ending time embedded incorrectly'
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_reduce_for_outputs
|
34
|
+
secpack = @client_query.reducer.reduce_for_outputs @output1, @output2
|
35
|
+
|
36
|
+
assert_equal @output1, secpack.get_bytes(:_output1, 16),
|
37
|
+
'Output1 embedded incorrectly'
|
38
|
+
assert_equal @output2, secpack.get_bytes(:_output2, 16),
|
39
|
+
'Output2 embedded incorrectly'
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_unpack_unencrypted_output
|
43
|
+
packed_output = [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0x31, 0x41, 0xCC, 0xCD,
|
44
|
+
0xCE, 0xBE, 0xEF, 0xFE]
|
45
|
+
output = @client_query.unpack_decrypted_output packed_output
|
46
|
+
|
47
|
+
assert_equal 0x12345678, output[:id], 'ID incorrectly unpacked'
|
48
|
+
assert_equal 0x3141, output[:score], 'Score incorrectly unpacked'
|
49
|
+
assert_equal [0xBE, 0xEF, 0xFE], output[:check], 'Check bytes'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class SerializedMapReduceJobTest < MapReduceJobTest
|
54
|
+
# Serialization is successful if a de-serialized job can pass all the tests.
|
55
|
+
|
56
|
+
def setup
|
57
|
+
super
|
58
|
+
@hash = @client_query.to_hash
|
59
|
+
@client_query = MapReduceJob.new @hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_serialized_form
|
63
|
+
assert_equal Hash, @hash.class, 'Serialization did not produce a plain hash'
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'test/mr_test_case'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
class MapReducePlannerTest < Test::Unit::TestCase
|
5
|
+
MRPlanner = Tem::Mr::Search::MapReducePlanner
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@testdata_path = File.join(File.dirname(__FILE__), '..', 'testdata')
|
9
|
+
end
|
10
|
+
|
11
|
+
def parallel_planning(planner)
|
12
|
+
all_actions = []
|
13
|
+
until planner.done?
|
14
|
+
actions = planner.next_actions!
|
15
|
+
all_actions << actions
|
16
|
+
actions.each { |action| planner.action_done action }
|
17
|
+
end
|
18
|
+
all_actions
|
19
|
+
end
|
20
|
+
|
21
|
+
def serial_planning(planner)
|
22
|
+
all_actions = []
|
23
|
+
pending_actions = []
|
24
|
+
until planner.done?
|
25
|
+
actions = planner.next_actions!
|
26
|
+
all_actions << actions
|
27
|
+
pending_actions += actions
|
28
|
+
action = pending_actions.shift
|
29
|
+
planner.action_done action if action
|
30
|
+
end
|
31
|
+
all_actions
|
32
|
+
end
|
33
|
+
|
34
|
+
def _test_planning(method_name, items, tems, root_tem, gold_file)
|
35
|
+
planner = MRPlanner.new nil, items, tems, root_tem
|
36
|
+
all_actions = self.send method_name, planner
|
37
|
+
gold_actions = File.open(File.join(@testdata_path, gold_file), 'r') do |f|
|
38
|
+
YAML.load f
|
39
|
+
end
|
40
|
+
assert_equal gold_actions, all_actions, "Failed #{method_name}: " +
|
41
|
+
"#{tems} tems with root #{root_tem}, #{items} items"
|
42
|
+
assert_equal items * 2 - 1, planner.output_id, "Wrong final output_id"
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_planning
|
46
|
+
[[:parallel_planning, 7, 4, 0, 'parallel_plan_740.yml'],
|
47
|
+
[:parallel_planning, 4, 3, 1, 'parallel_plan_431.yml'],
|
48
|
+
[:serial_planning, 4, 1, 0, 'serial_plan_410.yml'],
|
49
|
+
[:serial_planning, 7, 4, 0, 'serial_plan_740.yml'],
|
50
|
+
[:serial_planning, 4, 3, 1, 'serial_plan_431.yml'],
|
51
|
+
].each do |testcase|
|
52
|
+
_test_planning *testcase
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test/mr_test_case'
|
2
|
+
|
3
|
+
class QueryBuildersTest < MrTestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
Tem.auto_conf
|
7
|
+
$tem.activate
|
8
|
+
$tem.emit
|
9
|
+
end
|
10
|
+
|
11
|
+
def _test_map_fare(fare)
|
12
|
+
enc_output = @client_query.mapper.map_object fare, $tem
|
13
|
+
output = @client_query.unpack_output enc_output
|
14
|
+
assert_equal fare_id(fare), output[:id], 'Object ID incorrectly encoded'
|
15
|
+
assert_equal fare_score(fare), output[:score],
|
16
|
+
'Score incorrectly computed'
|
17
|
+
enc_output
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_map_reduce
|
21
|
+
fare1 = @db.item 0
|
22
|
+
output1 = _test_map_fare fare1
|
23
|
+
fare2 = @db.item 1
|
24
|
+
output2 = _test_map_fare fare2
|
25
|
+
|
26
|
+
win_fare = (fare_score(fare1) > fare_score(fare2)) ? fare1 : fare2
|
27
|
+
# Try both permutations to ensure all branches of the reduce code work.
|
28
|
+
[[output1, output2], [output2, output1]].each do |o1, o2|
|
29
|
+
enc_output = @client_query.reducer.reduce_outputs o1, o2, $tem
|
30
|
+
output = @client_query.unpack_output enc_output
|
31
|
+
assert_equal fare_id(win_fare), output[:id], 'The wrong fare won (bad ID)'
|
32
|
+
assert_equal fare_score(win_fare), output[:score],
|
33
|
+
'The wrong fare won (bad score)'
|
34
|
+
assert_equal [1, 2, 3], output[:check], 'Incorrect check bytes'
|
35
|
+
|
36
|
+
assert_not_equal enc_output, output1, 'Nonce fail'
|
37
|
+
assert_not_equal enc_output, output2, 'Nonce fail'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|