fluent-plugin-droonga 0.9.0 → 0.9.9

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/Gemfile +8 -1
  4. data/fluent-plugin-droonga.gemspec +2 -2
  5. data/lib/droonga/adapter.rb +39 -0
  6. data/lib/droonga/adapter_runner.rb +99 -0
  7. data/lib/droonga/catalog/base.rb +11 -11
  8. data/lib/droonga/catalog/dataset.rb +54 -0
  9. data/lib/droonga/catalog/version1.rb +1 -1
  10. data/lib/droonga/collector.rb +5 -7
  11. data/lib/droonga/collector_plugin.rb +7 -7
  12. data/lib/droonga/command.rb +36 -0
  13. data/lib/droonga/{plugin/input_adapter/crud.rb → command_repository.rb} +14 -8
  14. data/lib/droonga/dispatcher.rb +86 -54
  15. data/lib/droonga/distributed_command_planner.rb +183 -0
  16. data/lib/droonga/distributor.rb +43 -17
  17. data/lib/droonga/handler.rb +13 -72
  18. data/lib/droonga/handler_message.rb +5 -5
  19. data/lib/droonga/handler_messenger.rb +4 -1
  20. data/lib/droonga/handler_plugin.rb +2 -2
  21. data/lib/droonga/handler_runner.rb +104 -0
  22. data/lib/droonga/input_message.rb +4 -4
  23. data/lib/droonga/legacy_pluggable.rb +66 -0
  24. data/lib/droonga/{input_adapter.rb → legacy_plugin.rb} +27 -22
  25. data/lib/droonga/{plugin_repository.rb → legacy_plugin_repository.rb} +2 -4
  26. data/lib/droonga/message_matcher.rb +101 -0
  27. data/lib/droonga/{input_adapter_plugin.rb → planner.rb} +14 -10
  28. data/lib/droonga/planner_plugin.rb +54 -0
  29. data/lib/droonga/pluggable.rb +9 -45
  30. data/lib/droonga/plugin.rb +9 -33
  31. data/lib/droonga/plugin/collector/basic.rb +2 -0
  32. data/lib/droonga/plugin/collector/search.rb +31 -37
  33. data/lib/droonga/plugin/{handler/groonga/table_remove.rb → metadata/adapter_message.rb} +23 -18
  34. data/lib/droonga/plugin/{handler/search.rb → metadata/handler_action.rb} +19 -15
  35. data/lib/droonga/plugin/metadata/input_message.rb +39 -0
  36. data/lib/droonga/plugin/planner/crud.rb +49 -0
  37. data/lib/droonga/plugin/{distributor → planner}/distributed_search_planner.rb +62 -70
  38. data/lib/droonga/plugin/{distributor → planner}/groonga.rb +11 -32
  39. data/lib/droonga/plugin/{distributor → planner}/search.rb +5 -5
  40. data/lib/droonga/plugin/{distributor → planner}/watch.rb +15 -6
  41. data/lib/droonga/plugin_loader.rb +10 -0
  42. data/lib/droonga/plugin_registerable.rb +34 -10
  43. data/lib/droonga/plugin_registry.rb +58 -0
  44. data/lib/droonga/plugins/crud.rb +124 -0
  45. data/lib/droonga/plugins/error.rb +50 -0
  46. data/lib/droonga/{output_adapter_plugin.rb → plugins/groonga.rb} +9 -13
  47. data/lib/droonga/plugins/groonga/column_create.rb +123 -0
  48. data/lib/droonga/plugins/groonga/generic_command.rb +65 -0
  49. data/lib/droonga/{plugin/output_adapter/groonga.rb → plugins/groonga/generic_response.rb} +16 -15
  50. data/lib/droonga/plugins/groonga/select.rb +124 -0
  51. data/lib/droonga/plugins/groonga/table_create.rb +106 -0
  52. data/lib/droonga/plugins/groonga/table_remove.rb +57 -0
  53. data/lib/droonga/plugins/search.rb +40 -0
  54. data/lib/droonga/plugins/watch.rb +156 -0
  55. data/lib/droonga/processor.rb +8 -10
  56. data/lib/droonga/searcher.rb +14 -4
  57. data/lib/droonga/searcher/mecab_filter.rb +67 -0
  58. data/lib/droonga/session.rb +5 -5
  59. data/lib/droonga/test.rb +1 -1
  60. data/lib/droonga/test/stub_handler_message.rb +1 -1
  61. data/lib/droonga/test/{stub_distributor.rb → stub_planner.rb} +1 -1
  62. data/lib/droonga/worker.rb +7 -8
  63. data/lib/fluent/plugin/out_droonga.rb +0 -1
  64. data/sample/cluster/catalog.json +2 -4
  65. data/sample/mecab_filter/data.grn +7 -0
  66. data/sample/mecab_filter/ddl.grn +7 -0
  67. data/sample/mecab_filter/search_with_mecab_filter.json +21 -0
  68. data/sample/mecab_filter/search_without_mecab_filter.json +21 -0
  69. data/test/command/config/default/catalog.json +2 -5
  70. data/test/command/suite/search/error/no-query.expected +13 -0
  71. data/test/command/suite/search/error/no-query.test +7 -0
  72. data/test/command/suite/search/error/unknown-source.expected +26 -0
  73. data/test/command/suite/watch/subscribe.expected +3 -3
  74. data/test/command/suite/watch/unsubscribe.expected +3 -3
  75. data/test/unit/catalog/test_dataset.rb +385 -0
  76. data/test/unit/catalog/test_version1.rb +111 -45
  77. data/test/unit/fixtures/catalog/version1.json +0 -3
  78. data/test/unit/helper.rb +2 -1
  79. data/test/unit/helper/distributed_search_planner_helper.rb +83 -0
  80. data/test/unit/plugin/collector/test_basic.rb +233 -376
  81. data/test/unit/plugin/collector/test_search.rb +8 -17
  82. data/test/unit/plugin/planner/search_planner/test_basic.rb +120 -0
  83. data/test/unit/plugin/planner/search_planner/test_group_by.rb +573 -0
  84. data/test/unit/plugin/planner/search_planner/test_output.rb +388 -0
  85. data/test/unit/plugin/planner/search_planner/test_sort_by.rb +938 -0
  86. data/test/unit/plugin/{distributor → planner}/test_search.rb +20 -75
  87. data/test/unit/{plugin/handler → plugins/crud}/test_add.rb +11 -11
  88. data/test/unit/plugins/groonga/select/test_adapter_input.rb +213 -0
  89. data/test/unit/{plugin/output_adapter/groonga/test_select.rb → plugins/groonga/select/test_adapter_output.rb} +12 -13
  90. data/test/unit/{plugin/handler → plugins}/groonga/test_column_create.rb +20 -5
  91. data/test/unit/{plugin/handler → plugins}/groonga/test_table_create.rb +5 -0
  92. data/test/unit/{plugin/handler → plugins}/groonga/test_table_remove.rb +8 -1
  93. data/test/unit/{plugin/handler → plugins}/test_groonga.rb +5 -5
  94. data/test/unit/{plugin/handler → plugins}/test_search.rb +21 -5
  95. data/test/unit/{plugin/handler → plugins}/test_watch.rb +29 -10
  96. data/{lib/droonga/command_mapper.rb → test/unit/test_command_repository.rb} +16 -22
  97. data/test/unit/{test_plugin.rb → test_legacy_plugin.rb} +3 -3
  98. data/test/unit/{test_plugin_repository.rb → test_legacy_plugin_repository.rb} +3 -3
  99. data/test/unit/test_message_matcher.rb +137 -0
  100. metadata +86 -66
  101. data/bin/grn2jsons +0 -82
  102. data/lib/droonga/distribution_planner.rb +0 -76
  103. data/lib/droonga/distributor_plugin.rb +0 -95
  104. data/lib/droonga/output_adapter.rb +0 -53
  105. data/lib/droonga/plugin/collector/groonga.rb +0 -83
  106. data/lib/droonga/plugin/distributor/crud.rb +0 -84
  107. data/lib/droonga/plugin/handler/add.rb +0 -109
  108. data/lib/droonga/plugin/handler/forward.rb +0 -75
  109. data/lib/droonga/plugin/handler/groonga.rb +0 -99
  110. data/lib/droonga/plugin/handler/groonga/column_create.rb +0 -106
  111. data/lib/droonga/plugin/handler/groonga/table_create.rb +0 -91
  112. data/lib/droonga/plugin/handler/watch.rb +0 -108
  113. data/lib/droonga/plugin/input_adapter/groonga.rb +0 -49
  114. data/lib/droonga/plugin/input_adapter/groonga/select.rb +0 -63
  115. data/lib/droonga/plugin/output_adapter/crud.rb +0 -51
  116. data/lib/droonga/plugin/output_adapter/groonga/select.rb +0 -54
  117. data/lib/groonga_command_converter.rb +0 -143
  118. data/sample/fluentd.conf +0 -8
  119. data/test/unit/plugin/distributor/test_search_planner.rb +0 -1102
  120. data/test/unit/plugin/input_adapter/groonga/test_select.rb +0 -248
  121. data/test/unit/test_command_mapper.rb +0 -44
  122. data/test/unit/test_groonga_command_converter.rb +0 -242
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
- #
3
- # Copyright (C) 2013 Droonga Project
1
+ # Copyright (C) 2013-2014 Droonga Project
4
2
  #
