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.
Files changed (195) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/benchmark/timer-watcher/benchmark.rb +44 -0
  4. data/bin/droonga-engine-absorb-data +246 -187
  5. data/bin/droonga-engine-catalog-generate +12 -12
  6. data/bin/droonga-engine-catalog-modify +4 -4
  7. data/bin/droonga-engine-join +352 -171
  8. data/bin/droonga-engine-set-role +54 -0
  9. data/bin/droonga-engine-unjoin +107 -112
  10. data/droonga-engine.gemspec +3 -3
  11. data/install.sh +55 -36
  12. data/install/centos/functions.sh +2 -2
  13. data/install/debian/functions.sh +2 -2
  14. data/lib/droonga/address.rb +26 -24
  15. data/lib/droonga/buffered_tcp_socket.rb +65 -10
  16. data/lib/droonga/catalog/base.rb +9 -6
  17. data/lib/droonga/catalog/dataset.rb +17 -41
  18. data/lib/droonga/catalog/fetcher.rb +64 -0
  19. data/lib/droonga/catalog/generator.rb +245 -0
  20. data/lib/droonga/catalog/loader.rb +66 -0
  21. data/lib/droonga/{catalog_modifier.rb → catalog/modifier.rb} +11 -18
  22. data/lib/droonga/catalog/replicas_volume.rb +123 -0
  23. data/lib/droonga/catalog/schema.rb +37 -37
  24. data/lib/droonga/catalog/single_volume.rb +11 -3
  25. data/lib/droonga/catalog/slice.rb +10 -6
  26. data/lib/droonga/catalog/{collection_volume.rb → slices_volume.rb} +47 -11
  27. data/lib/droonga/catalog/version1.rb +47 -19
  28. data/lib/droonga/catalog/version2.rb +11 -10
  29. data/lib/droonga/catalog/version2_validator.rb +4 -4
  30. data/lib/droonga/catalog/volume.rb +17 -5
  31. data/lib/droonga/changable.rb +25 -0
  32. data/lib/droonga/cluster.rb +237 -0
  33. data/lib/droonga/collector_runner.rb +4 -0
  34. data/lib/droonga/collectors.rb +2 -1
  35. data/lib/droonga/collectors/recursive_sum.rb +26 -0
  36. data/lib/droonga/command/droonga_engine.rb +404 -127
  37. data/lib/droonga/command/droonga_engine_service.rb +47 -11
  38. data/lib/droonga/command/droonga_engine_worker.rb +21 -1
  39. data/lib/droonga/command/remote_command_base.rb +78 -0
  40. data/lib/droonga/command/serf_event_handler.rb +29 -20
  41. data/lib/droonga/data_absorber_client.rb +222 -0
  42. data/lib/droonga/database_scanner.rb +106 -0
  43. data/lib/droonga/{live_nodes_list_loader.rb → deferrable.rb} +11 -24
  44. data/lib/droonga/differ.rb +58 -0
  45. data/lib/droonga/dispatcher.rb +155 -32
  46. data/lib/droonga/distributed_command_planner.rb +9 -11
  47. data/lib/droonga/engine.rb +83 -78
  48. data/lib/droonga/engine/version.rb +1 -1
  49. data/lib/droonga/engine_node.rb +301 -0
  50. data/lib/droonga/engine_state.rb +62 -40
  51. data/lib/droonga/farm.rb +44 -5
  52. data/lib/droonga/file_observer.rb +16 -12
  53. data/lib/droonga/fluent_message_receiver.rb +98 -29
  54. data/lib/droonga/fluent_message_sender.rb +30 -23
  55. data/lib/droonga/forward_buffer.rb +160 -0
  56. data/lib/droonga/forwarder.rb +73 -40
  57. data/lib/droonga/handler.rb +7 -6
  58. data/lib/droonga/handler_messenger.rb +15 -6
  59. data/lib/droonga/handler_runner.rb +6 -1
  60. data/lib/droonga/internal_fluent_message_receiver.rb +28 -8
  61. data/lib/droonga/job_pusher.rb +10 -7
  62. data/lib/droonga/job_receiver.rb +6 -4
  63. data/lib/droonga/logger.rb +7 -1
  64. data/lib/droonga/node_name.rb +90 -0
  65. data/lib/droonga/node_role.rb +72 -0
  66. data/lib/droonga/path.rb +34 -9
  67. data/lib/droonga/planner.rb +73 -7
  68. data/lib/droonga/plugin/async_command.rb +154 -0
  69. data/lib/droonga/plugins/catalog.rb +1 -0
  70. data/lib/droonga/plugins/crud.rb +22 -6
  71. data/lib/droonga/plugins/dump.rb +66 -135
  72. data/lib/droonga/plugins/groonga/delete.rb +13 -0
  73. data/lib/droonga/plugins/search/distributed_search_planner.rb +4 -4
  74. data/lib/droonga/plugins/system.rb +5 -26
  75. data/lib/droonga/plugins/system/absorb_data.rb +405 -0
  76. data/lib/droonga/plugins/system/statistics.rb +71 -0
  77. data/lib/droonga/plugins/system/status.rb +53 -0
  78. data/lib/droonga/process_control_protocol.rb +3 -1
  79. data/lib/droonga/process_supervisor.rb +32 -15
  80. data/lib/droonga/reducer.rb +69 -0
  81. data/lib/droonga/safe_file_writer.rb +1 -1
  82. data/lib/droonga/serf.rb +207 -276
  83. data/lib/droonga/serf/agent.rb +228 -0
  84. data/lib/droonga/serf/command.rb +94 -0
  85. data/lib/droonga/serf/downloader.rb +120 -0
  86. data/lib/droonga/serf/remote_command.rb +348 -0
  87. data/lib/droonga/serf/tag.rb +56 -0
  88. data/lib/droonga/service_installation.rb +2 -2
  89. data/lib/droonga/session.rb +49 -1
  90. data/lib/droonga/single_step.rb +6 -11
  91. data/lib/droonga/single_step_definition.rb +32 -1
  92. data/lib/droonga/slice.rb +14 -9
  93. data/lib/droonga/supervisor.rb +27 -20
  94. data/lib/droonga/test/stub_handler_messenger.rb +2 -1
  95. data/lib/droonga/timestamp.rb +69 -0
  96. data/lib/droonga/worker_process_agent.rb +33 -15
  97. data/sample/cluster-state.json +8 -0
  98. data/sample/cluster/Rakefile +30 -6
  99. data/test/command/fixture/integer-key-table.jsons +11 -0
  100. data/test/command/fixture/string-key-table.jsons +11 -0
  101. data/test/command/run-test.rb +4 -0
  102. data/test/command/suite/add/error/invalid-integer.expected +3 -3
  103. data/test/command/suite/add/error/invalid-time.expected +3 -3
  104. data/test/command/suite/add/{minimum.expected → key-integer.expected} +0 -0
  105. data/test/command/suite/add/{minimum.test → key-integer.test} +0 -0
  106. data/test/command/suite/add/key-string.expected +6 -0
  107. data/test/command/suite/add/key-string.test +9 -0
  108. data/test/command/suite/add/mismatched-key-type/acceptable/integer-for-string.expected +6 -0
  109. data/test/command/suite/add/mismatched-key-type/acceptable/integer-for-string.test +9 -0
  110. data/test/command/suite/add/mismatched-key-type/acceptable/string-for-integer.expected +6 -0
  111. data/test/command/suite/add/mismatched-key-type/acceptable/string-for-integer.test +9 -0
  112. data/test/command/suite/add/without-values.expected +6 -0
  113. data/test/command/suite/add/without-values.test +11 -0
  114. data/test/command/suite/dump/column/index.expected +33 -1
  115. data/test/command/suite/dump/column/index.test +1 -0
  116. data/test/command/suite/dump/column/scalar.expected +29 -1
  117. data/test/command/suite/dump/column/scalar.test +1 -0
  118. data/test/command/suite/dump/column/vector.expected +29 -1
  119. data/test/command/suite/dump/column/vector.test +1 -0
  120. data/test/command/suite/dump/record/scalar.catalog.json +12 -0
  121. data/test/command/suite/dump/record/scalar.expected +84 -0
  122. data/test/command/suite/dump/record/scalar.test +16 -0
  123. data/test/command/suite/dump/record/vector/reference.expected +83 -1
  124. data/test/command/suite/dump/record/vector/reference.test +1 -0
  125. data/test/command/suite/dump/table/array.expected +27 -1
  126. data/test/command/suite/dump/table/array.test +1 -0
  127. data/test/command/suite/dump/table/double_array_trie.expected +27 -1
  128. data/test/command/suite/dump/table/double_array_trie.test +1 -0
  129. data/test/command/suite/dump/table/hash.expected +27 -1
  130. data/test/command/suite/dump/table/hash.test +1 -0
  131. data/test/command/suite/dump/table/patricia_trie.expected +27 -1
  132. data/test/command/suite/dump/table/patricia_trie.test +1 -0
  133. data/test/command/suite/groonga/delete/{success.expected → key-integer.expected} +0 -0
  134. data/test/command/suite/groonga/delete/key-integer.test +17 -0
  135. data/test/command/suite/groonga/delete/key-string.expected +19 -0
  136. data/test/command/suite/groonga/delete/{success.test → key-string.test} +4 -6
  137. data/test/command/suite/groonga/delete/mismatched-type-key/acceptable/integer-for-string.expected +19 -0
  138. data/test/command/suite/groonga/delete/mismatched-type-key/acceptable/integer-for-string.test +17 -0
  139. data/test/command/suite/groonga/delete/mismatched-type-key/acceptable/string-for-integer.expected +19 -0
  140. data/test/command/suite/groonga/delete/mismatched-type-key/acceptable/string-for-integer.test +17 -0
  141. data/test/command/suite/message/error/missing-dataset.test +1 -0
  142. data/test/command/suite/system/absorb-data/records.catalog.json +58 -0
  143. data/test/command/suite/system/absorb-data/records.expected +32 -0
  144. data/test/command/suite/system/absorb-data/records.test +24 -0
  145. data/test/command/suite/system/statistics/object/count/empty.expected +11 -0
  146. data/test/command/suite/system/statistics/object/count/empty.test +12 -0
  147. data/test/command/suite/system/statistics/object/count/per-volume/empty.catalog.json +36 -0
  148. data/test/command/suite/system/statistics/object/count/per-volume/empty.expected +19 -0
  149. data/test/command/suite/system/statistics/object/count/per-volume/empty.test +12 -0
  150. data/test/command/suite/system/statistics/object/count/per-volume/record.catalog.json +40 -0
  151. data/test/command/suite/system/statistics/object/count/per-volume/record.expected +19 -0
  152. data/test/command/suite/system/statistics/object/count/per-volume/record.test +23 -0
  153. data/test/command/suite/system/statistics/object/count/per-volume/schema.catalog.json +40 -0
  154. data/test/command/suite/system/statistics/object/count/per-volume/schema.expected +19 -0
  155. data/test/command/suite/system/statistics/object/count/per-volume/schema.test +13 -0
  156. data/test/command/suite/system/statistics/object/count/record.catalog.json +12 -0
  157. data/test/command/suite/system/statistics/object/count/record.expected +11 -0
  158. data/test/command/suite/system/statistics/object/count/record.test +23 -0
  159. data/test/command/suite/system/statistics/object/count/schema.catalog.json +12 -0
  160. data/test/command/suite/system/statistics/object/count/schema.expected +11 -0
  161. data/test/command/suite/system/statistics/object/count/schema.test +13 -0
  162. data/test/command/suite/system/status.expected +3 -2
  163. data/test/unit/catalog/test_dataset.rb +4 -1
  164. data/test/unit/{test_catalog_generator.rb → catalog/test_generator.rb} +2 -2
  165. data/test/unit/catalog/test_replicas_volume.rb +79 -0
  166. data/test/unit/catalog/test_single_volume.rb +2 -2
  167. data/test/unit/catalog/test_slice.rb +33 -1
  168. data/test/unit/catalog/{test_collection_volume.rb → test_slices_volume.rb} +72 -11
  169. data/test/unit/catalog/test_version2.rb +3 -0
  170. data/test/unit/helper/distributed_search_planner_helper.rb +2 -2
  171. data/test/unit/plugins/catalog/test_fetch.rb +4 -4
  172. data/test/unit/plugins/crud/test_add.rb +44 -4
  173. data/test/unit/plugins/groonga/test_column_create.rb +4 -4
  174. data/test/unit/plugins/groonga/test_column_list.rb +4 -4
  175. data/test/unit/plugins/groonga/test_column_remove.rb +4 -4
  176. data/test/unit/plugins/groonga/test_column_rename.rb +4 -4
  177. data/test/unit/plugins/groonga/test_delete.rb +73 -10
  178. data/test/unit/plugins/groonga/test_table_create.rb +4 -4
  179. data/test/unit/plugins/groonga/test_table_list.rb +4 -4
  180. data/test/unit/plugins/groonga/test_table_remove.rb +4 -4
  181. data/test/unit/plugins/search/test_handler.rb +4 -4
  182. data/test/unit/plugins/search/test_planner.rb +4 -2
  183. data/test/unit/plugins/system/test_status.rb +31 -15
  184. data/test/unit/plugins/test_watch.rb +16 -16
  185. data/test/unit/test_address.rb +4 -4
  186. metadata +134 -35
  187. data/lib/droonga/catalog/volume_collection.rb +0 -79
  188. data/lib/droonga/catalog_fetcher.rb +0 -53
  189. data/lib/droonga/catalog_generator.rb +0 -243
  190. data/lib/droonga/catalog_loader.rb +0 -56
  191. data/lib/droonga/command/remote.rb +0 -404
  192. data/lib/droonga/data_absorber.rb +0 -264
  193. data/lib/droonga/node_status.rb +0 -71
  194. data/lib/droonga/serf_downloader.rb +0 -115
  195. data/test/unit/catalog/test_volume_collection.rb +0 -78
