droonga-engine 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (203) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +7 -0
  5. data/Rakefile +6 -2
  6. data/bin/droonga-engine +2 -2
  7. data/bin/{droonga-catalog-generate → droonga-engine-catalog-generate} +15 -3
  8. data/bin/droonga-engine-serf-event-handler +20 -0
  9. data/bin/droonga-engine-service +2 -2
  10. data/doc/text/news.md +21 -1
  11. data/droonga-engine.gemspec +5 -2
  12. data/lib/droonga/catalog/collection_volume.rb +12 -0
  13. data/lib/droonga/catalog/dataset.rb +25 -0
  14. data/lib/droonga/catalog/single_volume.rb +10 -0
  15. data/lib/droonga/catalog/slice.rb +4 -0
  16. data/lib/droonga/catalog/version1.rb +59 -48
  17. data/lib/droonga/catalog/version2.rb +10 -20
  18. data/lib/droonga/catalog/volume_collection.rb +27 -4
  19. data/lib/droonga/catalog_generator.rb +12 -5
  20. data/lib/droonga/catalog_observer.rb +17 -35
  21. data/lib/droonga/command/droonga_engine.rb +436 -0
  22. data/lib/droonga/command/droonga_engine_service.rb +273 -0
  23. data/lib/droonga/command/serf_event_handler.rb +85 -0
  24. data/lib/droonga/dispatcher.rb +8 -8
  25. data/lib/droonga/engine.rb +90 -26
  26. data/lib/droonga/engine/version.rb +1 -1
  27. data/lib/droonga/engine_state.rb +29 -3
  28. data/lib/droonga/internal_fluent_message_receiver.rb +100 -0
  29. data/lib/droonga/live_nodes_list_loader.rb +48 -0
  30. data/lib/droonga/live_nodes_list_observer.rb +72 -0
  31. data/lib/droonga/path.rb +47 -0
  32. data/lib/droonga/plugins/dump.rb +279 -38
  33. data/lib/droonga/plugins/groonga/select.rb +26 -14
  34. data/lib/droonga/plugins/search.rb +30 -2
  35. data/lib/droonga/plugins/search/distributed_search_planner.rb +28 -11
  36. data/lib/droonga/processor.rb +4 -0
  37. data/lib/droonga/searcher.rb +26 -0
  38. data/lib/droonga/serf.rb +119 -0
  39. data/lib/droonga/serf_downloader.rb +90 -0
  40. data/lib/droonga/server.rb +2 -2
  41. data/lib/droonga/service_control_protocol.rb +26 -0
  42. data/sample/cluster/catalog.json +1 -1
  43. data/test/command/config/default/catalog.json +2 -2
  44. data/test/command/config/version1/catalog.json +1 -1
  45. data/test/command/fixture/documents.jsons +18 -18
  46. data/test/command/fixture/event.jsons +4 -4
  47. data/test/command/fixture/user-table-array.jsons +4 -4
  48. data/test/command/fixture/user-table.jsons +5 -5
  49. data/test/command/suite/add/dimension/column.catalog.json +1 -1
  50. data/test/command/suite/add/dimension/column.test +4 -4
  51. data/test/command/suite/add/dimension/integer.catalog.json +1 -1
  52. data/test/command/suite/add/dimension/integer.test +4 -4
  53. data/test/command/suite/add/error/invalid-integer.test +1 -1
  54. data/test/command/suite/add/error/invalid-time.test +1 -1
  55. data/test/command/suite/add/error/missing-key.test +1 -1
  56. data/test/command/suite/add/error/missing-table.test +1 -1
  57. data/test/command/suite/add/error/unknown-column.test +1 -1
  58. data/test/command/suite/add/error/unknown-table.test +1 -1
  59. data/test/command/suite/add/minimum.test +1 -1
  60. data/test/command/suite/add/vector/short_text.catalog.json +26 -0
  61. data/test/command/suite/add/vector/short_text.expected +42 -0
  62. data/test/command/suite/add/vector/short_text.test +35 -0
  63. data/test/command/suite/add/with-values.test +1 -1
  64. data/test/command/suite/add/without-key.test +1 -1
  65. data/test/command/suite/dump/column/index.catalog.json +40 -0
  66. data/test/command/suite/dump/column/index.expected +195 -0
  67. data/test/command/suite/dump/column/index.test +5 -0
  68. data/test/command/suite/dump/column/scalar.catalog.json +19 -0
  69. data/test/command/suite/dump/column/scalar.expected +99 -0
  70. data/test/command/suite/dump/column/scalar.test +5 -0
  71. data/test/command/suite/dump/column/vector.catalog.json +22 -0
  72. data/test/command/suite/dump/column/vector.expected +108 -0
  73. data/test/command/suite/dump/column/vector.test +5 -0
  74. data/test/command/suite/dump/record/vector/reference.catalog.json +27 -0
  75. data/test/command/suite/dump/record/vector/reference.expected +213 -0
  76. data/test/command/suite/dump/record/vector/reference.test +21 -0
  77. data/test/command/suite/dump/table/array.catalog.json +13 -0
  78. data/test/command/suite/dump/table/array.expected +63 -0
  79. data/test/command/suite/dump/table/array.test +5 -0
  80. data/test/command/suite/dump/table/double_array_trie.catalog.json +14 -0
  81. data/test/command/suite/dump/table/double_array_trie.expected +66 -0
  82. data/test/command/suite/dump/table/double_array_trie.test +5 -0
  83. data/test/command/suite/dump/table/hash.catalog.json +14 -0
  84. data/test/command/suite/dump/table/hash.expected +66 -0
  85. data/test/command/suite/dump/table/hash.test +5 -0
  86. data/test/command/suite/dump/table/patricia_trie.catalog.json +14 -0
  87. data/test/command/suite/dump/table/patricia_trie.expected +66 -0
  88. data/test/command/suite/dump/table/patricia_trie.test +5 -0
  89. data/test/command/suite/groonga/column_create/scalar.test +2 -2
  90. data/test/command/suite/groonga/column_create/unknown-table.test +1 -1
  91. data/test/command/suite/groonga/column_create/vector.test +2 -2
  92. data/test/command/suite/groonga/column_list/success.test +3 -3
  93. data/test/command/suite/groonga/column_list/unknown-table.test +1 -1
  94. data/test/command/suite/groonga/column_remove/success.test +3 -3
  95. data/test/command/suite/groonga/column_remove/unknown-column.test +2 -2
  96. data/test/command/suite/groonga/column_remove/unknown-table.test +1 -1
  97. data/test/command/suite/groonga/column_rename/success.test +3 -3
  98. data/test/command/suite/groonga/column_rename/unknown-column.test +2 -2
  99. data/test/command/suite/groonga/column_rename/unknown-table.test +1 -1
  100. data/test/command/suite/groonga/delete/duplicated-identifiers.test +2 -2
  101. data/test/command/suite/groonga/delete/filter.test +2 -2
  102. data/test/command/suite/groonga/delete/invalid-filter.test +1 -1
  103. data/test/command/suite/groonga/delete/no-identifier.test +2 -2
  104. data/test/command/suite/groonga/delete/success.test +2 -2
  105. data/test/command/suite/groonga/delete/unknown-table.test +1 -1
  106. data/test/command/suite/groonga/select/minimum.expected +24 -1
  107. data/test/command/suite/groonga/select/minimum.test +1 -1
  108. data/test/command/suite/groonga/select/type/time.catalog.json +19 -0
  109. data/test/command/suite/groonga/select/type/time.expected +37 -0
  110. data/test/command/suite/groonga/select/type/time.test +35 -0
  111. data/test/command/suite/groonga/table_create/array.test +1 -1
  112. data/test/command/suite/groonga/table_create/hash.test +1 -1
  113. data/test/command/suite/groonga/table_list/success.test +2 -2
  114. data/test/command/suite/groonga/table_remove/success.test +1 -1
  115. data/test/command/suite/groonga/table_remove/unknown-table.test +1 -1
  116. data/test/command/suite/message/error/unknown-type.expected +1 -1
  117. data/test/command/suite/message/error/unknown-type.test +1 -1
  118. data/test/command/suite/search/adjusters/multiple.catalog.json +1 -1
  119. data/test/command/suite/search/adjusters/multiple.test +3 -3
  120. data/test/command/suite/search/adjusters/one.catalog.json +1 -1
  121. data/test/command/suite/search/adjusters/one.test +3 -3
  122. data/test/command/suite/search/attributes/array.expected +7 -0
  123. data/test/command/suite/search/attributes/array.test +1 -1
  124. data/test/command/suite/search/attributes/hash.expected +18 -0
  125. data/test/command/suite/search/attributes/hash.test +1 -1
  126. data/test/command/suite/search/complex.expected +12 -0
  127. data/test/command/suite/search/complex.test +1 -1
  128. data/test/command/suite/search/condition/nested.catalog.json +37 -0
  129. data/test/command/suite/search/condition/nested.expected +7 -0
  130. data/test/command/suite/search/condition/nested.test +103 -2
  131. data/test/command/suite/search/condition/query.catalog.json +37 -0
  132. data/test/command/suite/search/condition/query.expected +7 -0
  133. data/test/command/suite/search/condition/query.test +103 -2
  134. data/test/command/suite/search/condition/query/nonexistent_column.catalog.json +1 -1
  135. data/test/command/suite/search/condition/query/nonexistent_column.test +2 -2
  136. data/test/command/suite/search/condition/query/syntax_error.catalog.json +1 -1
  137. data/test/command/suite/search/condition/query/syntax_error.test +2 -2
  138. data/test/command/suite/search/condition/script.catalog.json +37 -0
  139. data/test/command/suite/search/condition/script.expected +7 -0
  140. data/test/command/suite/search/condition/script.test +103 -2
  141. data/test/command/suite/search/error/cyclic-source.test +1 -1
  142. data/test/command/suite/search/error/deeply-cyclic-source.test +1 -1
  143. data/test/command/suite/search/error/missing-source-parameter.test +1 -1
  144. data/test/command/suite/search/error/no-query.test +1 -1
  145. data/test/command/suite/search/error/unknown-source.test +1 -1
  146. data/test/command/suite/search/group/count.test +1 -1
  147. data/test/command/suite/search/group/limit.test +1 -1
  148. data/test/command/suite/search/group/string.catalog.json +41 -0
  149. data/test/command/suite/search/group/string.expected +18 -18
  150. data/test/command/suite/search/group/string.test +67 -22
  151. data/test/command/suite/search/group/subrecord/with-sort.catalog.json +1 -1
  152. data/test/command/suite/search/group/subrecord/with-sort.test +5 -5
  153. data/test/command/suite/search/multiple/chained.catalog.json +37 -0
  154. data/test/command/suite/search/multiple/chained.expected +14 -0
  155. data/test/command/suite/search/multiple/chained.test +103 -2
  156. data/test/command/suite/search/multiple/parallel.expected +14 -0
  157. data/test/command/suite/search/multiple/parallel.test +1 -1
  158. data/test/command/suite/search/output/attributes/invalid.catalog.json +1 -1
  159. data/test/command/suite/search/output/attributes/invalid.test +2 -2
  160. data/test/command/suite/search/output/attributes/star.catalog.json +23 -0
  161. data/test/command/suite/search/output/attributes/star.expected +27 -0
  162. data/test/command/suite/search/output/attributes/star.test +32 -0
  163. data/test/command/suite/search/range/only-output.expected +7 -0
  164. data/test/command/suite/search/range/only-output.test +1 -1
  165. data/test/command/suite/search/range/only-sort.expected +7 -0
  166. data/test/command/suite/search/range/only-sort.test +1 -1
  167. data/test/command/suite/search/range/sort-and-output.expected +7 -0
  168. data/test/command/suite/search/range/sort-and-output.test +1 -1
  169. data/test/command/suite/search/range/too-large-output-offset.expected +8 -0
  170. data/test/command/suite/search/range/too-large-output-offset.test +1 -1
  171. data/test/command/suite/search/range/too-large-sort-offset.expected +8 -0
  172. data/test/command/suite/search/range/too-large-sort-offset.test +1 -1
  173. data/test/command/suite/search/response/elapsed_time.catalog.json +1 -1
  174. data/test/command/suite/search/response/elapsed_time.test +2 -2
  175. data/test/command/suite/search/response/records/value/time.expected +12 -0
  176. data/test/command/suite/search/response/records/value/time.test +1 -1
  177. data/test/command/suite/search/simple.expected +12 -0
  178. data/test/command/suite/search/simple.test +1 -1
  179. data/test/command/suite/search/sort/default-offset-limit.expected +7 -0
  180. data/test/command/suite/search/sort/default-offset-limit.test +1 -1
  181. data/test/command/suite/search/sort/invisible-column.expected +7 -0
  182. data/test/command/suite/search/sort/invisible-column.test +1 -1
  183. data/test/unit/catalog/test_collection_volume.rb +16 -0
  184. data/test/unit/catalog/test_dataset.rb +36 -0
  185. data/test/unit/catalog/test_single_volume.rb +9 -0
  186. data/test/unit/catalog/test_slice.rb +11 -0
  187. data/test/unit/catalog/test_version1.rb +7 -12
  188. data/test/unit/catalog/test_version2.rb +7 -0
  189. data/test/unit/catalog/test_volume_collection.rb +28 -0
  190. data/test/unit/fixtures/catalog/version1.json +10 -3
  191. data/test/unit/fixtures/catalog/version2.json +2 -2
  192. data/test/unit/plugins/groonga/select/test_adapter_output.rb +8 -14
  193. data/test/unit/plugins/groonga/test_column_create.rb +5 -5
  194. data/test/unit/plugins/groonga/test_column_remove.rb +2 -2
  195. data/test/unit/plugins/groonga/test_column_rename.rb +2 -2
  196. data/test/unit/plugins/groonga/test_delete.rb +2 -2
  197. data/test/unit/plugins/groonga/test_table_create.rb +9 -9
  198. data/test/unit/plugins/groonga/test_table_remove.rb +1 -1
  199. data/test/unit/test_catalog_generator.rb +1 -1
  200. data/test/unit/test_schema_applier.rb +2 -2
  201. data/test/unit/test_watch_schema.rb +4 -4
  202. metadata +241 -72
  203. data/lib/droonga/engine/command/droonga_engine.rb +0 -441
