fluent-plugin-droonga 0.9.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.dir-locals.el +3 -0
- data/.travis.yml +6 -2
- data/README.md +6 -7
- data/Rakefile +23 -6
- data/fluent-plugin-droonga.gemspec +2 -2
- data/lib/droonga/adapter.rb +12 -3
- data/lib/droonga/adapter_runner.rb +28 -23
- data/lib/droonga/catalog/base.rb +7 -111
- data/lib/droonga/catalog/dataset.rb +13 -25
- data/lib/droonga/catalog/errors.rb +94 -0
- data/lib/droonga/catalog/schema.rb +277 -0
- data/lib/droonga/catalog/version1.rb +404 -0
- data/lib/droonga/catalog/version2.rb +160 -0
- data/lib/droonga/catalog_loader.rb +27 -4
- data/lib/droonga/catalog_observer.rb +44 -6
- data/lib/droonga/collector.rb +12 -10
- data/lib/droonga/{handler_plugin.rb → collector_message.rb} +47 -20
- data/lib/droonga/collector_runner.rb +64 -0
- data/lib/droonga/collectors.rb +18 -0
- data/lib/droonga/{catalog.rb → collectors/add.rb} +9 -7
- data/lib/droonga/{command_repository.rb → collectors/and.rb} +7 -14
- data/lib/droonga/collectors/sum.rb +26 -0
- data/lib/droonga/dispatcher.rb +74 -41
- data/lib/droonga/distributed_command_planner.rb +2 -2
- data/lib/droonga/engine.rb +13 -5
- data/lib/droonga/{message_processing_error.rb → error.rb} +33 -12
- data/lib/droonga/{plugin/planner/search.rb → error_messages.rb} +12 -10
- data/lib/droonga/farm.rb +15 -14
- data/lib/droonga/fluent_message_sender.rb +15 -11
- data/lib/droonga/forwarder.rb +22 -18
- data/lib/droonga/handler.rb +8 -2
- data/lib/droonga/handler_runner.rb +47 -26
- data/lib/droonga/input_message.rb +6 -6
- data/lib/droonga/{command.rb → loggable.rb} +7 -14
- data/lib/droonga/logger.rb +56 -15
- data/lib/droonga/message_matcher.rb +12 -7
- data/lib/droonga/message_pusher.rb +8 -4
- data/lib/droonga/message_receiver.rb +11 -9
- data/lib/droonga/output_message.rb +2 -0
- data/lib/droonga/planner.rb +21 -10
- data/lib/droonga/plugin.rb +15 -0
- data/lib/droonga/plugin/metadata/{adapter_message.rb → adapter_input_message.rb} +6 -14
- data/lib/droonga/plugin/metadata/adapter_output_message.rb +39 -0
- data/lib/droonga/plugin/metadata/collector_message.rb +39 -0
- data/lib/droonga/plugin/metadata/input_message.rb +15 -0
- data/lib/droonga/plugin_loader.rb +33 -25
- data/lib/droonga/plugin_registry.rb +9 -1
- data/lib/droonga/plugins/basic.rb +54 -0
- data/lib/droonga/plugins/crud.rb +36 -15
- data/lib/droonga/plugins/error.rb +5 -4
- data/lib/droonga/plugins/groonga.rb +9 -6
- data/lib/droonga/plugins/groonga/column_create.rb +10 -5
- data/lib/droonga/plugins/groonga/generic_command.rb +2 -8
- data/lib/droonga/plugins/groonga/generic_response.rb +2 -2
- data/lib/droonga/plugins/groonga/select.rb +2 -2
- data/lib/droonga/plugins/groonga/table_create.rb +9 -4
- data/lib/droonga/plugins/groonga/table_remove.rb +10 -5
- data/lib/droonga/plugins/search.rb +106 -5
- data/lib/droonga/plugins/search/distributed_search_planner.rb +398 -0
- data/lib/droonga/plugins/watch.rb +41 -20
- data/lib/droonga/processor.rb +12 -9
- data/lib/droonga/{plugin/collector/basic.rb → reducer.rb} +36 -50
- data/lib/droonga/replier.rb +7 -4
- data/lib/droonga/searcher.rb +40 -37
- data/lib/droonga/server.rb +8 -6
- data/lib/droonga/session.rb +17 -7
- data/lib/droonga/single_step.rb +53 -0
- data/lib/droonga/{plugin/planner/watch.rb → single_step_definition.rb} +27 -26
- data/lib/droonga/{partition.rb → slice.rb} +23 -12
- data/lib/droonga/status_code.rb +25 -0
- data/lib/droonga/step_runner.rb +63 -0
- data/lib/droonga/watch_schema.rb +7 -3
- data/lib/droonga/watcher.rb +4 -4
- data/lib/droonga/worker.rb +6 -6
- data/lib/fluent/plugin/out_droonga.rb +27 -2
- data/sample/cluster/catalog.json +33 -32
- data/test/command/config/default/catalog.json +72 -45
- data/test/command/config/version1/catalog.json +68 -0
- data/test/command/config/version1/fluentd.conf +11 -0
- data/test/command/suite/message/error/missing-dataset.expected +1 -1
- data/test/command/suite/message/error/unknown-dataset.expected +1 -1
- data/test/command/suite/message/error/unknown-type.expected +13 -0
- data/test/command/suite/message/error/{unknown-command.test → unknown-type.test} +1 -1
- data/test/command/suite/search/error/missing-source-parameter.expected +1 -1
- data/test/command/suite/search/error/unknown-source.expected +15 -3
- data/test/command/suite/watch/subscribe.expected +1 -3
- data/test/command/suite/watch/unsubscribe.expected +1 -3
- data/test/performance/watch/catalog.json +1 -0
- data/test/unit/catalog/test_dataset.rb +16 -358
- data/test/unit/catalog/test_schema.rb +285 -0
- data/test/unit/catalog/test_version1.rb +222 -28
- data/test/unit/catalog/test_version2.rb +155 -0
- data/test/unit/fixtures/catalog/version2.json +62 -0
- data/test/unit/helper/distributed_search_planner_helper.rb +2 -2
- data/test/unit/plugins/crud/test_add.rb +13 -13
- data/test/unit/plugins/groonga/test_column_create.rb +14 -11
- data/test/unit/plugins/groonga/test_table_create.rb +4 -9
- data/test/unit/plugins/groonga/test_table_remove.rb +4 -9
- data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_basic.rb +0 -0
- data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_group_by.rb +0 -0
- data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_output.rb +0 -0
- data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_sort_by.rb +0 -0
- data/test/unit/{plugin/collector/test_search.rb → plugins/search/test_collector.rb} +40 -39
- data/test/unit/plugins/{test_search.rb → search/test_handler.rb} +6 -5
- data/test/unit/{plugin/planner/test_search.rb → plugins/search/test_planner.rb} +3 -3
- data/test/unit/{plugin/collector → plugins}/test_basic.rb +68 -50
- data/test/unit/plugins/test_groonga.rb +2 -15
- data/test/unit/plugins/test_watch.rb +25 -22
- data/test/unit/test_message_matcher.rb +29 -6
- data/test/unit/test_output.rb +4 -0
- metadata +58 -50
- data/lib/droonga/collector_plugin.rb +0 -50
- data/lib/droonga/legacy_pluggable.rb +0 -66
- data/lib/droonga/legacy_plugin.rb +0 -57
- data/lib/droonga/legacy_plugin_repository.rb +0 -54
- data/lib/droonga/planner_plugin.rb +0 -54
- data/lib/droonga/plugin/collector/search.rb +0 -98
- data/lib/droonga/plugin/planner/crud.rb +0 -49
- data/lib/droonga/plugin/planner/distributed_search_planner.rb +0 -393
- data/lib/droonga/plugin/planner/groonga.rb +0 -54
- data/lib/droonga/plugin_registerable.rb +0 -75
- data/test/command/suite/message/error/unknown-command.expected +0 -13
- data/test/unit/test_command_repository.rb +0 -39
- data/test/unit/test_legacy_plugin.rb +0 -50
- data/test/unit/test_legacy_plugin_repository.rb +0 -89
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Copyright (C) 2014 Droonga Project
|
|
2
|
+
#
|
|
3
|
+
# This library is free software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
5
|
+
# License version 2.1 as published by the Free Software Foundation.
|
|
6
|
+
#
|
|
7
|
+
# This library is distributed in the hope that it will be useful,
|
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
10
|
+
# Lesser General Public License for more details.
|
|
11
|
+
#
|
|
12
|
+
# You should have received a copy of the GNU Lesser General Public
|
|
13
|
+
# License along with this library; if not, write to the Free Software
|
|
14
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
15
|
+
|
|
16
|
+
require "droonga/error"
|
|
17
|
+
|
|
18
|
+
module Droonga
|
|
19
|
+
module Catalog
|
|
20
|
+
class ValidationError < Error
|
|
21
|
+
def initialize(message, path)
|
|
22
|
+
if path
|
|
23
|
+
super("[Validation Error <#{path}>]#{message}")
|
|
24
|
+
else
|
|
25
|
+
super(message)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class MissingRequiredParameter < ValidationError
|
|
31
|
+
def initialize(name, path)
|
|
32
|
+
super("[#{name}] A required parameter is missing.", path)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class MismatchedParameterType < ValidationError
|
|
37
|
+
def initialize(name, expected_types, actual, path)
|
|
38
|
+
expected_types = [expected_types] unless expected_types.is_a?(Array)
|
|
39
|
+
message = nil
|
|
40
|
+
if expected_types.size == 1
|
|
41
|
+
message = "[#{name}] Mismatched parameter type: " +
|
|
42
|
+
"expected=<#{expected_types.first}>, actual=<#{actual}>"
|
|
43
|
+
else
|
|
44
|
+
message = "[#{name}] Mismatched parameter type: " +
|
|
45
|
+
"expected=<#{expected_types.join(" or ")}>, " +
|
|
46
|
+
"actual=<#{actual}>"
|
|
47
|
+
end
|
|
48
|
+
super(message, path)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class InvalidDate < ValidationError
|
|
53
|
+
def initialize(name, value, path)
|
|
54
|
+
super("[#{name}] Invalid date string: <#{value}>", path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class NegativeNumber < ValidationError
|
|
59
|
+
def initialize(name, actual, path)
|
|
60
|
+
super("[#{name}] A positive number is expected, but <#{actual}>", path)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class SmallerThanOne < ValidationError
|
|
65
|
+
def initialize(name, actual, path)
|
|
66
|
+
super("[#{name}] A number 1 or larger is expected, but <#{actual}>", path)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class FarmNotZoned < ValidationError
|
|
71
|
+
def initialize(name, zones, path)
|
|
72
|
+
super("The farm does not appear in zones: <#{name}>, zones=<#{zones}>", path)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class UnknownFarmInZones < ValidationError
|
|
77
|
+
def initialize(name, zones, path)
|
|
78
|
+
super("The farm is unknown: <#{name}>, zones=<#{zones}>", path)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class UnknownFarmForPartition < ValidationError
|
|
83
|
+
def initialize(name, slice, path)
|
|
84
|
+
super("The farm is unknown: <{#name}>, slice=<#{slice}>", path)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class UnsupportedValue < ValidationError
|
|
89
|
+
def initialize(name, value, path)
|
|
90
|
+
super("[#{name}] Not supported value: <#{value}>", path)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Copyright (C) 2014 Droonga Project
|
|
2
|
+
#
|
|
3
|
+
# This library is free software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
5
|
+
# License version 2.1 as published by the Free Software Foundation.
|
|
6
|
+
#
|
|
7
|
+
# This library is distributed in the hope that it will be useful,
|
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
10
|
+
# Lesser General Public License for more details.
|
|
11
|
+
#
|
|
12
|
+
# You should have received a copy of the GNU Lesser General Public
|
|
13
|
+
# License along with this library; if not, write to the Free Software
|
|
14
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
15
|
+
|
|
16
|
+
require "tsort"
|
|
17
|
+
|
|
18
|
+
module Droonga
|
|
19
|
+
module Catalog
|
|
20
|
+
class Schema
|
|
21
|
+
class ColumnIndexOptions
|
|
22
|
+
def initialize(data)
|
|
23
|
+
@data = data
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def section
|
|
27
|
+
@data["section"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def weight
|
|
31
|
+
@data["weight"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def position
|
|
35
|
+
@data["position"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def sources
|
|
39
|
+
@data["sources"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def flags
|
|
43
|
+
flags = []
|
|
44
|
+
flags << "WITH_SECTION" if section
|
|
45
|
+
flags << "WITH_WEIGHT" if weight
|
|
46
|
+
flags << "WITH_POSITION" if position
|
|
47
|
+
flags
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Column
|
|
52
|
+
attr_reader :table, :name, :data, :index_options
|
|
53
|
+
def initialize(table, name, data)
|
|
54
|
+
@table = table
|
|
55
|
+
@name = name
|
|
56
|
+
@data = data
|
|
57
|
+
@index_options = ColumnIndexOptions.new(index_options_data)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ==(other)
|
|
61
|
+
self.class == other.class and
|
|
62
|
+
name == other.name and
|
|
63
|
+
data == other.data
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def type
|
|
67
|
+
@data["type"] || "Scalar"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def type_flag
|
|
71
|
+
case type
|
|
72
|
+
when "Scalar"
|
|
73
|
+
"COLUMN_SCALAR"
|
|
74
|
+
when "Vector"
|
|
75
|
+
"COLUMN_VECTOR"
|
|
76
|
+
when "Index"
|
|
77
|
+
"COLUMN_INDEX"
|
|
78
|
+
else
|
|
79
|
+
# TODO raise appropriate error
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def flags
|
|
84
|
+
[type_flag] + index_options.flags
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def value_type
|
|
88
|
+
@data["valueType"]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def value_type_groonga
|
|
92
|
+
if value_type == "Integer"
|
|
93
|
+
"Int64"
|
|
94
|
+
else
|
|
95
|
+
value_type
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def to_column_create_body
|
|
100
|
+
body = {
|
|
101
|
+
"name" => name,
|
|
102
|
+
"table" => table,
|
|
103
|
+
"flags" => flags.join("|"),
|
|
104
|
+
"type" => value_type_groonga
|
|
105
|
+
}
|
|
106
|
+
sources = index_options.sources
|
|
107
|
+
if sources
|
|
108
|
+
body["source"] = sources.join(",")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
body
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
def index_options_data
|
|
116
|
+
@data["indexOptions"] || {}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class Table
|
|
121
|
+
attr_reader :name, :columns, :data
|
|
122
|
+
def initialize(name, data)
|
|
123
|
+
@name = name
|
|
124
|
+
@data = data
|
|
125
|
+
@columns = {}
|
|
126
|
+
|
|
127
|
+
columns_data.each do |column_name, column_data|
|
|
128
|
+
@columns[column_name] = Column.new(name, column_name, column_data)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def ==(other)
|
|
133
|
+
self.class == other.class and
|
|
134
|
+
name == other.name and
|
|
135
|
+
data == other.data
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def type
|
|
139
|
+
@data["type"] || "Hash"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def key_type
|
|
143
|
+
@data["keyType"]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def key_type_groonga
|
|
147
|
+
case key_type
|
|
148
|
+
when "Integer"
|
|
149
|
+
"Int64"
|
|
150
|
+
when "Float", "Time", "ShortText", "TokyoGeoPoint", "WGS84GeoPoint"
|
|
151
|
+
key_type
|
|
152
|
+
else
|
|
153
|
+
# TODO raise appropriate error
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def tokenizer
|
|
158
|
+
@data["tokenizer"]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def normalizer
|
|
162
|
+
@data["normalizer"]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def type_flag
|
|
166
|
+
case type
|
|
167
|
+
when "Array"
|
|
168
|
+
"TABLE_NO_KEY"
|
|
169
|
+
when "Hash"
|
|
170
|
+
"TABLE_HASH_KEY"
|
|
171
|
+
when "PatriciaTrie"
|
|
172
|
+
"TABLE_PAT_KEY"
|
|
173
|
+
when "DoubleArrayTrie"
|
|
174
|
+
"TABLE_DAT_KEY"
|
|
175
|
+
else
|
|
176
|
+
# TODO raise appropriate error
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def flags
|
|
181
|
+
[type_flag]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def to_table_create_body
|
|
185
|
+
body = {
|
|
186
|
+
"name" => name,
|
|
187
|
+
"key_type" => key_type_groonga,
|
|
188
|
+
"flags" => flags.join("|")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if tokenizer
|
|
192
|
+
body["default_tokenizer"] = tokenizer
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if normalizer
|
|
196
|
+
body["normalizer"] = normalizer
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
body
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
def columns_data
|
|
204
|
+
@data["columns"] || []
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
class ColumnCreateSorter
|
|
209
|
+
include TSort
|
|
210
|
+
|
|
211
|
+
def initialize(tables)
|
|
212
|
+
@tables = tables
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def all_columns
|
|
216
|
+
@tables.values.collect {|table| table.columns.values}.flatten
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def tsort_each_node(&block)
|
|
220
|
+
all_columns.each(&block)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def tsort_each_child(column, &block)
|
|
224
|
+
dependent_column_names = column.index_options.sources || []
|
|
225
|
+
dependent_column_names -= ["_key"] # _key always exists after the table created
|
|
226
|
+
reference_table = @tables[column.value_type_groonga]
|
|
227
|
+
# TODO when _key specified, check to ensure reference_table is not Array
|
|
228
|
+
dependent_columns = dependent_column_names.collect do |column_name|
|
|
229
|
+
reference_table.columns[column_name]
|
|
230
|
+
end
|
|
231
|
+
dependent_columns.each(&block)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
attr_reader :tables
|
|
236
|
+
def initialize(dataset_name, data)
|
|
237
|
+
@dataset_name = dataset_name
|
|
238
|
+
@data = data || []
|
|
239
|
+
@tables = {}
|
|
240
|
+
@data.each do |table_name, table_data|
|
|
241
|
+
@tables[table_name] = Table.new(table_name, table_data)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def to_messages
|
|
246
|
+
messages = []
|
|
247
|
+
|
|
248
|
+
tables.each do |name, table|
|
|
249
|
+
messages << {
|
|
250
|
+
"type" => "table_create",
|
|
251
|
+
"dataset" => @dataset_name,
|
|
252
|
+
"body" => table.to_table_create_body
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
sorter = ColumnCreateSorter.new(tables)
|
|
257
|
+
columns = sorter.tsort
|
|
258
|
+
# TODO handle TSort::Cyclic
|
|
259
|
+
|
|
260
|
+
columns.each do |column|
|
|
261
|
+
messages << {
|
|
262
|
+
"type" => "column_create",
|
|
263
|
+
"dataset" => @dataset_name,
|
|
264
|
+
"body" => column.to_column_create_body
|
|
265
|
+
}
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
messages
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def ==(other)
|
|
272
|
+
self.class == other.class and
|
|
273
|
+
tables == other.tables
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -14,10 +14,414 @@
|
|
|
14
14
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
15
15
|
|
|
16
16
|
require "droonga/catalog/base"
|
|
17
|
+
require "droonga/catalog/dataset"
|
|
17
18
|
|
|
18
19
|
module Droonga
|
|
19
20
|
module Catalog
|
|
20
21
|
class Version1 < Base
|
|
22
|
+
def initialize(data, path)
|
|
23
|
+
super
|
|
24
|
+
@errors = []
|
|
25
|
+
|
|
26
|
+
validate
|
|
27
|
+
raise MultiplexError.new(@errors) unless @errors.empty?
|
|
28
|
+
|
|
29
|
+
prepare_data
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def datasets
|
|
33
|
+
@datasets
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def slices(name)
|
|
37
|
+
get_partitions(name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_partitions(name)
|
|
41
|
+
device = @data["farms"][name]["device"]
|
|
42
|
+
pattern = Regexp.new("^#{name}\.")
|
|
43
|
+
results = {}
|
|
44
|
+
@data["datasets"].each do |dataset_name, dataset_data|
|
|
45
|
+
dataset = Dataset.new(dataset_name, dataset_data)
|
|
46
|
+
workers = dataset["workers"]
|
|
47
|
+
plugins = dataset["plugins"]
|
|
48
|
+
dataset["ring"].each do |key, part|
|
|
49
|
+
part["partitions"].each do |range, partitions|
|
|
50
|
+
partitions.each do |partition|
|
|
51
|
+
if partition =~ pattern
|
|
52
|
+
path = File.join([device, $POSTMATCH, "db"])
|
|
53
|
+
path = File.expand_path(path, base_path)
|
|
54
|
+
options = {
|
|
55
|
+
:dataset => dataset_name,
|
|
56
|
+
:database => path,
|
|
57
|
+
:n_workers => workers,
|
|
58
|
+
:plugins => plugins
|
|
59
|
+
}
|
|
60
|
+
results[partition] = options
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
return results
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def get_routes(name, args)
|
|
70
|
+
routes = []
|
|
71
|
+
dataset = dataset(name)
|
|
72
|
+
case args["type"]
|
|
73
|
+
when "broadcast"
|
|
74
|
+
dataset["ring"].each do |key, partition|
|
|
75
|
+
select_range_and_replicas(partition, args, routes)
|
|
76
|
+
end
|
|
77
|
+
when "scatter"
|
|
78
|
+
name = get_partition(dataset, args["key"])
|
|
79
|
+
partition = dataset["ring"][name]
|
|
80
|
+
select_range_and_replicas(partition, args, routes)
|
|
81
|
+
end
|
|
82
|
+
return routes
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get_partition(dataset, key)
|
|
86
|
+
continuum = dataset["continuum"]
|
|
87
|
+
return dataset["ring"].keys[0] unless continuum
|
|
88
|
+
hash = Zlib.crc32(key)
|
|
89
|
+
min = 0
|
|
90
|
+
max = continuum.size - 1
|
|
91
|
+
while (min < max) do
|
|
92
|
+
index = (min + max) / 2
|
|
93
|
+
value, key = continuum[index]
|
|
94
|
+
return key if value == hash
|
|
95
|
+
if value > hash
|
|
96
|
+
max = index
|
|
97
|
+
else
|
|
98
|
+
min = index + 1
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
return continuum[max][1]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def select_range_and_replicas(partition, args, routes)
|
|
105
|
+
date_range = args["date_range"] || 0..-1
|
|
106
|
+
partition["partitions"].sort[date_range].each do |time, replicas|
|
|
107
|
+
case args["replica"]
|
|
108
|
+
when "top"
|
|
109
|
+
routes << replicas[0]
|
|
110
|
+
when "random"
|
|
111
|
+
routes << replicas[rand(replicas.size)]
|
|
112
|
+
when "all"
|
|
113
|
+
routes.concat(replicas)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
def prepare_data
|
|
120
|
+
@datasets = {}
|
|
121
|
+
@data["datasets"].each do |name, dataset|
|
|
122
|
+
@datasets[name] = Dataset.new(name, dataset)
|
|
123
|
+
number_of_partitions = dataset["number_of_partitions"]
|
|
124
|
+
next if number_of_partitions < 2
|
|
125
|
+
total_weight = compute_total_weight(dataset)
|
|
126
|
+
continuum = []
|
|
127
|
+
dataset["ring"].each do |key, value|
|
|
128
|
+
points = number_of_partitions * 160 * value["weight"] / total_weight
|
|
129
|
+
points.times do |point|
|
|
130
|
+
hash = Digest::SHA1.hexdigest("#{key}:#{point}")
|
|
131
|
+
continuum << [hash[0..7].to_i(16), key]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
dataset["continuum"] = continuum.sort do |a, b| a[0] - b[0]; end
|
|
135
|
+
end
|
|
136
|
+
@options = @data["options"] || {}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def compute_total_weight(dataset)
|
|
140
|
+
dataset["ring"].reduce(0) do |result, zone|
|
|
141
|
+
result + zone[1]["weight"]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def validate
|
|
146
|
+
do_validation do
|
|
147
|
+
validate_effective_date
|
|
148
|
+
end
|
|
149
|
+
do_validation do
|
|
150
|
+
validate_farms
|
|
151
|
+
end
|
|
152
|
+
do_validation do
|
|
153
|
+
validate_zones
|
|
154
|
+
end
|
|
155
|
+
do_validation do
|
|
156
|
+
validate_datasets
|
|
157
|
+
end
|
|
158
|
+
do_validation do
|
|
159
|
+
validate_zone_relations
|
|
160
|
+
end
|
|
161
|
+
do_validation do
|
|
162
|
+
validate_database_relations
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def do_validation(&block)
|
|
167
|
+
begin
|
|
168
|
+
yield
|
|
169
|
+
rescue ValidationError => error
|
|
170
|
+
@errors << error
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def validate_required_parameter(value, name)
|
|
175
|
+
raise MissingRequiredParameter.new(name, @path) unless value
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def validate_parameter_type(expected_types, value, name)
|
|
179
|
+
expected_types = [expected_types] unless expected_types.is_a?(Array)
|
|
180
|
+
|
|
181
|
+
if expected_types.any? do |type|
|
|
182
|
+
value.is_a?(type)
|
|
183
|
+
end
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
raise MismatchedParameterType.new(name,
|
|
188
|
+
expected_types,
|
|
189
|
+
value.class,
|
|
190
|
+
@path)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def validate_valid_datetime(value, name)
|
|
194
|
+
validate_required_parameter(value, name)
|
|
195
|
+
validate_parameter_type(String, value, name)
|
|
196
|
+
begin
|
|
197
|
+
Time.parse(value)
|
|
198
|
+
rescue ArgumentError => error
|
|
199
|
+
raise InvalidDate.new(name, value, @path)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def validate_positive_numeric_parameter(value, name)
|
|
204
|
+
validate_required_parameter(value, name)
|
|
205
|
+
validate_parameter_type(Numeric, value, name)
|
|
206
|
+
if value < 0
|
|
207
|
+
raise NegativeNumber.new(name, value, @path)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def validate_positive_integer_parameter(value, name)
|
|
212
|
+
validate_required_parameter(value, name)
|
|
213
|
+
validate_parameter_type(Integer, value, name)
|
|
214
|
+
if value < 0
|
|
215
|
+
raise NegativeNumber.new(name, value, @path)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def validate_one_or_larger_integer_parameter(value, name)
|
|
220
|
+
validate_required_parameter(value, name)
|
|
221
|
+
validate_parameter_type(Integer, value, name)
|
|
222
|
+
if value < 1
|
|
223
|
+
raise SmallerThanOne.new(name, value, @path)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def validate_effective_date
|
|
228
|
+
date = @data["effective_date"]
|
|
229
|
+
validate_required_parameter(date, "effective_date")
|
|
230
|
+
validate_valid_datetime(date, "effective_date")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def validate_farms
|
|
234
|
+
farms = @data["farms"]
|
|
235
|
+
|
|
236
|
+
validate_required_parameter(farms, "farms")
|
|
237
|
+
validate_parameter_type(Hash, farms, "farms")
|
|
238
|
+
|
|
239
|
+
farms.each do |key, value|
|
|
240
|
+
validate_farm(value, "farms.#{key}")
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def validate_farm(farm, name)
|
|
245
|
+
validate_parameter_type(Hash, farm, name)
|
|
246
|
+
|
|
247
|
+
validate_required_parameter(farm["device"], "#{name}.device")
|
|
248
|
+
validate_parameter_type(String, farm["device"], "#{name}.device")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def validate_zones
|
|
252
|
+
zones = @data["zones"]
|
|
253
|
+
|
|
254
|
+
validate_required_parameter(zones, "zones")
|
|
255
|
+
validate_parameter_type(Array, zones, "zones")
|
|
256
|
+
|
|
257
|
+
validate_zone(zones, "zones")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def validate_zone(zone, name)
|
|
261
|
+
case zone
|
|
262
|
+
when String
|
|
263
|
+
return
|
|
264
|
+
when Array
|
|
265
|
+
zone.each_with_index do |sub_zone, index|
|
|
266
|
+
validate_zone(sub_zone, "#{name}[#{index}]")
|
|
267
|
+
end
|
|
268
|
+
else
|
|
269
|
+
validate_parameter_type([String, Array], zone, name)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def validate_datasets
|
|
274
|
+
datasets = @data["datasets"]
|
|
275
|
+
|
|
276
|
+
validate_required_parameter(datasets, "datasets")
|
|
277
|
+
validate_parameter_type(Hash, datasets, "datasets")
|
|
278
|
+
|
|
279
|
+
datasets.each do |name, dataset|
|
|
280
|
+
validate_dataset(dataset, "datasets.#{name}")
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def validate_dataset(dataset, name)
|
|
285
|
+
validate_parameter_type(Hash, dataset, name)
|
|
286
|
+
|
|
287
|
+
do_validation do
|
|
288
|
+
validate_one_or_larger_integer_parameter(dataset["number_of_partitions"],
|
|
289
|
+
"#{name}.number_of_partitions")
|
|
290
|
+
end
|
|
291
|
+
do_validation do
|
|
292
|
+
validate_one_or_larger_integer_parameter(dataset["number_of_replicas"],
|
|
293
|
+
"#{name}.number_of_replicas")
|
|
294
|
+
end
|
|
295
|
+
do_validation do
|
|
296
|
+
validate_positive_integer_parameter(dataset["workers"],
|
|
297
|
+
"#{name}.workers")
|
|
298
|
+
end
|
|
299
|
+
do_validation do
|
|
300
|
+
validate_date_range(dataset["date_range"], "#{name}.date_range")
|
|
301
|
+
end
|
|
302
|
+
do_validation do
|
|
303
|
+
validate_partition_key(dataset["partition_key"],
|
|
304
|
+
"#{name}.partition_key")
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
do_validation do
|
|
308
|
+
ring = dataset["ring"]
|
|
309
|
+
validate_required_parameter(ring, "#{name}.ring")
|
|
310
|
+
validate_parameter_type(Hash, ring, "#{name}.ring")
|
|
311
|
+
ring.each do |key, value|
|
|
312
|
+
validate_ring(value, "#{name}.ring.#{key}")
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
do_validation do
|
|
317
|
+
validate_plugins(dataset["plugins"], "#{name}.plugins")
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def validate_date_range(value, name)
|
|
322
|
+
validate_required_parameter(value, name)
|
|
323
|
+
return if value == "infinity"
|
|
324
|
+
raise UnsupportedValue.new(name, value, @path)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def validate_partition_key(value, name)
|
|
328
|
+
validate_required_parameter(value, name)
|
|
329
|
+
validate_parameter_type(String, value, name)
|
|
330
|
+
return if value == "_key"
|
|
331
|
+
raise UnsupportedValue.new(name, value, @path)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def validate_ring(ring, name)
|
|
335
|
+
validate_parameter_type(Hash, ring, name)
|
|
336
|
+
|
|
337
|
+
do_validation do
|
|
338
|
+
validate_positive_numeric_parameter(ring["weight"], "#{name}.weight")
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
do_validation do
|
|
342
|
+
validate_parameter_type(Hash, ring["partitions"], "#{name}.partitions")
|
|
343
|
+
ring["partitions"].each do |key, value|
|
|
344
|
+
validate_partition(value, "#{name}.partitions.#{key}")
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def validate_partition(partition, name)
|
|
350
|
+
validate_parameter_type(Array, partition, name)
|
|
351
|
+
|
|
352
|
+
partition.each_with_index do |value, index|
|
|
353
|
+
do_validation do
|
|
354
|
+
validate_parameter_type(String, value, "#{name}[#{index}]")
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def validate_plugins(plugins, name)
|
|
360
|
+
return unless plugins
|
|
361
|
+
validate_required_parameter(plugins, name)
|
|
362
|
+
validate_parameter_type(Array, plugins, "#{name}.plugins")
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def validate_zone_relations
|
|
366
|
+
return unless @data["zones"].is_a?(Array)
|
|
367
|
+
return unless @data["farms"].is_a?(Hash)
|
|
368
|
+
|
|
369
|
+
farms = @data["farms"]
|
|
370
|
+
zones = @data["zones"]
|
|
371
|
+
|
|
372
|
+
all_farms = farms.keys
|
|
373
|
+
all_zones = zones.flatten
|
|
374
|
+
|
|
375
|
+
all_farms.each do |farm|
|
|
376
|
+
unless all_zones.include?(farm)
|
|
377
|
+
raise FarmNotZoned.new(farm, zones, @path)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
all_zones.each do |zone|
|
|
382
|
+
unless all_farms.include?(zone)
|
|
383
|
+
raise UnknownFarmInZones.new(farm, zones, @path)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def validate_database_relations
|
|
389
|
+
return unless @data["farms"]
|
|
390
|
+
|
|
391
|
+
farm_names = @data["farms"].keys.collect do |name|
|
|
392
|
+
Regexp.escape(name)
|
|
393
|
+
end
|
|
394
|
+
valid_farms_matcher = Regexp.new("^(#{farm_names.join("|")})\.")
|
|
395
|
+
|
|
396
|
+
@data["datasets"].each do |dataset_name, dataset|
|
|
397
|
+
ring = dataset["ring"]
|
|
398
|
+
next if ring.nil? or !ring.is_a?(Hash)
|
|
399
|
+
ring.each do |ring_key, part|
|
|
400
|
+
partitions_set = part["partitions"]
|
|
401
|
+
next if partitions_set.nil? or !partitions_set.is_a?(Hash)
|
|
402
|
+
partitions_set.each do |range, partitions|
|
|
403
|
+
next unless partitions.is_a?(Array)
|
|
404
|
+
partitions.each_with_index do |partition, index|
|
|
405
|
+
name = "datasets.#{dataset_name}.ring.#{ring_key}." +
|
|
406
|
+
"partitions.#{range}[#{index}]"
|
|
407
|
+
do_validation do
|
|
408
|
+
unless partition =~ valid_farms_matcher
|
|
409
|
+
raise UnknownFarmForPartition.new(name, partition, @path)
|
|
410
|
+
end
|
|
411
|
+
do_validation do
|
|
412
|
+
directory_name = $POSTMATCH
|
|
413
|
+
if directory_name.nil? or directory_name.empty?
|
|
414
|
+
message = "\"#{partition}\" has no database name. " +
|
|
415
|
+
"You mus specify a database name for \"#{name}\"."
|
|
416
|
+
raise ValidationError.new(message, @path)
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
21
425
|
end
|
|
22
426
|
end
|
|
23
427
|
end
|