@@ -0,0 +1,71 @@
1
+ # Copyright (C) 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 "droonga/plugin"
17
+ require "droonga/database_scanner"
18
+
19
+ module Droonga
20
+ module Plugins
21
+ module System
22
+ class StatisticsObjectCountHandler < Droonga::Handler
23
+ include DatabaseScanner
24
+
25
+ def handle(message)
26
+ counts(message.request["output"])
27
+ end
28
+
29
+ def counts(output)
30
+ counts = {}
31
+ if output and output.is_a?(Array)
32
+ if output.include?("tables")
33
+ counts["tables"] = n_tables
34
+ end
35
+ if output.include?("columns")
36
+ counts["columns"] = n_columns
37
+ end
38
+ if output.include?("records")
39
+ counts["records"] = n_records
40
+ end
41
+ if output.include?("total")
42
+ counts["total"] = total_n_objects
43
+ end
44
+ end
45
+ counts
46
+ end
47
+ end
48
+
49
+ define_single_step do |step|
50
+ step.name = "system.statistics.object.count"
51
+ step.handler = StatisticsObjectCountHandler
52
+ step.collector = Collectors::RecursiveSum
53
+ end
54
+
55
+ class StatisticsObjectCountPerVolumeHandler < StatisticsObjectCountHandler
56
+ def handle(message)
57
+ {
58
+ label => counts(message.request["output"]),
59
+ }
60
+ end
61
+ end
62
+
63
+ define_single_step do |step|
64
+ step.name = "system.statistics.object.count.per-volume"
65
+ step.use_all_replicas = true
66
+ step.handler = StatisticsObjectCountPerVolumeHandler
67
+ step.collector = Collectors::Sum
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,53 @@
1
+ # Copyright (C) 2013-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 "droonga/plugin"
17
+
18
+ module Droonga
19
+ module Plugins
20
+ module System
21
+ class StatusHandler < Droonga::Handler
22
+ action.synchronous = true
23
+
24
+ def handle(message)
25
+ {
26
+ "nodes" => cluster.engine_nodes_status,
27
+ "reporter" => reporter,
28
+ }
29
+ end
30
+
31
+ private
32
+ def cluster
33
+ @messenger.cluster
34
+ end
35
+
36
+ def reporter
37
+ "#{engine_state.internal_name} @ #{engine_state.name}"
38
+ end
39
+
40
+ def engine_state
41
+ @messenger.engine_state
42
+ end
43
+ end
44
+
45
+ define_single_step do |step|
46
+ step.name = "system.status"
47
+ step.single_operation = true
48
+ step.handler = StatusHandler
49
+ step.collector = Collectors::Or
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2014 Droonga Project
1
+ # Copyright (C) 2014-2015 Droonga Project
2
2
  #