@@ -39,18 +39,41 @@ module Droonga
39
39
  to_a.hash
40
40
  end
41
41
 
42
- def select(how=nil)
42
+ def select(how=nil, live_nodes=nil)
43
+ volumes = live_volumes(live_nodes)
43
44
  case how
44
45
  when :top
45
- [@volumes.first]
46
+ [volumes.first]
46
47
  when :random
47
- [@volumes.sample]
48
+ [volumes.sample]
48
49
  when :all
49
- @volumes
50
+ volumes
50
51
  else
51
52
  super
52
53
  end
53
54
  end
55
+
56
+ def all_nodes
57
+ @all_nodes ||= collect_all_nodes
58
+ end
59
+
60
+ def live_volumes(live_nodes=nil)
61
+ return @volumes unless live_nodes
62
+
63
+ @volumes.select do |volume|
64
+ dead_nodes = volume.all_nodes - live_nodes
65
+ dead_nodes.empty?
66
+ end
67
+ end
68
+
69
+ private
70
+ def collect_all_nodes
71
+ nodes = []
72
+ @volumes.each do |volume|
73
+ nodes += volume.all_nodes
74
+ end
75
+ nodes.sort.uniq
76
+ end
54
77
  end
55
78
  end
56
79
  end
@@ -17,6 +17,13 @@ require "time"
17
17
 