5
3
  # This library is free software; you can redistribute it and/or
6
4
  # modify it under the terms of the GNU Lesser General Public
@@ -18,9 +16,8 @@
18
16
  require "English"
19
17
  require "tsort"
20
18
 
21
- require "droonga/input_adapter"
22
- require "droonga/output_adapter"
23
- require "droonga/distributor"
19
+ require "droonga/adapter_runner"
20
+ require "droonga/planner"
24
21
  require "droonga/catalog"
25
22
  require "droonga/collector"
26
23
  require "droonga/farm"
@@ -28,6 +25,7 @@ require "droonga/session"
28
25
  require "droonga/replier"
29
26
  require "droonga/message_processing_error"
30
27
  require "droonga/catalog_observer"
28
+ require "droonga/distributor"
31
29
 
32
30
  module Droonga
33
31
  class Dispatcher
@@ -39,6 +37,12 @@ module Droonga
39
37
  end
40
38
  end
41
39
 
40
+ class UnknownDataset < NotFound
41
+ def initialize(dataset)
42
+ super("The dataset #{dataset.inspect} does not exist.")
43
+ end
44
+ end
45
+
42
46
  class UnknownCommand < BadRequest
43
47
  def initialize(command, dataset)
44
48
  super("The command #{command.inspect} is not available " +
@@ -54,15 +58,14 @@ module Droonga
54
58
  @sessions = {}
55
59
  @current_id = 0
56
60
  @local = Regexp.new("^#{@name}")
57
- @input_adapter =
58
- InputAdapter.new(self, :plugins => Droonga.catalog.option("plugins"))
59
- @output_adapter =
60
- OutputAdapter.new(self, :plugins => Droonga.catalog.option("plugins"))
61
+ @adapter_runners = create_adapter_runners
61
62
  @farm = Farm.new(name, @loop, :dispatcher => self)
62
63
  @forwarder = Forwarder.new(@loop)
63
64
  @replier = Replier.new(@forwarder)
64
- @distributor = Distributor.new(self, @options)
65
- @collector = Collector.new
65
+ # TODO: make customizable
66
+ @planner = Planner.new(self, ["search", "crud", "groonga", "watch"])
67
+ # TODO: make customizable
68
+ @collector = Collector.new(["basic", "search"])
66
69
  end
67
70
 
68
71
  def start
@@ -75,10 +78,11 @@ module Droonga
75
78
 
76
79
  def shutdown
77
80
  @forwarder.shutdown
78
- @distributor.shutdown
81
+ @planner.shutdown
79
82
  @collector.shutdown
80
- @input_adapter.shutdown
81
- @output_adapter.shutdown
83
+ @adapter_runners.each_value do |adapter_runner|
84
+ adapter_runner.shutdown
85
+ end
82
86
  @farm.shutdown
83
87
  @loop.stop
84
88
  @loop_thread.join
@@ -90,11 +94,17 @@ module Droonga
90
94
  process_internal_message(message["body"])
91
95
  else
92
96
  begin
93
- assert_valid_message
97
+ assert_valid_message(message)
94
98
  process_input_message(message)
95
99
  rescue MessageProcessingError => error
96
100
  reply("statusCode" => error.status_code,
97
101
  "body" => error.response_body)
102
+ rescue => error
103
+ Logger.error("failed to process input message", error)
104
+ formatted_error = MessageProcessingError.new("Unknown internal error")
105
+ reply("statusCode" => formatted_error.status_code,
106
+ "body" => formatted_error.response_body)
107
+ raise error
98
108
  end
99
109
  end
100
110
  end
@@ -118,7 +128,11 @@ module Droonga
118
128
  #
119
129
  # @see Replier#reply
120
130
  def reply(message)
121
- adapted_message = @output_adapter.adapt(@message.merge(message))
131
+ adapted_message = @message.merge(message)
132
+ adapter_runner = @adapter_runners[adapted_message["dataset"]]
133
+ if adapter_runner
134
+ adapted_message = adapter_runner.adapt_output(adapted_message)
135
+ end
122
136
  return if adapted_message["replyTo"].nil?
123
137
  @replier.reply(adapted_message)
124
138
  end
@@ -129,10 +143,10 @@ module Droonga
129
143
  if session
130
144
  session.receive(message["input"], message["value"])
131
145
  else
132
- components = message["components"]
133
- if components
134
- planner = Planner.new(self, components)
135
- session = planner.create_session(id, @collector)
146
+ steps = message["steps"]
147
+ if steps
148
+ session_planner = SessionPlanner.new(self, steps)
149
+ session = session_planner.create_session(id, @collector)
136
150
  @sessions[id] = session
137
151
  else
138
152
  #todo: take cases receiving result before its query into account
@@ -152,23 +166,23 @@ module Droonga
152
166
  end
153
167
  end
154
168
 
155
- def dispatch_components(components)
169
+ def dispatch_steps(steps)
156
170
  id = generate_id
157
171
  destinations = {}
158
- components.each do |component|
159
- dataset = component["dataset"]
172
+ steps.each do |step|
173
+ dataset = step["dataset"]
160
174
  if dataset
161
- routes = Droonga.catalog.get_routes(dataset, component)
162
- component["routes"] = routes
175
+ routes = Droonga.catalog.get_routes(dataset, step)
176
+ step["routes"] = routes
163
177
  else
164
- component["routes"] ||= [id]
178
+ step["routes"] ||= [id]
165
179
  end
166
- routes = component["routes"]
180
+ routes = step["routes"]
167
181
  routes.each do |route|
168
182
  destinations[farm_path(route)] = true
169
183
  end
170
184
  end
171
- dispatch_message = { "id" => id, "components" => components }
185
+ dispatch_message = { "id" => id, "steps" => steps }
172
186
  destinations.each_key do |destination|
173
187
  dispatch(dispatch_message, destination)
174
188
  end
@@ -177,10 +191,10 @@ module Droonga
177
191
  def process_local_message(local_message)
178
192
  task = local_message["task"]
179
193
  partition_name = task["route"]
180
- component = task["component"]
181
- command = component["command"]
194
+ step = task["step"]
195
+ command = step["command"]
182
196
  descendants = {}
183
- component["descendants"].each do |name, routes|
197
+ step["descendants"].each do |name, routes|
184
198
  descendants[name] = routes.collect do |route|
185
199
  farm_path(route)
186
200
  end
@@ -211,43 +225,61 @@ module Droonga
211
225
  end
212
226
 
213
227
  def process_input_message(message)
214
- adapted_message = @input_adapter.adapt(message)
215
- @distributor.process(adapted_message["type"], adapted_message)
216
- rescue Droonga::Pluggable::UnknownPlugin => error
228
+ dataset = message["dataset"]
229
+ adapter_runner = @adapter_runners[dataset]
230
+ adapted_message = adapter_runner.adapt_input(message)
231
+ plan = @planner.process(adapted_message["type"], adapted_message)
232
+ distributor = Distributor.new(self)
233
+ distributor.distribute(plan)
234
+ rescue Droonga::LegacyPluggable::UnknownPlugin => error
217
235
  raise UnknownCommand.new(error.command, message["dataset"])
218
236
  end
219
237
 
220
- def assert_valid_message
221
- raise MissingDatasetParameter.new unless @message.include?("dataset")
238
+ def assert_valid_message(message)
239
+ unless message.key?("dataset")
240
+ raise MissingDatasetParameter.new
241
+ end
242
+ dataset = message["dataset"]
243
+ unless Droonga.catalog.have_dataset?(dataset)
244
+ raise UnknownDataset.new(dataset)
245
+ end
246
+ end
247
+
248
+ def create_adapter_runners
249
+ runners = {}
250
+ Droonga.catalog.datasets.each do |name, configuration|
251
+ runners[name] = AdapterRunner.new(self, configuration["plugins"] || [])
252
+ end
253
+ runners
222
254
  end
223
255
 
224
256
  def log_tag
225
257
  "[#{Process.ppid}][#{Process.pid}] dispatcher"
226
258
  end
227
259
 
228
- class Planner
229
- attr_reader :components
260
+ class SessionPlanner
261
+ attr_reader :steps
230
262
 
231
- def initialize(dispatcher, components)
263
+ def initialize(dispatcher, steps)
232
264
  @dispatcher = dispatcher
233
- @components = components
265
+ @steps = steps
234
266
  end
235
267
 
236
268
  def create_session(id, collector)
237
269
  resolve_descendants
238
270
  tasks = []
239
271
  inputs = {}
240
- @components.each do |component|
241
- component["routes"].each do |route|
272
+ @steps.each do |step|
273
+ step["routes"].each do |route|
242
274
  next unless @dispatcher.local?(route)
243
275
  task = {
244
276
  "route" => route,
245
- "component" => component,
277
+ "step" => step,
246
278
  "n_of_inputs" => 0,
247
279
  "values" => {}
248
280
  }
249
281
  tasks << task
250
- (component["inputs"] || [nil]).each do |input|
282
+ (step["inputs"] || [nil]).each do |input|
251
283
  inputs[input] ||= []
252
284
  inputs[input] << task
253
285
  end
@@ -258,24 +290,24 @@ module Droonga
258
290
 
259
291
  def resolve_descendants
260
292
  @descendants = {}
261
- @components.size.times do |index|
262
- component = @components[index]
263
- (component["inputs"] || []).each do |input|
293
+ @steps.size.times do |index|
294
+ step = @steps[index]
295
+ (step["inputs"] || []).each do |input|
264
296
  @descendants[input] ||= []
265
297
  @descendants[input] << index
266
298
  end
267
- component["n_of_expects"] = 0
299
+ step["n_of_expects"] = 0
268
300
  end
269
- @components.each do |component|
301
+ @steps.each do |step|
270
302
  descendants = {}
271
- (component["outputs"] || []).each do |output|
303
+ (step["outputs"] || []).each do |output|
272
304
  descendants[output] = []
273
305
  @descendants[output].each do |index|
274
- @components[index]["n_of_expects"] += component["routes"].size
275
- descendants[output].concat(@components[index]["routes"])
306
+ @steps[index]["n_of_expects"] += step["routes"].size
307
+ descendants[output].concat(@steps[index]["routes"])
276
308
  end
277
309
  end
278
- component["descendants"] = descendants
310
+ step["descendants"] = descendants
279
311
  end
280
312
  end
281
313
  end
@@ -0,0 +1,183 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2014 Droonga Project
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License version 2.1 as published by the Free Software Foundation.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+
18
+ module Droonga
19
+ class DistributedCommandPlanner
20
+ attr_accessor :key, :dataset
21
+
22
+ REDUCE_SUM = "sum"
23
+
24
+ DEFAULT_LIMIT = -1
25
+
26
+ def initialize(source_message)
27
+ @source_message = source_message
28
+
29
+ @key = nil
30
+ @dataset = nil
31
+ @outputs = []
32
+
33
+ @reducers = []
34
+ @gatherers = []
35
+ @processor = nil
36
+
37
+ plan_errors_handling
38
+ end
39
+
40
+ def plan
41
+ unified_reducers + unified_gatherers + [fixed_processor]
42
+ end
43
+
44
+ def reduce(params=nil)
45
+ return unless params
46
+ params.each do |name, reducer|
47
+ gatherer = nil
48
+ if reducer.is_a?(Hash) && reducer[:gather]
49
+ gatherer = reducer[:gather]
50
+ reducer = reducer[:reduce]
51
+ end
52
+ @reducers << reducer_message(reduce_command, name, reducer)
53
+ @gatherers << gatherer_message(gather_command, name, gatherer)
54
+ @outputs << name
55
+ end
56
+ end
57
+
58
+ def scatter(options={})
59
+ @processor = {
60
+ "command" => @source_message["type"],
61
+ "dataset" => @dataset || @source_message["dataset"],
62
+ "body" => options[:body] || @source_message["body"],
63
+ "key" => nil,
64
+ "type" => "scatter",
65
+ "outputs" => [],
66
+ "replica" => "all",
67
+ "post" => true
68
+ }
69
+ end
70
+
71
+ def broadcast(options={})
72
+ processor = {
73
+ "command" => @source_message["type"],
74
+ "dataset" => @dataset || @source_message["dataset"],
75
+ "body" => options[:body] || @source_message["body"],
76
+ "type" => "broadcast",
77
+ "outputs" => [],
78
+ "replica" => "random"
79
+ }
80
+ if options[:write]
81
+ processor["replica"] = "all"
82
+ processor["post"] = true
83
+ end
84
+ @processor = processor
85
+ end
86
+
87
+ private
88
+ def reduce_command
89
+ "reduce"
90
+ end
91
+
92
+ def gather_command
93
+ "gather"
94
+ end
95
+
96
+ def unified_reducers
97
+ unified_reducers = {}
98
+ @reducers.each do |reducer|
99
+ type = reducer["type"]
100
+ unified = unified_reducers[type]
101
+ if unified
102
+ unified["body"] = unified["body"].merge(reducer["body"])
103
+ unified["inputs"] = unified["inputs"] + reducer["inputs"]
104
+ unified["outputs"] = unified["outputs"] + reducer["outputs"]
105
+ else
106
+ unified_reducers[type] = Marshal.load(Marshal.dump(reducer))
107
+ end
108
+ end
109
+ unified_reducers.values
110
+ end
111
+
112
+ def unified_gatherers
113
+ unified_gatherers = {}
114
+ @gatherers.each do |gatherer|
115
+ type = gatherer["type"]
116
+ unified = unified_gatherers[type]
117
+ if unified
118
+ unified["body"] = unified["body"].merge(gatherer["body"])
119
+ unified["inputs"] = unified["inputs"] + gatherer["inputs"]
120
+ else
121
+ unified_gatherers[type] = Marshal.load(Marshal.dump(gatherer))
122
+ end
123
+ end
124
+ unified_gatherers.values
125
+ end
126
+
127
+ def fixed_processor
128
+ @processor["outputs"] = @outputs
129
+ if @processor["type"] == "scatter"
130
+ raise MessageProcessingError.new("missing key") unless @key
131
+ @processor["key"] = @key
132
+ end
133
+ @processor
134
+ end
135
+
136
+ def reducer_message(command, name, reducer)
137
+ if reducer.is_a?(String)
138
+ reducer = {
139
+ "type" => reducer,
140
+ }
141
+ if reducer["type"] == REDUCE_SUM
142
+ reducer["limit"] = DEFAULT_LIMIT
143
+ end
144
+ end
145
+ {
146
+ "type" => command,
147
+ "body" => {
148
+ name => {
149
+ output_name(name) => reducer,
150
+ },
151
+ },
152
+ "inputs" => [name],
153
+ "outputs" => [output_name(name)],
154
+ }
155
+ end
156
+
157
+ def gatherer_message(command, name, gatherer=nil)
158
+ gatherer ||= {}
159
+ {
160
+ "type" => command,
161
+ "body" => {
162
+ output_name(name) => {
163
+ "output" => name,
164
+ }.merge(gatherer),
165
+ },
166
+ "inputs" => [output_name(name)],
167
+ "post" => true,
168
+ }
169
+ end
170
+
171
+ def output_name(name)
172
+ "#{name}_reduced"
173
+ end
174
+
175
+ #XXX Now, we include definitions to merge errors in the body.
176
+ # However, this makes the term "errors" reserved, so plugins
177
+ # cannot use their custom "errors" in the body.
178
+ # This must be rewritten.
179
+ def plan_errors_handling
180
+ reduce("errors"=> REDUCE_SUM)
181
+ end
182
+ end
183
+ end