3
3
  # This library is free software; you can redistribute it and/or
4
4
  # modify it under the terms of the GNU Lesser General Public
@@ -18,6 +18,8 @@ module Droonga
18
18
  module Messages
19
19
  STOP_GRACEFUL = "stop-graceful\n"
20
20
  STOP_IMMEDIATELY = "stop-immediately\n"
21
+ REFRESH_SELF_REFERENCE = "refresh-self-reference\n"
22
+ REFRESH_NODE_REFERENCE = "refresh-node-reference\n"
21
23
 
22
24
  READY = "ready\n"
23
25
  FINISH = "finish\n"
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2014 Droonga Project
1
+ # Copyright (C) 2014-2015 Droonga Project
2
2
  #
3
3
  # This library is free software; you can redistribute it and/or
4
4
  # modify it under the terms of the GNU Lesser General Public
@@ -15,52 +15,72 @@
15
15
 
16
16
  require "coolio"
17
17
 
18
+ require "droonga/loggable"
19
+ require "droonga/deferrable"
18
20
  require "droonga/process_control_protocol"
19
21
  require "droonga/line_buffer"
20
22
 
21
23
  module Droonga
22
24
  class ProcessSupervisor
25
+ include Loggable
26
+ include Deferrable
23
27
  include ProcessControlProtocol