18
18
  module Droonga
19
19
  class CatalogGenerator
20
+ DEFAULT_DATASET = "Default"
21
+ DEFAULT_HOSTS = ["127.0.0.1"]
22
+ DEFAULT_N_WORKERS = 4
23
+ DEFAULT_PLUGINS = ["groonga", "search", "crud", "dump"]
24
+ DEFAULT_PORT = 10031
25
+ DEFAULT_TAG = "droonga"
26
+
20
27
  def initialize
21
28
  @version = 2
22
29
  @effective_date = Time.now
@@ -51,11 +58,11 @@ module Droonga
51
58
  end
52
59
 
53
60
  def n_workers
54
- @options[:n_workers] || 4
61
+ @options[:n_workers] || DEFAULT_N_WORKERS
55
62
  end
56
63
 
57
64
  def plugins
58
- @options[:plugins] || ["groonga", "search", "crud"]
65
+ @options[:plugins] || DEFAULT_PLUGINS
59
66
  end
60
67
 
61
68
  def schema
@@ -87,7 +94,7 @@ module Droonga
87
94
 
88
95
  class Replicas
89
96
  def initialize(options={})
90
- @hosts = options[:hosts] || ["127.0.0.1"]
97
+ @hosts = options[:hosts] || DEFAULT_HOSTS
91
98
  @port = options[:port]
