droonga-engine 1.0.2 → 1.0.3

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 (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