24
28
 
29
+ attr_writer :on_finish
30
+
25
31
  def initialize(loop, input, output)
26
32
  @loop = loop
27
33
  @input = create_input(input)
28
34
  @output = create_output(output)
29
- @on_ready = nil
30
- @on_finish = nil
31
35
  end
32
36
 
33
37
  def start
34
38
  @loop.attach(@input)
39
+ logger.trace("start: input watcher attached",
40
+ :watcher => @input)
35
41
  @loop.attach(@output)
42
+ logger.trace("start: output watcher attached",
43
+ :watcher => @output)
36
44
  end
37
45
 
38
46
  def stop
39
47
  @input.close
48
+ logger.trace("stop: input watcher detached",
49
+ :watcher => @input)
40
50
  @output.close
51
+ logger.trace("stop: output watcher detached",
52
+ :watcher => @output)
41
53
  end
42
54
 
43
55
  def stop_gracefully
56
+ logger.trace("stop_gracefully: start")
44
57
  @output.write(Messages::STOP_GRACEFUL)
58
+ logger.trace("stop_gracefully: done")
45
59
  end
46
60
 
47
61
  def stop_immediately
62
+ logger.trace("stop_immediately: start")
48
63
  @output.write(Messages::STOP_IMMEDIATELY)
64
+ logger.trace("stop_immediately: done")
49
65
  end
50
66
 
51
- def on_ready=(callback)
52
- @on_ready = callback
67
+ def refresh_self_reference
68
+ logger.trace("refresh_self_reference: start")
69
+ @output.write(Messages::REFRESH_SELF_REFERENCE)
70
+ logger.trace("refresh_self_reference: done")
53
71
  end
54
72
 
55
- def on_finish=(callback)
56
- @on_finish = callback
73
+ def refresh_node_reference
74
+ logger.trace("refresh_node_reference: start")
75
+ @output.write(Messages::REFRESH_NODE_REFERENCE)
76
+ logger.trace("refresh_node_reference: done")
57
77
  end
58
78
 
59
79
  private
60
80
  def create_input(raw_input)
61
81
  input = Coolio::IO.new(raw_input)
62
82
  line_buffer = LineBuffer.new
63
- on_read = lambda do |data|
83
+ input.on_read do |data|
64
84
  line_buffer.feed(data) do |line|
65
85
  case line
66
86
  when Messages::READY
@@ -70,9 +90,6 @@ module Droonga
70
90
  end
71
91
  end
72
92
  end
73
- input.on_read do |data|
74
- on_read.call(data)
75
- end
76
93
  input
77
94
  end
78
95
 
@@ -80,12 +97,12 @@ module Droonga
80
97
  Coolio::IO.new(raw_output)
81
98
  end
82
99
 
83
- def on_ready
84
- @on_ready.call if @on_ready
85
- end
86
-
87
100
  def on_finish
88
101
  @on_finish.call if @on_finish
89
102
  end
103
+
104
+ def log_tag
105
+ "process_supervisor"
106
+ end
90
107
  end
91
108
  end
@@ -58,6 +58,10 @@ module Droonga
58
58
  reduced_value = sum(left_value, right_value)
59
59
  reduced_value = self.class.apply_range(reduced_value,
60
60
  "limit" => @deal["limit"])
61
+ when "recursive-sum"
62
+ reduced_value = recursive_sum(left_value, right_value)
63
+ reduced_value = self.class.apply_range(reduced_value,
64
+ "limit" => @deal["limit"])
61
65
  when "average"
62
66
  reduced_value = (left_value.to_f + right_value.to_f) / 2