92
99
  @tag = options[:tag]
93
100
  @n_slices = options[:n_slices]
@@ -113,8 +120,8 @@ module Droonga
113
120
  class Replica
114
121
  def initialize(host, options={})
115
122
  @host = host
116
- @port = options[:port] || 10031
117
- @tag = options[:tag] || "droonga"
123
+ @port = options[:port] || DEFAULT_PORT
124
+ @tag = options[:tag] || DEFAULT_TAG
118
125
  @n_slices = options[:n_slices] || 1
119
126
 
120
127
  @n_volumes = 0
@@ -15,30 +15,36 @@
15
15
  # License along with this library; if not, write to the Free Software
16
16
  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
17
 
18
+ require "coolio"
19
+
20
+ require "droonga/path"
18
21
  require "droonga/loggable"
19
- require "droonga/catalog_loader"
20
22
 
21
23
  module Droonga
22
24
  class CatalogObserver
23
25
  include Loggable
24
26
 
25
- DEFAULT_CATALOG_PATH = "catalog.json"
26
27
  CHECK_INTERVAL = 1
27
28
 
28
- attr_reader :catalog
29
- attr_accessor :on_reload
29
+ attr_accessor :on_change
30
30
 
31
31
  def initialize(loop)
32
32
  @loop = loop
