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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.dir-locals.el +3 -0
  3. data/.travis.yml +6 -2
  4. data/README.md +6 -7
  5. data/Rakefile +23 -6
  6. data/fluent-plugin-droonga.gemspec +2 -2
  7. data/lib/droonga/adapter.rb +12 -3
  8. data/lib/droonga/adapter_runner.rb +28 -23
  9. data/lib/droonga/catalog/base.rb +7 -111
  10. data/lib/droonga/catalog/dataset.rb +13 -25
  11. data/lib/droonga/catalog/errors.rb +94 -0
  12. data/lib/droonga/catalog/schema.rb +277 -0
  13. data/lib/droonga/catalog/version1.rb +404 -0
  14. data/lib/droonga/catalog/version2.rb +160 -0
  15. data/lib/droonga/catalog_loader.rb +27 -4
  16. data/lib/droonga/catalog_observer.rb +44 -6
  17. data/lib/droonga/collector.rb +12 -10
  18. data/lib/droonga/{handler_plugin.rb → collector_message.rb} +47 -20
  19. data/lib/droonga/collector_runner.rb +64 -0
  20. data/lib/droonga/collectors.rb +18 -0
  21. data/lib/droonga/{catalog.rb → collectors/add.rb} +9 -7
  22. data/lib/droonga/{command_repository.rb → collectors/and.rb} +7 -14
  23. data/lib/droonga/collectors/sum.rb +26 -0
  24. data/lib/droonga/dispatcher.rb +74 -41
  25. data/lib/droonga/distributed_command_planner.rb +2 -2
  26. data/lib/droonga/engine.rb +13 -5
  27. data/lib/droonga/{message_processing_error.rb → error.rb} +33 -12
  28. data/lib/droonga/{plugin/planner/search.rb → error_messages.rb} +12 -10
  29. data/lib/droonga/farm.rb +15 -14
  30. data/lib/droonga/fluent_message_sender.rb +15 -11
  31. data/lib/droonga/forwarder.rb +22 -18
  32. data/lib/droonga/handler.rb +8 -2
  33. data/lib/droonga/handler_runner.rb +47 -26
  34. data/lib/droonga/input_message.rb +6 -6
  35. data/lib/droonga/{command.rb → loggable.rb} +7 -14
  36. data/lib/droonga/logger.rb +56 -15
  37. data/lib/droonga/message_matcher.rb +12 -7
  38. data/lib/droonga/message_pusher.rb +8 -4
  39. data/lib/droonga/message_receiver.rb +11 -9
  40. data/lib/droonga/output_message.rb +2 -0
  41. data/lib/droonga/planner.rb +21 -10
  42. data/lib/droonga/plugin.rb +15 -0
  43. data/lib/droonga/plugin/metadata/{adapter_message.rb → adapter_input_message.rb} +6 -14
  44. data/lib/droonga/plugin/metadata/adapter_output_message.rb +39 -0
  45. data/lib/droonga/plugin/metadata/collector_message.rb +39 -0
  46. data/lib/droonga/plugin/metadata/input_message.rb +15 -0
  47. data/lib/droonga/plugin_loader.rb +33 -25
  48. data/lib/droonga/plugin_registry.rb +9 -1
  49. data/lib/droonga/plugins/basic.rb +54 -0
  50. data/lib/droonga/plugins/crud.rb +36 -15
  51. data/lib/droonga/plugins/error.rb +5 -4
  52. data/lib/droonga/plugins/groonga.rb +9 -6
  53. data/lib/droonga/plugins/groonga/column_create.rb +10 -5
  54. data/lib/droonga/plugins/groonga/generic_command.rb +2 -8
  55. data/lib/droonga/plugins/groonga/generic_response.rb +2 -2
  56. data/lib/droonga/plugins/groonga/select.rb +2 -2
  57. data/lib/droonga/plugins/groonga/table_create.rb +9 -4
  58. data/lib/droonga/plugins/groonga/table_remove.rb +10 -5
  59. data/lib/droonga/plugins/search.rb +106 -5
  60. data/lib/droonga/plugins/search/distributed_search_planner.rb +398 -0
  61. data/lib/droonga/plugins/watch.rb +41 -20
  62. data/lib/droonga/processor.rb +12 -9
  63. data/lib/droonga/{plugin/collector/basic.rb → reducer.rb} +36 -50
  64. data/lib/droonga/replier.rb +7 -4
  65. data/lib/droonga/searcher.rb +40 -37
  66. data/lib/droonga/server.rb +8 -6
  67. data/lib/droonga/session.rb +17 -7
  68. data/lib/droonga/single_step.rb +53 -0
  69. data/lib/droonga/{plugin/planner/watch.rb → single_step_definition.rb} +27 -26
  70. data/lib/droonga/{partition.rb → slice.rb} +23 -12
  71. data/lib/droonga/status_code.rb +25 -0
  72. data/lib/droonga/step_runner.rb +63 -0
  73. data/lib/droonga/watch_schema.rb +7 -3
  74. data/lib/droonga/watcher.rb +4 -4
  75. data/lib/droonga/worker.rb +6 -6
  76. data/lib/fluent/plugin/out_droonga.rb +27 -2
  77. data/sample/cluster/catalog.json +33 -32
  78. data/test/command/config/default/catalog.json +72 -45
  79. data/test/command/config/version1/catalog.json +68 -0
  80. data/test/command/config/version1/fluentd.conf +11 -0
  81. data/test/command/suite/message/error/missing-dataset.expected +1 -1
  82. data/test/command/suite/message/error/unknown-dataset.expected +1 -1
  83. data/test/command/suite/message/error/unknown-type.expected +13 -0
  84. data/test/command/suite/message/error/{unknown-command.test → unknown-type.test} +1 -1
  85. data/test/command/suite/search/error/missing-source-parameter.expected +1 -1
  86. data/test/command/suite/search/error/unknown-source.expected +15 -3
  87. data/test/command/suite/watch/subscribe.expected +1 -3
  88. data/test/command/suite/watch/unsubscribe.expected +1 -3
  89. data/test/performance/watch/catalog.json +1 -0
  90. data/test/unit/catalog/test_dataset.rb +16 -358
  91. data/test/unit/catalog/test_schema.rb +285 -0
  92. data/test/unit/catalog/test_version1.rb +222 -28
  93. data/test/unit/catalog/test_version2.rb +155 -0
  94. data/test/unit/fixtures/catalog/version2.json +62 -0
  95. data/test/unit/helper/distributed_search_planner_helper.rb +2 -2
  96. data/test/unit/plugins/crud/test_add.rb +13 -13
  97. data/test/unit/plugins/groonga/test_column_create.rb +14 -11
  98. data/test/unit/plugins/groonga/test_table_create.rb +4 -9
  99. data/test/unit/plugins/groonga/test_table_remove.rb +4 -9
  100. data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_basic.rb +0 -0
  101. data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_group_by.rb +0 -0
  102. data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_output.rb +0 -0
  103. data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_sort_by.rb +0 -0
  104. data/test/unit/{plugin/collector/test_search.rb → plugins/search/test_collector.rb} +40 -39
  105. data/test/unit/plugins/{test_search.rb → search/test_handler.rb} +6 -5
  106. data/test/unit/{plugin/planner/test_search.rb → plugins/search/test_planner.rb} +3 -3
  107. data/test/unit/{plugin/collector → plugins}/test_basic.rb +68 -50
  108. data/test/unit/plugins/test_groonga.rb +2 -15
  109. data/test/unit/plugins/test_watch.rb +25 -22
  110. data/test/unit/test_message_matcher.rb +29 -6
  111. data/test/unit/test_output.rb +4 -0
  112. metadata +58 -50
  113. data/lib/droonga/collector_plugin.rb +0 -50
  114. data/lib/droonga/legacy_pluggable.rb +0 -66
  115. data/lib/droonga/legacy_plugin.rb +0 -57
  116. data/lib/droonga/legacy_plugin_repository.rb +0 -54
  117. data/lib/droonga/planner_plugin.rb +0 -54
  118. data/lib/droonga/plugin/collector/search.rb +0 -98
  119. data/lib/droonga/plugin/planner/crud.rb +0 -49
  120. data/lib/droonga/plugin/planner/distributed_search_planner.rb +0 -393
  121. data/lib/droonga/plugin/planner/groonga.rb +0 -54
  122. data/lib/droonga/plugin_registerable.rb +0 -75
  123. data/test/command/suite/message/error/unknown-command.expected +0 -13
  124. data/test/unit/test_command_repository.rb +0 -39
  125. data/test/unit/test_legacy_plugin.rb +0 -50
  126. 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