63
67
  when "sort"
@@ -83,6 +87,22 @@ module Droonga
83
87
  end
84
88
  end
85
89
 
90
+ def recursive_sum(x, y)
91
+ return x || y if x.nil? or y.nil?
92
+
93
+ if x.is_a?(Hash) and y.is_a?(Hash)
94
+ if numeric_hash?(x) and numeric_hash?(y)
95
+ sum_numeric_hashes(x, y)
96
+ else
97
+ x.merge(y)
98
+ end
99
+ elsif numeric_array?(x) and numeric_array?(y)
100
+ sum_numeric_array(x, y)
101
+ else
102
+ x + y
103
+ end
104
+ end
105
+
86
106
  def merge(x, y, options={})
87
107
  operators = options[:operators] = normalize_operators(options[:operators])
88
108
 
@@ -165,5 +185,54 @@ module Droonga
165
185
  end
166
186
  end
167
187
  end
188
+
189
+ def numeric_array?(array)
190
+ return false unless array.is_a?(Array)
191
+ array.all? do |value|
192
+ case value
193
+ when Numeric
194
+ true
195
+ when Hash
196
+ numeric_hash?(value)
197
+ when Array
198
+ numeric_array?(value)
199
+ else
200
+ false
201
+ end
202
+ end
203
+ end
204
+
205
+ def numeric_hash?(hash)
206
+ return false unless hash.is_a?(Hash)
207
+ numeric_array?(hash.values)
208
+ end
209
+
210
+ def sum_numeric_values(x, y)
211
+ if x.nil? or y.nil?
212
+ x || y
213
+ elsif numeric_array?(x) and numeric_array?(y)
214
+ sum_numeric_arrays(x, y)
215
+ elsif numeric_hash?(x) and numeric_hash?(y)
216
+ sum_numeric_hashes(x, y)
217
+ else
218
+ x + y
219
+ end
220
+ end
221
+
222
+ def sum_numeric_arrays(x, y)
223
+ sum = []
224
+ [x.size, y.size].max.times do |index|
225
+ sum << sum_numeric_values(x[index], y[index])
226
+ end
227
+ sum
228
+ end
229
+
230
+ def sum_numeric_hashes(x, y)
231
+ sum = {}
232
+ (x.keys + y.keys).uniq.each do |key|
233
+ sum[key] = sum_numeric_values(x[key], y[key])
234
+ end
235
+ sum
236
+ end
168
237
  end
169
238
  end
@@ -24,7 +24,7 @@ module Droonga
24
24
  # Don't output the file directly to prevent loading of incomplete file!
25
25
  path = Pathname(path).expand_path
26
26
  FileUtils.mkdir_p(path.dirname.to_s)
27
- Tempfile.open(path.basename.to_s, path.dirname.to_s, "w") do |output|
27
+ Tempfile.open(path.basename.to_s, path.dirname.to_s) do |output|
28
28
  if block_given?
29
29
  yield(output, output.path)
30
30
  else
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2014 Droonga Project
1
+ # Copyright (C) 2014-2015 Droonga Project
2
2
  #
3
3
  # This library is free software; you can redistribute it and/or
4
4
  # modify it under the terms of the GNU Lesser General Public
@@ -13,161 +13,256 @@
13
13
  # License along with this library; if not, write to the Free Software
14
14
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
15
 
16
- require "English"
17
-
18
16
  require "json"
19
- require "coolio"
20
- require "open3"
21
17
 
22
18
  require "droonga/path"
23
19
  require "droonga/loggable"
24
- require "droonga/catalog_loader"
25
- require "droonga/node_status"
26
- require "droonga/serf_downloader"
20
+ require "droonga/catalog/loader"
21
+ require "droonga/node_name"
22
+ require "droonga/node_role"
23
+ require "droonga/serf/tag"
24
+ require "droonga/serf/downloader"
25
+ require "droonga/serf/agent"
26
+ require "droonga/serf/command"
27
27
  require "droonga/line_buffer"
28
+ require "droonga/safe_file_writer"
29
+ require "droonga/service_installation"
28
30
 
29
31
  module Droonga
30
32
  class Serf
31
- ROLE = {
32
- :default => {
33
- :port => 7946,
34
- },
35
- :source => {
36
- :port => 7947,
37
- },
38
- :destination => {
39
- :port => 7948,
40
- },
41
- }
42
-
43
- class << self
44
- def path
45
- Droonga::Path.base + "serf"
46
- end
47
- end
48
-
49
33
  include Loggable
50
34
 
51
- def initialize(loop, name)
52
- # TODO: Don't allow nil for loop. It reduces nil checks and
53
- # simplifies source code.
54
- @loop = loop
55
- @name = name
56
- @agent = nil
35
+ def initialize(name, options={})
36
+ @serf_command = nil
37
+ @name = NodeName.parse(name)
38
+ @verbose = options[:verbose] || false
39
+ @service_installation = ServiceInstallation.new
40
+ @tags_cache = {}
57
41
  end
58
42
 
59
- def start
60
- logger.trace("start: start")
43
+ def run_agent(loop)
44
+ logger.trace("run_agent: start")
61
45
  ensure_serf
62
- ENV["SERF"] = @serf
63
- ENV["SERF_RPC_ADDRESS"] = rpc_address
64
46
  retry_joins = []
65
47
  detect_other_hosts.each do |other_host|
66
48
  retry_joins.push("-retry-join", other_host)