33
- @catalog_path = catalog_path
34
- load_catalog!
33
+ @path = Path.catalog
34
+ @mtime = @path.mtime
35
+ @on_change = nil
35
36
  end
36
37
 
37
38
  def start
38
39
  @watcher = Cool.io::TimerWatcher.new(CHECK_INTERVAL, true)
39
- observer = self
40
+ on_timer = lambda do
41
+ if updated?
42
+ @mtime = @path.mtime
43
+ @on_change.call if @on_change
44
+ end
45
+ end
40
46
  @watcher.on_timer do
41
- observer.ensure_latest_catalog_loaded
47
+ on_timer.call
42
48
  end
43
49
  @loop.attach(@watcher)
44
50
  end
@@ -47,35 +53,11 @@ module Droonga
47
53
  @watcher.detach
48
54
  end
49
55
 
50
- def ensure_latest_catalog_loaded
51
- if catalog_updated?
52
- begin
53
- load_catalog!
54
- on_reload.call(catalog) if on_reload
55
- rescue Droonga::Error => error
56
- logger.warn("reload: fail", :path => @catalog_path, :error => error)
57
- end
58
- end
59
- end
60
-
61
- def catalog_path
62
- path = ENV["DROONGA_CATALOG"] || DEFAULT_CATALOG_PATH
63
- File.expand_path(path)
64
- end
65
-
66
- def catalog_updated?
67
- File.mtime(catalog_path) > @catalog_mtime
68
- end
69
-
70
- def load_catalog!
71
- loader = CatalogLoader.new(@catalog_path)
72
- @catalog = loader.load
73
- logger.info("loaded", :path => @catalog_path, :mtime => @catalog_mtime)
74
- ensure
75
- @catalog_mtime = File.mtime(@catalog_path)
56
+ private
57
+ def updated?
58
+ @path.mtime > @mtime
76
59
  end
77
60
 
78
- private
79
61
  def log_tag
80
62
  "catalog-observer"
81
63
  end
