droonga-engine 1.0.9 → 1.1.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/.travis.yml +1 -0
- data/benchmark/timer-watcher/benchmark.rb +44 -0
- data/bin/droonga-engine-absorb-data +246 -187
- data/bin/droonga-engine-catalog-generate +12 -12
- data/bin/droonga-engine-catalog-modify +4 -4
- data/bin/droonga-engine-join +352 -171
- data/bin/droonga-engine-set-role +54 -0
- data/bin/droonga-engine-unjoin +107 -112
- data/droonga-engine.gemspec +3 -3
- data/install.sh +55 -36
- data/install/centos/functions.sh +2 -2
- data/install/debian/functions.sh +2 -2
- data/lib/droonga/address.rb +26 -24
- data/lib/droonga/buffered_tcp_socket.rb +65 -10
- data/lib/droonga/catalog/base.rb +9 -6
- data/lib/droonga/catalog/dataset.rb +17 -41
- data/lib/droonga/catalog/fetcher.rb +64 -0
- data/lib/droonga/catalog/generator.rb +245 -0
- data/lib/droonga/catalog/loader.rb +66 -0
- data/lib/droonga/{catalog_modifier.rb → catalog/modifier.rb} +11 -18
- data/lib/droonga/catalog/replicas_volume.rb +123 -0
- data/lib/droonga/catalog/schema.rb +37 -37
- data/lib/droonga/catalog/single_volume.rb +11 -3
- data/lib/droonga/catalog/slice.rb +10 -6
- data/lib/droonga/catalog/{collection_volume.rb → slices_volume.rb} +47 -11
- data/lib/droonga/catalog/version1.rb +47 -19
- data/lib/droonga/catalog/version2.rb +11 -10
- data/lib/droonga/catalog/version2_validator.rb +4 -4
- data/lib/droonga/catalog/volume.rb +17 -5
- data/lib/droonga/changable.rb +25 -0
- data/lib/droonga/cluster.rb +237 -0
- data/lib/droonga/collector_runner.rb +4 -0
- data/lib/droonga/collectors.rb +2 -1
- data/lib/droonga/collectors/recursive_sum.rb +26 -0
- data/lib/droonga/command/droonga_engine.rb +404 -127
- data/lib/droonga/command/droonga_engine_service.rb +47 -11
- data/lib/droonga/command/droonga_engine_worker.rb +21 -1
- data/lib/droonga/command/remote_command_base.rb +78 -0
- data/lib/droonga/command/serf_event_handler.rb +29 -20
- data/lib/droonga/data_absorber_client.rb +222 -0
- data/lib/droonga/database_scanner.rb +106 -0
- data/lib/droonga/{live_nodes_list_loader.rb → deferrable.rb} +11 -24
- data/lib/droonga/differ.rb +58 -0
- data/lib/droonga/dispatcher.rb +155 -32
- data/lib/droonga/distributed_command_planner.rb +9 -11
- data/lib/droonga/engine.rb +83 -78
- data/lib/droonga/engine/version.rb +1 -1
- data/lib/droonga/engine_node.rb +301 -0
- data/lib/droonga/engine_state.rb +62 -40
- data/lib/droonga/farm.rb +44 -5
- data/lib/droonga/file_observer.rb +16 -12
- data/lib/droonga/fluent_message_receiver.rb +98 -29
- data/lib/droonga/fluent_message_sender.rb +30 -23
- data/lib/droonga/forward_buffer.rb +160 -0
- data/lib/droonga/forwarder.rb +73 -40
- data/lib/droonga/handler.rb +7 -6
- data/lib/droonga/handler_messenger.rb +15 -6
- data/lib/droonga/handler_runner.rb +6 -1
- data/lib/droonga/internal_fluent_message_receiver.rb +28 -8
- data/lib/droonga/job_pusher.rb +10 -7
- data/lib/droonga/job_receiver.rb +6 -4
- data/lib/droonga/logger.rb +7 -1
- data/lib/droonga/node_name.rb +90 -0
- data/lib/droonga/node_role.rb +72 -0
- data/lib/droonga/path.rb +34 -9
- data/lib/droonga/planner.rb +73 -7
- data/lib/droonga/plugin/async_command.rb +154 -0
- data/lib/droonga/plugins/catalog.rb +1 -0
- data/lib/droonga/plugins/crud.rb +22 -6
- data/lib/droonga/plugins/dump.rb +66 -135
- data/lib/droonga/plugins/groonga/delete.rb +13 -0
- data/lib/droonga/plugins/search/distributed_search_planner.rb +4 -4
- data/lib/droonga/plugins/system.rb +5 -26
- data/lib/droonga/plugins/system/absorb_data.rb +405 -0
- data/lib/droonga/plugins/system/statistics.rb +71 -0
- data/lib/droonga/plugins/system/status.rb +53 -0
- data/lib/droonga/process_control_protocol.rb +3 -1
- data/lib/droonga/process_supervisor.rb +32 -15
- data/lib/droonga/reducer.rb +69 -0
- data/lib/droonga/safe_file_writer.rb +1 -1
- data/lib/droonga/serf.rb +207 -276
- data/lib/droonga/serf/agent.rb +228 -0
- data/lib/droonga/serf/command.rb +94 -0
- data/lib/droonga/serf/downloader.rb +120 -0
- data/lib/droonga/serf/remote_command.rb +348 -0
- data/lib/droonga/serf/tag.rb +56 -0
- data/lib/droonga/service_installation.rb +2 -2
- data/lib/droonga/session.rb +49 -1
- data/lib/droonga/single_step.rb +6 -11
- data/lib/droonga/single_step_definition.rb +32 -1
- data/lib/droonga/slice.rb +14 -9
- data/lib/droonga/supervisor.rb +27 -20
- data/lib/droonga/test/stub_handler_messenger.rb +2 -1
- data/lib/droonga/timestamp.rb +69 -0
- data/lib/droonga/worker_process_agent.rb +33 -15
- data/sample/cluster-state.json +8 -0
- data/sample/cluster/Rakefile +30 -6
- data/test/command/fixture/integer-key-table.jsons +11 -0
- data/test/command/fixture/string-key-table.jsons +11 -0
- data/test/command/run-test.rb +4 -0
- data/test/command/suite/add/error/invalid-integer.expected +3 -3
- data/test/command/suite/add/error/invalid-time.expected +3 -3
- data/test/command/suite/add/{minimum.expected → key-integer.expected} +0 -0
- data/test/command/suite/add/{minimum.test → key-integer.test} +0 -0
- data/test/command/suite/add/key-string.expected +6 -0
- data/test/command/suite/add/key-string.test +9 -0
- data/test/command/suite/add/mismatched-key-type/acceptable/integer-for-string.expected +6 -0
- data/test/command/suite/add/mismatched-key-type/acceptable/integer-for-string.test +9 -0
- data/test/command/suite/add/mismatched-key-type/acceptable/string-for-integer.expected +6 -0
- data/test/command/suite/add/mismatched-key-type/acceptable/string-for-integer.test +9 -0
- data/test/command/suite/add/without-values.expected +6 -0
- data/test/command/suite/add/without-values.test +11 -0
- data/test/command/suite/dump/column/index.expected +33 -1
- data/test/command/suite/dump/column/index.test +1 -0
- data/test/command/suite/dump/column/scalar.expected +29 -1
- data/test/command/suite/dump/column/scalar.test +1 -0
- data/test/command/suite/dump/column/vector.expected +29 -1
- data/test/command/suite/dump/column/vector.test +1 -0
- data/test/command/suite/dump/record/scalar.catalog.json +12 -0
- data/test/command/suite/dump/record/scalar.expected +84 -0
- data/test/command/suite/dump/record/scalar.test +16 -0
- data/test/command/suite/dump/record/vector/reference.expected +83 -1
- data/test/command/suite/dump/record/vector/reference.test +1 -0
- data/test/command/suite/dump/table/array.expected +27 -1
- data/test/command/suite/dump/table/array.test +1 -0
- data/test/command/suite/dump/table/double_array_trie.expected +27 -1
- data/test/command/suite/dump/table/double_array_trie.test +1 -0
- data/test/command/suite/dump/table/hash.expected +27 -1
- data/test/command/suite/dump/table/hash.test +1 -0
- data/test/command/suite/dump/table/patricia_trie.expected +27 -1
- data/test/command/suite/dump/table/patricia_trie.test +1 -0
- data/test/command/suite/groonga/delete/{success.expected → key-integer.expected} +0 -0
- data/test/command/suite/groonga/delete/key-integer.test +17 -0
- data/test/command/suite/groonga/delete/key-string.expected +19 -0
- data/test/command/suite/groonga/delete/{success.test → key-string.test} +4 -6
- data/test/command/suite/groonga/delete/mismatched-type-key/acceptable/integer-for-string.expected +19 -0
- data/test/command/suite/groonga/delete/mismatched-type-key/acceptable/integer-for-string.test +17 -0
- data/test/command/suite/groonga/delete/mismatched-type-key/acceptable/string-for-integer.expected +19 -0
- data/test/command/suite/groonga/delete/mismatched-type-key/acceptable/string-for-integer.test +17 -0
- data/test/command/suite/message/error/missing-dataset.test +1 -0
- data/test/command/suite/system/absorb-data/records.catalog.json +58 -0
- data/test/command/suite/system/absorb-data/records.expected +32 -0
- data/test/command/suite/system/absorb-data/records.test +24 -0
- data/test/command/suite/system/statistics/object/count/empty.expected +11 -0
- data/test/command/suite/system/statistics/object/count/empty.test +12 -0
- data/test/command/suite/system/statistics/object/count/per-volume/empty.catalog.json +36 -0
- data/test/command/suite/system/statistics/object/count/per-volume/empty.expected +19 -0
- data/test/command/suite/system/statistics/object/count/per-volume/empty.test +12 -0
- data/test/command/suite/system/statistics/object/count/per-volume/record.catalog.json +40 -0
- data/test/command/suite/system/statistics/object/count/per-volume/record.expected +19 -0
- data/test/command/suite/system/statistics/object/count/per-volume/record.test +23 -0
- data/test/command/suite/system/statistics/object/count/per-volume/schema.catalog.json +40 -0
- data/test/command/suite/system/statistics/object/count/per-volume/schema.expected +19 -0
- data/test/command/suite/system/statistics/object/count/per-volume/schema.test +13 -0
- data/test/command/suite/system/statistics/object/count/record.catalog.json +12 -0
- data/test/command/suite/system/statistics/object/count/record.expected +11 -0
- data/test/command/suite/system/statistics/object/count/record.test +23 -0
- data/test/command/suite/system/statistics/object/count/schema.catalog.json +12 -0
- data/test/command/suite/system/statistics/object/count/schema.expected +11 -0
- data/test/command/suite/system/statistics/object/count/schema.test +13 -0
- data/test/command/suite/system/status.expected +3 -2
- data/test/unit/catalog/test_dataset.rb +4 -1
- data/test/unit/{test_catalog_generator.rb → catalog/test_generator.rb} +2 -2
- data/test/unit/catalog/test_replicas_volume.rb +79 -0
- data/test/unit/catalog/test_single_volume.rb +2 -2
- data/test/unit/catalog/test_slice.rb +33 -1
- data/test/unit/catalog/{test_collection_volume.rb → test_slices_volume.rb} +72 -11
- data/test/unit/catalog/test_version2.rb +3 -0
- data/test/unit/helper/distributed_search_planner_helper.rb +2 -2
- data/test/unit/plugins/catalog/test_fetch.rb +4 -4
- data/test/unit/plugins/crud/test_add.rb +44 -4
- data/test/unit/plugins/groonga/test_column_create.rb +4 -4
- data/test/unit/plugins/groonga/test_column_list.rb +4 -4
- data/test/unit/plugins/groonga/test_column_remove.rb +4 -4
- data/test/unit/plugins/groonga/test_column_rename.rb +4 -4
- data/test/unit/plugins/groonga/test_delete.rb +73 -10
- data/test/unit/plugins/groonga/test_table_create.rb +4 -4
- data/test/unit/plugins/groonga/test_table_list.rb +4 -4
- data/test/unit/plugins/groonga/test_table_remove.rb +4 -4
- data/test/unit/plugins/search/test_handler.rb +4 -4
- data/test/unit/plugins/search/test_planner.rb +4 -2
- data/test/unit/plugins/system/test_status.rb +31 -15
- data/test/unit/plugins/test_watch.rb +16 -16
- data/test/unit/test_address.rb +4 -4
- metadata +134 -35
- data/lib/droonga/catalog/volume_collection.rb +0 -79
- data/lib/droonga/catalog_fetcher.rb +0 -53
- data/lib/droonga/catalog_generator.rb +0 -243
- data/lib/droonga/catalog_loader.rb +0 -56
- data/lib/droonga/command/remote.rb +0 -404
- data/lib/droonga/data_absorber.rb +0 -264
- data/lib/droonga/node_status.rb +0 -71
- data/lib/droonga/serf_downloader.rb +0 -115
- data/test/unit/catalog/test_volume_collection.rb +0 -78
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Copyright (C) 2014-2015 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
15
|
+
|
|
16
|
+
require "English"
|
|
17
|
+
|
|
18
|
+
require "coolio"
|
|
19
|
+
|
|
20
|
+
require "droonga/loggable"
|
|
21
|
+
require "droonga/deferrable"
|
|
22
|
+
|
|
23
|
+
module Droonga
|
|
24
|
+
class Serf
|
|
25
|
+
class Agent
|
|
26
|
+
# the port must be different from droonga-http-server's agent!
|
|
27
|
+
PORT = 7946
|
|
28
|
+
|
|
29
|
+
include Loggable
|
|
30
|
+
include Deferrable
|
|
31
|
+
|
|
32
|
+
MAX_N_READ_CHECKS = 10
|
|
33
|
+
|
|
34
|
+
def initialize(loop, serf, host, bind_port, rpc_port, *options)
|
|
35
|
+
@loop = loop
|
|
36
|
+
@serf = serf
|
|
37
|
+
@host = host
|
|
38
|
+
@bind_port = bind_port
|
|
39
|
+
@rpc_port = rpc_port
|
|
40
|
+
@options = options
|
|
41
|
+
@pid = nil
|
|
42
|
+
@n_ready_checks = 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def start
|
|
46
|
+
capture_output do |output_write, error_write|
|
|
47
|
+
env = {}
|
|
48
|
+
spawn_options = {
|
|
49
|
+
:out => output_write,
|
|
50
|
+
:err => error_write,
|
|
51
|
+
}
|
|
52
|
+
@pid = spawn(env, @serf, "agent",
|
|
53
|
+
"-bind", "#{@host}:#{@bind_port}",
|
|
54
|
+
"-rpc-addr", "#{@host}:#{@rpc_port}",
|
|
55
|
+
"-log-level", serf_log_level,
|
|
56
|
+
*@options, spawn_options)
|
|
57
|
+
end
|
|
58
|
+
start_ready_check
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def stop(&block)
|
|
62
|
+
if @pid.nil?
|
|
63
|
+
yield if block_given?
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
Process.waitpid(@pid)
|
|
67
|
+
@output_io.close
|
|
68
|
+
# logger.trace("stop: output_io watcher detached",
|
|
69
|
+
# :watcher => @output_io)
|
|
70
|
+
@error_io.close
|
|
71
|
+
# logger.trace("stop: error_io watcher detached",
|
|
72
|
+
# :watcher => @error_io)
|
|
73
|
+
@pid = nil
|
|
74
|
+
yield if block_given?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def running?
|
|
78
|
+
not @pid.nil?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
def serf_log_level
|
|
83
|
+
level = Logger::Level.default
|
|
84
|
+
case level
|
|
85
|
+
when "trace", "debug", "info", "warn"
|
|
86
|
+
level
|
|
87
|
+
when "error", "fatal"
|
|
88
|
+
"err"
|
|
89
|
+
else
|
|
90
|
+
level # Or error?
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def capture_output
|
|
95
|
+
result = nil
|
|
96
|
+
output_read, output_write = IO.pipe
|
|
97
|
+
error_read, error_write = IO.pipe
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
result = yield(output_write, error_write)
|
|
101
|
+
rescue
|
|
102
|
+
output_read.close unless output_read.closed?
|
|
103
|
+
output_write.close unless output_write.closed?
|
|
104
|
+
error_read.close unless error_read.closed?
|
|
105
|
+
error_write.close unless error_write.closed?
|
|
106
|
+
raise
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
output_line_buffer = LineBuffer.new
|
|
110
|
+
@output_io = Coolio::IO.new(output_read)
|
|
111
|
+
@output_io.on_read do |data|
|
|
112
|
+
on_standard_output(output_line_buffer, data)
|
|
113
|
+
end
|
|
114
|
+
@loop.attach(@output_io)
|
|
115
|
+
# logger.trace("capture_output: new output_io watcher attached",
|
|
116
|
+
# :watcher => @output_io)
|
|
117
|
+
|
|
118
|
+
error_line_buffer = LineBuffer.new
|
|
119
|
+
@error_io = Coolio::IO.new(error_read)
|
|
120
|
+
@error_io.on_read do |data|
|
|
121
|
+
on_error_output(error_line_buffer, data)
|
|
122
|
+
end
|
|
123
|
+
@loop.attach(@error_io)
|
|
124
|
+
# logger.trace("capture_output: new error_io watcher attached",
|
|
125
|
+
# :watcher => @error_io)
|
|
126
|
+
|
|
127
|
+
result
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def on_standard_output(line_buffer, data)
|
|
131
|
+
line_buffer.feed(data) do |line|
|
|
132
|
+
line = line.chomp
|
|
133
|
+
case line
|
|
134
|
+
when /\A==> /
|
|
135
|
+
content = $POSTMATCH
|
|
136
|
+
logger.info(content)
|
|
137
|
+
when /\A /
|
|
138
|
+
content = $POSTMATCH
|
|
139
|
+
case content
|
|
140
|
+
when /\A(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2}) \[(\w+)\] /
|
|
141
|
+
# year, month, day = $1, $2, $3
|
|
142
|
+
# hour, minute, second = $4, $5, $6
|
|
143
|
+
level = $7
|
|
144
|
+
content = $POSTMATCH
|
|
145
|
+
return unless needed_log_message?(content)
|
|
146
|
+
level = normalize_level(level)
|
|
147
|
+
logger.send(level, content)
|
|
148
|
+
else
|
|
149
|
+
logger.info(content)
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
logger.info(line)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def needed_log_message?(content)
|
|
158
|
+
case content
|
|
159
|
+
when /\Amemberlist: Failed to receive remote state: EOF\z/
|
|
160
|
+
# See also: https://github.com/hashicorp/consul/issues/598#issuecomment-71576948
|
|
161
|
+
false
|
|
162
|
+
when /\Aagent: Script .*droonga-engine-serf-event-handler.* slow, execution exceeding/
|
|
163
|
+
# Droonga's serf event handler can be slow for absorbing or some operations.
|
|
164
|
+
false
|
|
165
|
+
else
|
|
166
|
+
true
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def normalize_level(level)
|
|
171
|
+
level = level.downcase
|
|
172
|
+
case level
|
|
173
|
+
when "err"
|
|
174
|
+
"error"
|
|
175
|
+
else
|
|
176
|
+
level
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def on_error_output(line_buffer, data)
|
|
181
|
+
line_buffer.feed(data) do |line|
|
|
182
|
+
line = line.chomp
|
|
183
|
+
logger.error(line.gsub(/\A==> /, ""))
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def start_ready_check
|
|
188
|
+
@n_ready_checks += 1
|
|
189
|
+
|
|
190
|
+
checker = Coolio::TCPSocket.connect(@host, @bind_port)
|
|
191
|
+
|
|
192
|
+
checker.on_connect do
|
|
193
|
+
on_ready
|
|
194
|
+
checker.close
|
|
195
|
+
logger.trace("start_ready_check: socket detached",
|
|
196
|
+
:watcher => checker)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
checker.on_connect_failed do
|
|
200
|
+
if @n_ready_checks >= MAX_N_READ_CHECKS
|
|
201
|
+
on_failure
|
|
202
|
+
else
|
|
203
|
+
timer = Coolio::TimerWatcher.new(1)
|
|
204
|
+
timer.on_timer do
|
|
205
|
+
start_ready_check
|
|
206
|
+
timer.detach
|
|
207
|
+
logger.trace("start_ready_check: timer detached",
|
|
208
|
+
:watcher => timer)
|
|
209
|
+
end
|
|
210
|
+
@loop.attach(timer)
|
|
211
|
+
logger.trace("start_ready_check: timer attached",
|
|
212
|
+
:watcher => timer)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
@loop.attach(checker)
|
|
217
|
+
logger.trace("start_ready_check: socket attached",
|
|
218
|
+
:watcher => checker)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def log_tag
|
|
222
|
+
tag = "serf-agent"
|
|
223
|
+
tag << "[#{@pid}]" if @pid
|
|
224
|
+
tag
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Copyright (C) 2014-2015 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
15
|
+
|
|
16
|
+
require "open3"
|
|
17
|
+
require "pp"
|
|
18
|
+
|
|
19
|
+
require "droonga/loggable"
|
|
20
|
+
|
|
21
|
+
module Droonga
|
|
22
|
+
class Serf
|
|
23
|
+
class Command
|
|
24
|
+
class Failure < Error
|
|
25
|
+
attr_reader :command_line, :exit_status, :output, :error
|
|
26
|
+
def initialize(command_line, exit_status, output, error)
|
|
27
|
+
@command_line = command_line
|
|
28
|
+
@exit_status = exit_status
|
|
29
|
+
@output = output
|
|
30
|
+
@error = error
|
|
31
|
+
message = "Failed to run serf: (#{@exit_status}): "
|
|
32
|
+
message << "#{@error.strip}[#{@output.strip}]: "
|
|
33
|
+
message << @command_line.join(" ")
|
|
34
|
+
super(message)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class ForbiddenCommandInEventHandler < Error
|
|
39
|
+
def initialize(command)
|
|
40
|
+
message = "#{command} is forbidden in an event handler script."
|
|
41
|
+
super(message)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
DANGEROUS_COMMANDS_IN_EVENT_HANDLER = [
|
|
46
|
+
"event",
|
|
47
|
+
"query",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
include Loggable
|
|
51
|
+
|
|
52
|
+
attr_accessor :verbose
|
|
53
|
+
|
|
54
|
+
def initialize(serf, command, *options)
|
|
55
|
+
assert_safe_command(command)
|
|
56
|
+
@serf = serf
|
|
57
|
+
@command = command
|
|
58
|
+
@options = options
|
|
59
|
+
@verbose = false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def run
|
|
63
|
+
command_line = [@serf, @command] + @options
|
|
64
|
+
p command_line if @verbose
|
|
65
|
+
stdout, stderror, status = Open3.capture3(*command_line,
|
|
66
|
+
:pgroup => true)
|
|
67
|
+
unless status.success?
|
|
68
|
+
raise Failure.new(command_line, status.to_i, stdout, stderror)
|
|
69
|
+
end
|
|
70
|
+
logger.error("run: #{stderror}") unless stderror.empty?
|
|
71
|
+
if @verbose
|
|
72
|
+
begin
|
|
73
|
+
pp JSON.parse(stdout)
|
|
74
|
+
rescue JSON::ParserError
|
|
75
|
+
p stdout
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
stdout
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
def assert_safe_command(command)
|
|
83
|
+
if ENV.key?("SERF_EVENT") and
|
|
84
|
+
DANGEROUS_COMMANDS_IN_EVENT_HANDLER.include?(command)
|
|
85
|
+
raise ForbiddenCommandInEventHandler.new(command)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def log_tag
|
|
90
|
+
"serf[#{@command}]"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
15
|
+
|
|
16
|
+
require "stringio"
|
|
17
|
+
require "tmpdir"
|
|
18
|
+
require "fileutils"
|
|
19
|
+
|
|
20
|
+
require "faraday"
|
|
21
|
+
require "faraday_middleware"
|
|
22
|
+
require "archive/zip"
|
|
23
|
+
|
|
24
|
+
require "droonga/loggable"
|
|
25
|
+
|
|
26
|
+
module Droonga
|
|
27
|
+
class Serf
|
|
28
|
+
class Downloader
|
|
29
|
+
include Loggable
|
|
30
|
+
|
|
31
|
+
class DownloadFailed < StandardError
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
MAX_RETRY_COUNT = 5
|
|
35
|
+
RETRY_INTERVAL = 10
|
|
36
|
+
|
|
37
|
+
TARGET_VERSION = "0.6.3"
|
|
38
|
+
|
|
39
|
+
def initialize(output_path)
|
|
40
|
+
@output_path = output_path
|
|
41
|
+
@retry_count = 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def download
|
|
45
|
+
detect_platform
|
|
46
|
+
url_base = "https://dl.bintray.com/mitchellh/serf"
|
|
47
|
+
base_name = "#{TARGET_VERSION}_#{@os}_#{@architecture}.zip"
|
|
48
|
+
connection = Faraday.new(url_base) do |builder|
|
|
49
|
+
builder.response(:follow_redirects)
|
|
50
|
+
builder.adapter(Faraday.default_adapter)
|
|
51
|
+
end
|
|
52
|
+
response = connection.get(base_name)
|
|
53
|
+
absolete_output_path = @output_path.expand_path
|
|
54
|
+
Dir.mktmpdir do |dir|
|
|
55
|
+
Archive::Zip.extract(StringIO.new(response.body),
|
|
56
|
+
dir,
|
|
57
|
+
:directories => false)
|
|
58
|
+
FileUtils.mv("#{dir}/serf", absolete_output_path.to_s)
|
|
59
|
+
FileUtils.chmod(0755, absolete_output_path.to_s)
|
|
60
|
+
end
|
|
61
|
+
rescue Archive::Zip::UnzipError => archive_error
|
|
62
|
+
logger.warn("Downloaded zip file is broken.",
|
|
63
|
+
:detail => archive_error)
|
|
64
|
+
if @retry_count < MAX_RETRY_COUNT
|
|
65
|
+
@retry_count += 1
|
|
66
|
+
sleep(RETRY_INTERVAL * @retry_count)
|
|
67
|
+
download
|
|
68
|
+
else
|
|
69
|
+
raise DownloadFailed.new("Couldn't download serf executable. Try it later.")
|
|
70
|
+
end
|
|
71
|
+
rescue Faraday::ConnectionFailed => network_error
|
|
72
|
+
logger.warn("Connection failed.",
|
|
73
|
+
:detail => network_error)
|
|
74
|
+
if @retry_count < MAX_RETRY_COUNT
|
|
75
|
+
@retry_count += 1
|
|
76
|
+
sleep(RETRY_INTERVAL * @retry_count)
|
|
77
|
+
download
|
|
78
|
+
else
|
|
79
|
+
raise DownloadFailed.new("Couldn't download serf executable. Try it later.")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
def detect_platform
|
|
85
|
+
detect_os
|
|
86
|
+
detect_architecture
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def detect_os
|
|
90
|
+
case RUBY_PLATFORM
|
|
91
|
+
when /linux/
|
|
92
|
+
@os = "linux"
|
|
93
|
+
when /freebsd/
|
|
94
|
+
@os = "freebsd"
|
|
95
|
+
when /darwin/
|
|
96
|
+
@os = "darwin"
|
|
97
|
+
when /mswin|mingw/
|
|
98
|
+
@os = "windows"
|
|
99
|
+
else
|
|
100
|
+
raise "Unsupported OS: #{RUBY_PLATFORM}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def detect_architecture
|
|
105
|
+
case RUBY_PLATFORM
|
|
106
|
+
when /x86_64|x64/
|
|
107
|
+
@architecture = "amd64"
|
|
108
|
+
when /i\d86/
|
|
109
|
+
@architecture = "386"
|
|
110
|
+
else
|
|
111
|
+
raise "Unsupported architecture: #{RUBY_PLATFORM}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def log_tag
|
|
116
|
+
"serf-downloader"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# Copyright (C) 2014-2015 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
15
|
+
|
|
16
|
+
require "json"
|
|
17
|
+
|
|
18
|
+
require "droonga/path"
|
|
19
|
+
require "droonga/serf"
|
|
20
|
+
require "droonga/node_name"
|
|
21
|
+
require "droonga/catalog/generator"
|
|
22
|
+
require "droonga/catalog/modifier"
|
|
23
|
+
require "droonga/catalog/fetcher"
|
|
24
|
+
require "droonga/safe_file_writer"
|
|
25
|
+
require "droonga/timestamp"
|
|
26
|
+
require "droonga/service_installation"
|
|
27
|
+
|
|
28
|
+
module Droonga
|
|
29
|
+
class Serf
|
|
30
|
+
module RemoteCommand
|
|
31
|
+
class Base
|
|
32
|
+
attr_reader :response
|
|
33
|
+
|
|
34
|
+
def initialize(serf_name, params)
|
|
35
|
+
@serf_name = serf_name
|
|
36
|
+
@params = params
|
|
37
|
+
@response = {
|
|
38
|
+
"log" => []
|
|
39
|
+
}
|
|
40
|
+
@serf = Serf.new(@serf_name)
|
|
41
|
+
|
|
42
|
+
@service_installation = ServiceInstallation.new
|
|
43
|
+
@service_installation.ensure_using_service_base_directory
|
|
44
|
+
|
|
45
|
+
log("params = #{params}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def process
|
|
49
|
+
# override me!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def should_process?
|
|
53
|
+
if @params.nil?
|
|
54
|
+
log("anonymous query (to be processed)")
|
|
55
|
+
return true
|
|
56
|
+
end
|
|
57
|
+
unless for_this_cluster?
|
|
58
|
+
log("query for different cluster (mine: #{cluster_id}, to be ignroed)")
|
|
59
|
+
return false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
unless @params.include?("node")
|
|
63
|
+
log("anonymous node query (to be processed)")
|
|
64
|
+
return true
|
|
65
|
+
end
|
|
66
|
+
unless for_me?
|
|
67
|
+
log("query for different node (me: #{@serf_name}, to be ignored)")
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
log("query for this node (to be processed)")
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def log(message)
|
|
76
|
+
@response["log"] << message
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
def node
|
|
81
|
+
@node ||= NodeName.parse(@serf_name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def host
|
|
85
|
+
node.host
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def cluster_id
|
|
89
|
+
@serf.cluster_id
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def target_cluster
|
|
93
|
+
return nil unless @params
|
|
94
|
+
@params["cluster_id"]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def target_node
|
|
98
|
+
return nil unless @params
|
|
99
|
+
@target_node ||= NodeName.parse(@params["node"] || "")
|
|
100
|
+
rescue ArgumentError
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def for_this_cluster?
|
|
105
|
+
target_cluster.nil? or target_cluster == cluster_id
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def for_me?
|
|
109
|
+
target_node == node
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def catalog
|
|
113
|
+
@catalog ||= JSON.parse(Path.catalog.read)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class ChangeRole < Base
|
|
118
|
+
def process
|
|
119
|
+
log("old role: #{@serf.role}")
|
|
120
|
+
@serf.role = @params["role"]
|
|
121
|
+
log("new role: #{@serf.role}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class ReportLastMessageTimestamp < Base
|
|
126
|
+
def process
|
|
127
|
+
timestamp = Timestamp.last_message_timestamp
|
|
128
|
+
if timestamp
|
|
129
|
+
@response["timestamp"] = Timestamp.stringify(timestamp)
|
|
130
|
+
else
|
|
131
|
+
@response["timestamp"] = nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class AcceptMessagesNewerThan < Base
|
|
137
|
+
def process
|
|
138
|
+
log("old timestamp: #{@serf.accept_messages_newer_than_timestamp}")
|
|
139
|
+
@serf.accept_messages_newer_than(@params["timestamp"])
|
|
140
|
+
log("new timestamp: #{@serf.accept_messages_newer_than_timestamp}")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class CrossNodeCommandBase < Base
|
|
145
|
+
private
|
|
146
|
+
def source_node
|
|
147
|
+
return nil unless @params
|
|
148
|
+
@source_node ||= NodeName.parse(@params["source"] || "")
|
|
149
|
+
rescue ArgumentError
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def dataset
|
|
154
|
+
@dataset ||= @params["dataset"]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def source_host
|
|
158
|
+
source_node.host
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def port
|
|
162
|
+
@port ||= @params["port"] || source_node.port
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def tag
|
|
166
|
+
@tag ||= @params["tag"] || source_node.tag
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
class Join < CrossNodeCommandBase
|
|
171
|
+
def process
|
|
172
|
+
log("type = #{type}")
|
|
173
|
+
case type
|
|
174
|
+
when "replica"
|
|
175
|
+
join_as_replica
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
def type
|
|
181
|
+
@params["type"]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def joining_node
|
|
185
|
+
target_node
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def joining_host
|
|
189
|
+
joining_node.host
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def valid_params?
|
|
193
|
+
not dataset.nil? and
|
|
194
|
+
not source_node.nil? and
|
|
195
|
+
not joining_node.nil?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def join_as_replica
|
|
199
|
+
return unless valid_params?
|
|
200
|
+
|
|
201
|
+
log("source_node = #{source_node}")
|
|
202
|
+
|
|
203
|
+
@catalog = fetch_catalog
|
|
204
|
+
|
|
205
|
+
@other_hosts = replica_hosts
|
|
206
|
+
log("other_hosts = #{@other_hosts}")
|
|
207
|
+
return if @other_hosts.empty?
|
|
208
|
+
|
|
209
|
+
join_to_cluster
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def replica_hosts
|
|
213
|
+
generator = Catalog::Generator.new
|
|
214
|
+
generator.load(catalog)
|
|
215
|
+
dataset = generator.dataset_for_host(source_host) ||
|
|
216
|
+
generator.dataset_for_host(host)
|
|
217
|
+
return [] unless dataset
|
|
218
|
+
dataset.replicas.hosts
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def fetch_catalog
|
|
222
|
+
fetcher = Catalog::Fetcher.new(:host => source_host,
|
|
223
|
+
:port => port,
|
|
224
|
+
:tag => tag,
|
|
225
|
+
:receiver_host => host)
|
|
226
|
+
fetcher.fetch(:dataset => dataset)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def join_to_cluster
|
|
230
|
+
log("joining to the cluster")
|
|
231
|
+
@serf.join(*@other_hosts)
|
|
232
|
+
|
|
233
|
+
log("update catalog.json from fetched catalog")
|
|
234
|
+
Catalog::Modifier.new(catalog).modify do |modifier, file|
|
|
235
|
+
modifier.datasets[dataset].replicas.hosts += [joining_host]
|
|
236
|
+
modifier.datasets[dataset].replicas.hosts.uniq!
|
|
237
|
+
@service_installation.ensure_correct_file_permission(file)
|
|
238
|
+
end
|
|
239
|
+
log("done")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
class ModifyReplicasBase < Base
|
|
244
|
+
private
|
|
245
|
+
def dataset
|
|
246
|
+
@params["dataset"]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def hosts
|
|
250
|
+
@hosts ||= prepare_hosts
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def prepare_hosts
|
|
254
|
+
hosts = @params["hosts"]
|
|
255
|
+
return nil unless hosts
|
|
256
|
+
hosts = [hosts] if hosts.is_a?(String)
|
|
257
|
+
hosts
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
class SetReplicas < ModifyReplicasBase
|
|
262
|
+
def process
|
|
263
|
+
return if dataset.nil? or hosts.nil?
|
|
264
|
+
|
|
265
|
+
log("new replicas: #{hosts.join(",")}")
|
|
266
|
+
|
|
267
|
+
log("joining to the cluster")
|
|
268
|
+
@serf.join(*hosts)
|
|
269
|
+
|
|
270
|
+
log("setting replicas to the cluster")
|
|
271
|
+
Catalog::Modifier.new(catalog).modify do |modifier, file|
|
|
272
|
+
modifier.datasets[dataset].replicas.hosts = hosts
|
|
273
|
+
@service_installation.ensure_correct_file_permission(file)
|
|
274
|
+
end
|
|
275
|
+
log("done")
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
class AddReplicas < ModifyReplicasBase
|
|
280
|
+
def process
|
|
281
|
+
return if dataset.nil? or hosts.nil?
|
|
282
|
+
|
|
283
|
+
added_hosts = hosts - [host]
|
|
284
|
+
log("adding replicas: #{added_hosts.join(",")}")
|
|
285
|
+
return if added_hosts.empty?
|
|
286
|
+
|
|
287
|
+
log("joining to the cluster")
|
|
288
|
+
@serf.join(*added_hosts)
|
|
289
|
+
|
|
290
|
+
log("adding replicas to the cluster")
|
|
291
|
+
Catalog::Modifier.new(catalog).modify do |modifier, file|
|
|
292
|
+
modifier.datasets[dataset].replicas.hosts += added_hosts
|
|
293
|
+
modifier.datasets[dataset].replicas.hosts.uniq!
|
|
294
|
+
@service_installation.ensure_correct_file_permission(file)
|
|
295
|
+
end
|
|
296
|
+
log("done")
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
class RemoveReplicas < ModifyReplicasBase
|
|
301
|
+
def process
|
|
302
|
+
return if dataset.nil? or hosts.nil?
|
|
303
|
+
|
|
304
|
+
log("removing replicas: #{hosts.join(",")}")
|
|
305
|
+
|
|
306
|
+
log("removing replicas from the cluster")
|
|
307
|
+
Catalog::Modifier.new(catalog).modify do |modifier, file|
|
|
308
|
+
modifier.datasets[dataset].replicas.hosts -= hosts
|
|
309
|
+
@service_installation.ensure_correct_file_permission(file)
|
|
310
|
+
end
|
|
311
|
+
log("done")
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
class Unjoin < ModifyReplicasBase
|
|
316
|
+
def process
|
|
317
|
+
return if dataset.nil? or hosts.nil?
|
|
318
|
+
|
|
319
|
+
log("unjoining replicas: #{hosts.join(",")}")
|
|
320
|
+
|
|
321
|
+
log("unjoining from the cluster")
|
|
322
|
+
Catalog::Modifier.new(catalog).modify do |modifier, file|
|
|
323
|
+
if unjoining_node?
|
|
324
|
+
modifier.datasets[dataset].replicas.hosts = hosts
|
|
325
|
+
else
|
|
326
|
+
modifier.datasets[dataset].replicas.hosts -= hosts
|
|
327
|
+
end
|
|
328
|
+
@service_installation.ensure_correct_file_permission(file)
|
|
329
|
+
end
|
|
330
|
+
log("done")
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
private
|
|
334
|
+
def unjoining_node?
|
|
335
|
+
hosts.include?(host)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
class UpdateClusterState < Base
|
|
340
|
+
def process
|
|
341
|
+
log("updating cluster state")
|
|
342
|
+
@serf.update_cluster_state
|
|
343
|
+
log("done")
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|