67
49
  end
68
- @agent = run("agent",
69
- "-node", @name,
70
- "-bind", "#{extract_host(@name)}:#{port}",
71
- "-event-handler", "droonga-engine-serf-event-handler",
72
- "-log-level", log_level,
73
- "-tag", "role=engine",
74
- "-tag", "cluster_id=#{cluster_id}",
75
- *retry_joins)
76
- logger.trace("start: done")
50
+ tags_file = Path.serf_tags_file
51
+ FileUtils.mkdir_p(tags_file.dirname)
52
+ agent = Agent.new(loop, @serf_command,
53
+ @name.host, agent_port, rpc_port,
54
+ "-node", @name.to_s,
55
+ "-event-handler", "droonga-engine-serf-event-handler",
56
+ "-tags-file", tags_file.to_s,
57
+ *retry_joins)
58
+ agent.start
59
+ logger.trace("run_agent: done")
60
+ agent
77
61
  end
78
62
 
79
- def running?
80
- @agent and @agent.running?
63
+ def initialize_tags
64
+ set_tag(Tag.node_type, "engine")
65
+ set_tag(Tag.node_role, role)
66
+ set_tag(Tag.cluster_id, cluster_id)
81
67
  end
82
68
 
83
- def stop
84
- logger.trace("stop: start")
85
- run("leave").stop
86
- @agent.stop
87
- @agent = nil
88
- logger.trace("stop: done")
89
- end
90
-
91
- def restart
92
- logger.trace("restart: start")
93
- stop
94
- start
95
- logger.trace("restart: done")
69
+ def leave
70
+ run_command("leave")
96
71
  end
97
72
 
98
73
  def join(*hosts)
99
- ensure_serf
100
74
  nodes = hosts.collect do |host|
101
- "#{host}:#{port}"
75
+ "#{host}:#{agent_port}"
102
76
  end
103
- run_once("join", *nodes)
77
+ run_command("join", *nodes)
104
78
  end
105
79
 
106
80
  def send_query(query, payload)
107
- ensure_serf
108
81
  options = ["-format", "json"] + additional_options_from_payload(payload)
109
82
  options += [query, JSON.generate(payload)]
110
- result = run_once("query", *options)
111
- result[:result] = JSON.parse(result[:result])
112
- if payload["node"]
113
- responses = result[:result]["Responses"]
114
- response = responses[payload["node"]]
83
+ raw_serf_response = run_command("query", *options)
84
+ serf_response = JSON.parse(raw_serf_response)
85
+
86
+ node = payload["node"]
87
+ if node
88
+ responses = serf_response["Responses"]
89
+ response = responses[node]
115
90
  if response.is_a?(String)
116
91
  begin
117
- result[:response] = JSON.parse(response)
92
+ JSON.parse(response)
118
93
  rescue JSON::ParserError
119
- result[:response] = response
94
+ response
120
95
  end
121
96
  else
122
- result[:response] = response
97
+ response
123
98
  end
99
+ else
100
+ response
124
101
  end
125
- result
126
102
  end
127
103
 
128
- def live_nodes
129
- ensure_serf
130
- nodes = {}
131
- result = run_once("members", "-format", "json")
132
- result[:result] = JSON.parse(result[:result])
133
- members = result[:result]
104
+ def update_cluster_state
105
+ path = Path.cluster_state
106
+ new_state = current_cluster_state
107
+ file_contents = JSON.pretty_generate(new_state)
108
+ SafeFileWriter.write(path) do |output, file|
109
+ output.puts(file_contents)
110
+ @service_installation.ensure_correct_file_permission(file)
111
+ end
112
+ end
113
+
114
+ def current_members
115
+ raw_response = run_command("members", "-format", "json")
116
+ response = JSON.parse(raw_response)
117
+ response["members"]
118
+ end
119
+
120
+ def current_cluster_state
134
121
  current_cluster_id = cluster_id
135
- members["members"].each do |member|
136
- if member["status"] == "alive" and
137
- member["tags"]["cluster_id"] == current_cluster_id
138
- nodes[member["name"]] = {
139
- "serfAddress" => member["addr"],
140
- "tags" => member["tags"],
141
- }
122
+ nodes = {}
123
+ unprocessed_messages_existence = {}
124
+ current_members.each do |member|
125
+ foreign = member["tags"][Tag.cluster_id] != current_cluster_id
126
+ next if foreign
127
+
128
+ member["tags"].each do |key, value|
129
+ next unless Tag.have_unprocessed_messages_tag?(key)
130
+ node_name = Tag.extract_node_name_from_have_unprocessed_messages_tag(key)
131
+ next if unprocessed_messages_existence[node_name]
132
+ unprocessed_messages_existence[node_name] = value == "true"
142
133
  end
134
+
135
+ nodes[member["name"]] = {
136
+ "type" => member["tags"][Tag.node_type],
137
+ "role" => member["tags"][Tag.node_role],
138
+ "internal_name" => member["tags"][Tag.internal_node_name],
139
+ "accept_messages_newer_than" => member["tags"][Tag.accept_messages_newer_than],
140
+ "live" => member["status"] == "alive",
141
+ }
142
+ end
143
+ unprocessed_messages_existence.each do |node_name, have_messages|
144
+ nodes[node_name]["have_unprocessed_messages"] = have_messages
145
+ end
146
+ sorted_nodes = {}
147
+ nodes.keys.sort.each do |key|
148
+ sorted_nodes[key] = nodes[key]
149
+ end
150
+ sorted_nodes
151
+ end
152
+
153
+ def get_tag(name)
154
+ myself = current_members.find do |member|
155
+ member["name"] == @name.to_s
156
+ end
157
+ if myself
158
+ myself["tags"][name]
159
+ else
160
+ nil
143
161
  end