@@ -0,0 +1,436 @@
1
+ # Copyright (C) 2014 Droonga Project
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License version 2.1 as published by the Free Software Foundation.
6
+ #
7
+ # This library is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10
+ # Lesser General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU Lesser General Public
13
+ # License along with this library; if not, write to the Free Software
14
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
+
16
+ require "optparse"
17
+ require "socket"
18
+ require "ipaddr"
19
+ require "fileutils"
20
+
21
+ require "coolio"
22
+
23
+ require "droonga/path"
24
+ require "droonga/serf"
25
+ require "droonga/catalog_observer"
26
+ require "droonga/service_control_protocol"
27
+
28
+ module Droonga
29
+ module Command
30
+ class DroongaEngine
31
+ class << self
32
+ def run(command_line_arguments)
33
+ new.run(command_line_arguments)
34
+ end
35
+ end
36
+
37
+ def initialize
38
+ @configuration = Configuration.new
39
+ @log_output = nil
40
+ end
41
+
42
+ def run(command_line_arguments)
43
+ parse_command_line_arguments!(command_line_arguments)
44
+
45
+ setup_path
46
+
47
+ if @configuration.daemon?
48
+ Process.daemon
49
+ end
50
+
51
+ open_log_file do
52
+ write_pid_file do
53
+ run_main_loop
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+ def parse_command_line_arguments!(command_line_arguments)
60
+ parser = OptionParser.new
61
+ @configuration.add_command_line_options(parser)
62
+ parser.parse!(command_line_arguments)
63
+ end
64
+
65
+ def setup_path
66
+ Path.setup
67
+ unless $0 == File.basename($0)
68
+ droonga_engine_bin_path = File.expand_path(File.dirname($0))
69
+ new_paths = [
70
+ droonga_engine_bin_path,
71
+ ENV["PATH"],
72
+ ]
73
+ ENV["PATH"] = new_paths.join(File::PATH_SEPARATOR)
74
+ end
75
+ end
76
+
77
+ def run_main_loop
78
+ main_loop = MainLoop.new(@configuration)
79
+ main_loop.run
80
+ end
81
+
82
+ def open_log_file
83
+ if @configuration.log_file
84
+ File.open(@configuration.log_file, "a") do |file|
85
+ $stdout.reopen(file)
86
+ $stderr.reopen(file)
87
+ yield
88
+ end
89
+ else
90
+ yield
91
+ end
92
+ end
93
+
94
+ def write_pid_file
95
+ if @configuration.pid_file
96
+ File.open(@configuration.pid_file, "w") do |file|
97
+ file.puts(Process.pid)
98
+ end
99
+ begin
100
+ yield
101
+ ensure
102
+ FileUtils.rm_f(@configuration.pid_file)
103
+ end
104
+ else
105
+ yield
106
+ end
107
+ end
108
+
109
+ class Configuration
110
+ DEFAULT_HOST = Socket.gethostname
111
+ DEFAULT_PORT = 10031
112
+
113
+ attr_reader :host, :port, :tag, :log_file, :pid_file
114
+ def initialize
115
+ @host = DEFAULT_HOST
116
+ @port = DEFAULT_PORT
117
+ @tag = "droonga"
118
+ @log_file = nil
119
+ @daemon = false
120
+ @pid_file = nil
121
+ end
122
+
123
+ def engine_name
124
+ "#{@host}:#{@port}/#{@tag}"
125
+ end
126
+
127
+ def address_family
128
+ ip_address = IPAddr.new(IPSocket.getaddress(@host))
129
+ ip_address.family
130
+ end
131
+
132
+ def log_level
133
+ ENV["DROONGA_LOG_LEVEL"] || Logger::Level.default_label
134
+ end
135
+
136
+ def daemon?
137
+ @daemon
138
+ end
139
+
140
+ def to_command_line
141
+ command_line_options = [
142
+ "--engine-name", engine_name,
143
+ ]
144
+ command_line_options
145
+ end
146
+
147
+ def add_command_line_options(parser)
148
+ add_connection_options(parser)
149
+ add_log_options(parser)
150
+ add_process_options(parser)
151
+ add_path_options(parser)
152
+ end
153
+
154
+ def listen_socket
155
+ @listen_socket ||= TCPServer.new(@host, @port)
156
+ end
157
+
158
+ def heartbeat_socket
159
+ @heartbeat_socket ||= bind_heartbeat_socket
160
+ end
161
+
162
+ private
163
+ def add_connection_options(parser)
164
+ parser.separator("")
165
+ parser.separator("Connection:")
166
+ parser.on("--host=HOST",
167
+ "The host name of the Droonga engine",
168
+ "(#{@host})") do |host|
169
+ @host = host
170
+ end
171
+ parser.on("--port=PORT", Integer,
172
+ "The port number of the Droonga engine",
173
+ "(#{@port})") do |port|
174
+ @port = port
175
+ end
176
+ parser.on("--tag=TAG",
177
+ "The tag of the Droonga engine",
178
+ "(#{@tag})") do |tag|
179
+ @tag = tag
180
+ end
181
+ end
182
+
183
+ def add_log_options(parser)
184
+ parser.separator("")
185
+ parser.separator("Log:")
186
+ levels = Logger::Level::LABELS
187
+ levels_label = levels.join(",")
188
+ parser.on("--log-level=LEVEL", levels,
189
+ "The log level of the Droonga engine",
190
+ "[#{levels_label}]",
191
+ "(#{log_level})") do |level|
192
+ ENV["DROONGA_LOG_LEVEL"] = level
193
+ end
194
+ parser.on("--log-file=FILE",
195
+ "Output logs to FILE") do |file|
196
+ @log_file = File.expand_path(file)
197
+ end
198
+ end
199
+
200
+ def add_process_options(parser)
201
+ parser.separator("")
202
+ parser.separator("Process:")
203
+ parser.on("--daemon",
204
+ "Run as a daemon") do
205
+ @daemon = true
206
+ end
207
+ parser.on("--pid-file=FILE",
208
+ "Put PID to the FILE") do |file|
209
+ @pid_file = File.expand_path(file)
210
+ end
211
+ end
212
+
213
+ def add_path_options(parser)
214
+ parser.separator("")
215
+ parser.separator("Path:")
216
+ parser.on("--base-dir=DIR",
217
+ "Use DIR as the base directory",
218
+ "(#{Path.base})") do |dir|
219
+ Path.base = File.expand_path(dir)
220
+ end
221
+ end
222
+
223
+ def bind_heartbeat_socket
224
+ socket = UDPSocket.new(address_family)
225
+ socket.bind(@host, @port)
226
+ socket
227
+ end
228
+ end
229
+
230
+ class MainLoop
231
+ def initialize(configuration)
232
+ @configuration = configuration
233
+ @loop = Coolio::Loop.default
234
+ end
235
+
236
+ def run
237
+ @serf = run_serf
238
+ @service_runner = run_service
239
+ @catalog_observer = run_catalog_observer
240
+ @loop_breaker = Coolio::AsyncWatcher.new
241
+ @loop.attach(@loop_breaker)
242
+
243
+ trap_signals
244
+ @loop.run
245
+ @serf.shutdown if @serf.running?
246
+
247
+ @service_runner.success?
248
+ end
249
+
250
+ private
251
+ def trap_signals
252
+ trap(:TERM) do
253
+ stop_gracefully
254
+ trap(:TERM, "DEFAULT")
255
+ end
256
+ trap(:INT) do
257
+ stop_immediately
258
+ trap(:INT, "DEFAULT")
259
+ end
260
+ trap(:QUIT) do
261
+ stop_immediately
262
+ trap(:QUIT, "DEFAULT")
263
+ end
264
+ trap(:USR1) do
265
+ restart_graceful
266
+ end
267
+ trap(:HUP) do
268
+ restart_immediately
269
+ end
270
+ end
271
+
272
+ def stop_gracefully
273
+ @loop_breaker.signal
274
+ @loop_breaker.detach
275
+ @serf.shutdown
276
+ @catalog_observer.stop
277
+ @service_runner.stop_gracefully
278
+ end
279
+
280
+ def stop_immediately
281
+ @loop_breaker.signal
282
+ @loop_breaker.detach
283
+ @serf.shutdown
284
+ @catalog_observer.stop
285
+ @service_runner.stop_immediately
286
+ end
287
+
288
+ def restart_graceful
289
+ @loop_breaker.signal
290
+ old_service_runner = @service_runner
291
+ @service_runner = run_service
292
+ @service_runner.on_ready = lambda do
293
+ @serf.restart if @serf.running?
294
+ @service_runner.on_failure = nil
295
+ old_service_runner.stop_gracefully
296
+ end
297
+ @service_runner.on_failure = lambda do
298
+ @service_runner.on_failure = nil
299
+ @service_runner = old_service_runner
300
+ end
301
+ end
302
+
303
+ def restart_immediately
304
+ @loop_breaker.signal
305
+ old_service_runner = @service_runner
306
+ @service_runner = run_service
307
+ @serf.restart if @serf.running?
308
+ old_service_runner.stop_immediately
309
+ end
310
+
311
+ def run_service
312
+ service_runner = ServiceRunner.new(@loop, @configuration)
313
+ service_runner.run
314
+ service_runner
315
+ end
316
+
317
+ def run_serf
318
+ serf = Serf.new(@loop, @configuration.engine_name)
319
+ serf.start
320
+ serf
321
+ end
322
+
323
+ def run_catalog_observer
324
+ catalog_observer = CatalogObserver.new(@loop)
325
+ catalog_observer.on_change = lambda do
326
+ restart_graceful
327
+ end
328
+ catalog_observer.start
329
+ catalog_observer
330
+ end
331
+ end
332
+
333
+ class ServiceRunner
334
+ include ServiceControlProtocol
335
+
336
+ def initialize(raw_loop, configuration)
337
+ @raw_loop = raw_loop
338
+ @configuration = configuration
339
+ @success = false
340
+ @on_ready = nil
341
+ @on_failure = nil
342
+ end
343
+
344
+ def on_ready=(callback)
345
+ @on_ready = callback
346
+ end
347
+
348
+ def on_failure=(callback)
349
+ @on_failure = callback
350
+ end
351
+
352
+ def run
353
+ control_write_in, control_write_out = IO.pipe
354
+ control_read_in, control_read_out = IO.pipe
355
+ listen_fd = @configuration.listen_socket.fileno
356
+ heartbeat_fd = @configuration.heartbeat_socket.fileno
357
+ env = {}
358
+ command_line = [
359
+ RbConfig.ruby,
360
+ "-S",
361
+ "droonga-engine-service",
362
+ "--listen-fd", listen_fd.to_s,
363
+ "--heartbeat-fd", heartbeat_fd.to_s,
364
+ "--control-read-fd", control_write_in.fileno.to_s,
365
+ "--control-write-fd", control_read_out.fileno.to_s,
366
+ *@configuration.to_command_line,
367
+ ]
368
+ options = {
369
+ listen_fd => listen_fd,
370
+ heartbeat_fd => heartbeat_fd,
371
+ control_write_in => control_write_in,
372
+ control_read_out => control_read_out,
373
+ }
374
+ @pid = spawn(env, *command_line, options)
375
+ control_write_in.close
376
+ control_read_out.close
377
+ attach_control_write_out(control_write_out)
378
+ attach_control_read_in(control_read_in)
379
+ end
380
+
381
+ def stop_gracefully
382
+ @control_write_out.write(Messages::STOP_GRACEFUL)
383
+ end
384
+
385
+ def stop_immediately
386
+ @control_write_out.write(Messages::STOP_IMMEDIATELY)
387
+ end
388
+
389
+ def success?
390
+ @success
391
+ end
392
+
393
+ private
394
+ def on_ready
395
+ @on_ready.call if @on_ready
396
+ end
397
+
398
+ def on_failure
399
+ @on_failure.call if @on_failure
400
+ end
401
+
402
+ def on_finish
403
+ _, status = Process.waitpid2(@pid)
404
+ @success = status.success?
405
+ @control_write_out.close
406
+ @control_read_in.close
407
+ on_failure unless success?
408
+ end
409
+
410
+ def attach_control_write_out(control_write_out)
411
+ @control_write_out = Coolio::IO.new(control_write_out)
412
+ @raw_loop.attach(@control_write_out)
413
+ end
414
+
415
+ def attach_control_read_in(control_read_in)
416
+ @control_read_in = Coolio::IO.new(control_read_in)
417
+ on_read = lambda do |data|
418
+ # TODO: should buffer data to handle half line received case
419
+ data.each_line do |line|
420
+ case line
421
+ when Messages::READY
422
+ on_ready
423
+ when Messages::FINISH
424
+ on_finish
425
+ end
426
+ end
427
+ end
428
+ @control_read_in.on_read do |data|
429
+ on_read.call(data)
430
+ end
431
+ @raw_loop.attach(@control_read_in)
432
+ end
433
+ end
434
+ end
435
+ end
436
+ end