144
- nodes
145
162
  end
146
163
 
147
164
  def set_tag(name, value)
148
- ensure_serf
149
- run_once("tags", "-set", "#{name}=#{value}")
165
+ run_command("tags", "-set", "#{name}=#{value}")
166
+ @tags_cache[name] = value
167
+ end
168
+
169
+ def delete_tag(name)
170
+ run_command("tags", "-delete", name)
171
+ @tags_cache.delete(name)
150
172
  end
151
173
 
152
174
  def update_cluster_id
153
- set_tag("cluster_id", cluster_id)
175
+ set_tag(Tag.cluster_id, cluster_id)
176
+ end
177
+
178
+ def set_have_unprocessed_messages_for(node_name)
179
+ tag = Tag.have_unprocessed_messages_tag_for(node_name)
180
+ set_tag(tag, true) unless @tags_cache.key?(tag)
181
+ end
182
+
183
+ def reset_have_unprocessed_messages_for(node_name)
184
+ delete_tag(Tag.have_unprocessed_messages_tag_for(node_name))
185
+ end
186
+
187
+ def role
188
+ NodeRole.normalize(get_tag(Tag.node_role))
189
+ end
190
+
191
+ def role=(new_role)
192
+ role = NodeRole.normalize(new_role)
193
+ set_tag(Tag.node_role, role)
194
+ # after that you must run update_cluster_state to update the cluster information cache
195
+ role
196
+ end
197
+
198
+ def last_message_timestamp
199
+ response = send_query("report_last_message_timestamp",
200
+ "node" => @name.to_s)
201
+ if response
202
+ response["timestamp"]
203
+ else
204
+ nil
205
+ end
206
+ end
207
+
208
+ def accept_messages_newer_than_timestamp
209
+ get_tag(Tag.accept_messages_newer_than)
210
+ end
211
+
212
+ def accept_messages_newer_than(timestamp)
213
+ set_tag(Tag.accept_messages_newer_than, timestamp)
214
+ # after that you must run update_cluster_state to update the cluster information cache
154
215
  end
155
216
 
156
217
  def cluster_id
157
- loader = CatalogLoader.new(Path.catalog.to_s)
218
+ loader = Catalog::Loader.new(Path.catalog.to_s)
158
219
  catalog = loader.load
159
220
  catalog.cluster_id
160
221
  end
161
222
 
223
+ CHECK_RESTARTED_INTERVAL = 3
224
+ CHECK_RESTARTED_TIMEOUT = 60 * 5
225
+
226
+ def ensure_restarted(*nodes, &block)
227
+ nodes << @name.to_s if nodes.empty?
228
+
229
+ targets = nodes.collect do |node|
230
+ serf = self.class.new(node)
231
+ {
232
+ :serf => serf,
233
+ :previous_name => serf.get_tag(Tag.internal_node_name),
234
+ }
235
+ end
236
+
237
+ start_time = Time.now
238
+
239
+ yield # the given operation must restart the service.
240
+
241
+ while Time.now - start_time < CHECK_RESTARTED_TIMEOUT
242
+ puts "Checking nodes are restarted or not:" if @verbose
243
+ targets.reject! do |target|
244
+ name = target[:serf].get_tag(Tag.internal_node_name)
245
+ restarted = name != target[:previous_name]
246
+ puts " #{name} vs #{target[:previous_name]} => " +
247
+ "#{restarted ? "restarted" : "not yet"}" if @verbose
248
+ restarted
249
+ end
250
+ break if targets.empty?
251
+ sleep(CHECK_RESTARTED_INTERVAL)
252
+ end
253
+
254
+ targets.empty?
255
+ end
256
+
162
257
  private
163
258
  def ensure_serf
164
- @serf = find_system_serf
165
- return if @serf
259
+ @serf_command ||= find_system_serf
260
+ return if @serf_command
166
261
 
167
- serf_path = self.class.path
168
- @serf = serf_path.to_s
262
+ serf_path = Path.serf_command
263
+ @serf_command = serf_path.to_s
169
264
  return if serf_path.executable?
170
- downloader = SerfDownloader.new(serf_path)
265
+ downloader = Downloader.new(serf_path)
171
266
  downloader.download
172
267
  end
173
268
 
@@ -180,19 +275,13 @@ module Droonga
180
275
  nil
181
276
  end
182
277
 
183
- def run(command, *options)
184
- process = SerfProcess.new(@loop, @serf, command,
185
- "-rpc-addr", rpc_address,
186
- *options)
187
- process.start
188
- process
189
- end
190
-
191
- def run_once(command, *options)
192
- process = SerfProcess.new(@loop, @serf, command,
193
- "-rpc-addr", rpc_address,
194
- *options)
195
- process.run_once
278
+ def run_command(command, *options)
279
+ ensure_serf
280
+ command = Command.new(@serf_command, command,
281
+ "-rpc-addr", rpc_address,
282
+ *options)
283
+ command.verbose = @verbose
284
+ command.run
196
285
  end
197
286
 
198
287
  def additional_options_from_payload(payload)
@@ -204,192 +293,34 @@ module Droonga
204
293
  end
205
294
 
206
295
  def extract_host(node_name)
207
- node_name.split(":").first
208
- end
209
-
210
- def log_level
211
- level = Logger::Level.default
212
- case level
213
- when "trace", "debug", "info", "warn"
214
- level
215
- when "error", "fatal"
216
- "err"
217
- else
218
- level # Or error?
219
- end
296
+ node_name.to_s.split(":").first
220
297
  end
221
298
 
222
299
  def rpc_address
223
- "#{extract_host(@name)}:7373"
224
- end
225
-
226
- def node_status
227
- @node_status ||= NodeStatus.new
300
+ "#{@name.host}:#{rpc_port}"
228
301
  end
229
302
 
230
- def role
231
- if node_status.have?(:role)
232
- role = node_status.get(:role).to_sym
233
- if self.class::ROLE.key?(role)
234
- return role
235
- end
236
- end
237
- :default
303
+ def rpc_port
304
+ 7373
238
305
  end
239
306
 
240
- def port
241
- self.class::ROLE[role][:port]
307
+ def agent_port
308
+ Agent::PORT
242
309
  end
243
310
 
244
311
  def detect_other_hosts
245
- loader = CatalogLoader.new(Path.catalog.to_s)
312
+ loader = Catalog::Loader.new(Path.catalog.to_s)
246
313
  catalog = loader.load
247
314
  other_nodes = catalog.all_nodes.reject do |node|
248
- node == @name
315
+ node == @name.to_s
249
316
  end
250
317
  other_nodes.collect do |node|
251
- extract_host(node)
318
+ NodeName.parse(node).host
252
319
  end
253
320
  end
254
321
 
255
322
  def log_tag
256
323
  "serf"
257
324
  end
258
-
259
- class SerfProcess
260
- include Loggable
261
-
262
- def initialize(loop, serf, command, *options)
263
- @loop = loop
264
- @serf = serf
265
- @command = command
266
- @options = options
267
- @pid = nil
268
- end
269
-
270
- def start
271
- capture_output do |output_write, error_write|
272
- env = {}
273
- spawn_options = {
274
- :out => output_write,
275
- :err => error_write,
276
- }
277
- @pid = spawn(env, @serf, @command, *@options, spawn_options)
278
- end
279
- end
280
-
281
- def stop
282
- return if @pid.nil?
283
- Process.waitpid(@pid)
284
- @output_io.close
285
- @error_io.close
286
- @pid = nil
287
- end
288
-
289
- def running?
290
- not @pid.nil?
291
- end
292
-
293
- def run_once
294
- stdout, stderror, status = Open3.capture3(@serf, @command, *@options, :pgroup => true)
295
- {
296
- :result => stdout,
297
- :error => stderror,
298
- :status => status,
299
- }
300
- end
301
-
302
- private
303
- def capture_output
304
- result = nil
305
- output_read, output_write = IO.pipe
306
- error_read, error_write = IO.pipe
307
-
308
- begin
309
- result = yield(output_write, error_write)
310
- rescue
311
- output_read.close unless output_read.closed?
312
- output_write.close unless output_write.closed?
313
- error_read.close unless error_read.closed?
314
- error_write.close unless error_write.closed?
315
- raise
316
- end
317
-
318
- output_line_buffer = LineBuffer.new
319
- on_read_output = lambda do |data|
320
- on_standard_output(output_line_buffer, data)
321
- end
322
- @output_io = Coolio::IO.new(output_read)
323
- @output_io.on_read do |data|
324
- on_read_output.call(data)
325
- end
326
- # TODO: Don't allow nil for loop. It reduces nil checks and
327
- # simplifies source code.
328
- @loop.attach(@output_io) if @loop
329
-
330
- error_line_buffer = LineBuffer.new
331
- on_read_error = lambda do |data|
332
- on_error_output(error_line_buffer, data)
333
- end
334
- @error_io = Coolio::IO.new(error_read)
335
- @error_io.on_read do |data|
336
- on_read_error.call(data)
337
- end
338
- # TODO: Don't allow nil for loop. It reduces nil checks and
339
- # simplifies source code.
340
- @loop.attach(@error_io) if @loop
341
-
342
- result
343
- end
344
-
345
- def on_standard_output(line_buffer, data)
346
- line_buffer.feed(data) do |line|
347
- line = line.chomp
348
- case line
349
- when /\A==> /
350
- content = $POSTMATCH
351
- logger.info(content)
352
- when /\A /
353
- content = $POSTMATCH
354
- case content
355
- when /\A(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2}) \[(\w+)\] /
356
- year, month, day = $1, $2, $3
357
- hour, minute, second = $4, $5, $6
358
- level = $7
359
- content = $POSTMATCH
360
- level = normalize_level(level)
361
- logger.send(level, content)
362
- else
363
- logger.info(content)
364
- end
365
- else
366
- logger.info(line)
367
- end
368
- end
369
- end
370
-
371
- def normalize_level(level)
372
- level = level.downcase
373
- case level
374
- when "err"
375
- "error"
376
- else
377
- level
378
- end
379
- end
380
-
381
- def on_error_output(line_buffer, data)
382
- line_buffer.feed(data) do |line|
383
- line = line.chomp
384
- logger.error(line.gsub(/\A==> /, ""))
385
- end
386
- end
387
-
388
- def log_tag
389
- tag = "serf"
390
- tag << "[#{@pid}]" if @pid
391
- tag
392
- end
393
- end
394
325
  end
395
326
  end