droonga-engine 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/bin/droonga-engine-absorb-data +2 -1
  3. data/bin/droonga-engine-catalog-generate +21 -5
  4. data/bin/droonga-engine-catalog-modify +22 -6
  5. data/bin/droonga-engine-configure +215 -0
  6. data/bin/droonga-engine-join +48 -123
  7. data/bin/droonga-engine-unjoin +14 -1
  8. data/doc/text/news.md +21 -0
  9. data/droonga-engine.gemspec +12 -10
  10. data/install/centos/droonga-engine +60 -0
  11. data/install/centos/functions.sh +35 -0
  12. data/install/debian/droonga-engine +155 -0
  13. data/install/debian/functions.sh +33 -0
  14. data/install.sh +360 -0
  15. data/lib/droonga/address.rb +3 -1
  16. data/lib/droonga/catalog/dataset.rb +2 -0
  17. data/lib/droonga/catalog/version1.rb +16 -3
  18. data/lib/droonga/catalog/version2.rb +16 -3
  19. data/lib/droonga/catalog_fetcher.rb +51 -0
  20. data/lib/droonga/catalog_generator.rb +6 -5
  21. data/lib/droonga/catalog_modifier.rb +45 -0
  22. data/lib/droonga/command/droonga_engine.rb +96 -29
  23. data/lib/droonga/command/droonga_engine_service.rb +5 -0
  24. data/lib/droonga/command/remote.rb +368 -0
  25. data/lib/droonga/command/serf_event_handler.rb +37 -304
  26. data/lib/droonga/dispatcher.rb +15 -1
  27. data/lib/droonga/engine/version.rb +1 -1
  28. data/lib/droonga/engine.rb +11 -4
  29. data/lib/droonga/engine_state.rb +2 -0
  30. data/lib/droonga/farm.rb +14 -5
  31. data/lib/droonga/fluent_message_receiver.rb +23 -6
  32. data/lib/droonga/fluent_message_sender.rb +5 -1
  33. data/lib/droonga/node_status.rb +67 -0
  34. data/lib/droonga/path.rb +28 -4
  35. data/lib/droonga/plugins/catalog.rb +40 -0
  36. data/lib/droonga/safe_file_writer.rb +1 -1
  37. data/lib/droonga/searcher.rb +3 -15
  38. data/lib/droonga/serf.rb +17 -32
  39. data/lib/droonga/serf_downloader.rb +26 -1
  40. data/lib/droonga/service_installation.rb +123 -0
  41. data/lib/droonga/session.rb +4 -0
  42. data/lib/droonga/slice.rb +22 -12
  43. data/lib/droonga/supervisor.rb +16 -2
  44. data/lib/droonga/worker_process_agent.rb +13 -1
  45. data/sample/droonga-engine.yaml +5 -0
  46. data/test/command/config/default/catalog.json +1 -1
  47. data/test/command/config/default/droonga-engine.yaml +4 -0
  48. data/test/command/config/version1/catalog.json +1 -1
  49. data/test/command/suite/catalog/fetch.expected +64 -0
  50. data/test/command/suite/catalog/fetch.test +6 -0
  51. data/test/unit/catalog/test_version1.rb +2 -2
  52. data/test/unit/catalog/test_version2.rb +3 -3
  53. data/test/unit/helper/sandbox.rb +3 -1
  54. data/test/unit/plugins/catalog/test_fetch.rb +76 -0
  55. data/test/unit/test_catalog_generator.rb +7 -3
  56. metadata +74 -27
  57. data/bin/droonga-engine-data-publisher +0 -66
@@ -14,12 +14,9 @@
14
14
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
15
 
16
16
  require "json"
17
+ require "fileutils"
17
18
 
18
- require "droonga/path"
19
- require "droonga/serf"
20
- require "droonga/catalog_generator"
21
- require "droonga/data_absorber"
22
- require "droonga/safe_file_writer"
19
+ require "droonga/command/remote"
23
20
 
24
21
  module Droonga
25
22
  module Command
@@ -31,331 +28,67 @@ module Droonga
31
28
  end
32
29
 
33
30
  def initialize
34
- @serf = ENV["SERF"] || Serf.path
35
- @serf_rpc_address = ENV["SERF_RPC_ADDRESS"] || "127.0.0.1:7373"
36
- @serf_name = ENV["SERF_SELF_NAME"]
37
- @response = {
38
- "log" => []
39
- }
31
+ @payload = nil
40
32
  end
41
33
 
42
34
  def run
43
- parse_event
44
- unless event_for_me?
45
- log(" => ignoring event not for me")
46
- output_response
47
- return true
48
- end
35
+ command_class = detect_command_class
36
+ return true if command_class.nil?
49
37
 
50
- process_event
51
- output_live_nodes
52
- output_response
38
+ serf_name = ENV["SERF_SELF_NAME"]
39
+ command = command_class.new(serf_name, @payload)
40
+ command.process if command.should_process?
41
+ output_response(command.response)
42
+ true
43
+ rescue Exception => exception
44
+ #XXX Any exception blocks following serf operations.
45
+ # To keep it working, I rescue any exception for now.
46
+ FileUtils.mkdir_p(Path.serf_event_handler_errors)
47
+ File.open(Path.serf_event_handler_error_file, "w") do |file|
48
+ file.write(exception.inspect)
49
+ end
53
50
  true
54
51
  end
55
52
 
56
53
  private
57
- def parse_event
58
- @event_name = ENV["SERF_EVENT"]
59
- @payload = nil
60
- case @event_name
54
+ def detect_command_class
55
+ case ENV["SERF_EVENT"]
61
56
  when "user"
62
- @event_sub_name = ENV["SERF_USER_EVENT"]
63
57
  @payload = JSON.parse($stdin.gets)
64
- log("event sub name = #{@event_sub_name}")
58
+ detect_command_class_from_custom_event(ENV["SERF_USER_EVENT"])
65
59
  when "query"
66
- @event_sub_name = ENV["SERF_QUERY_NAME"]
67
60
  @payload = JSON.parse($stdin.gets)
68
- log("event sub name = #{@event_sub_name}")
61
+ detect_command_class_from_custom_event(ENV["SERF_QUERY_NAME"])
69
62
  when "member-join", "member-leave", "member-update", "member-reap"
70
- output_live_nodes
63
+ Remote::UpdateLiveNodes
64
+ else
65
+ nil
71
66
  end
72
67
  end
73
68
 
74
- def event_for_me?
75
- return true unless @payload
76
- return true unless @payload["node"]
77
-
78
- @payload["node"] == @serf_name
79
- end
80
-
81
- def process_event
82
- case @event_sub_name
69
+ def detect_command_class_from_custom_event(event_name)
70
+ case event_name
83
71
  when "change_role"
84
- save_status(:role, @payload["role"])
72
+ Remote::ChangeRole
85
73
  when "report_status"
86
- report_status
74
+ Remote::ReportStatus
87
75
  when "join"
88
- join
76
+ Remote::Join
89
77
  when "set_replicas"
90
- set_replicas
78
+ Remote::SetReplicas
91
79
  when "add_replicas"
92
- add_replicas
80
+ Remote::AddReplicas
93
81
  when "remove_replicas"
94
- remove_replicas
82
+ Remote::RemoveReplicas
95
83
  when "absorb_data"
96
- absorb_data
97
- when "publish_catalog"
98
- publish_catalog
99
- when "unpublish_catalog"
100
- unpublish_catalog
101
- end
102
- end
103
-
104
- def output_response
105
- puts JSON.generate(@response)
106
- end
107
-
108
- def host
109
- @serf_name.split(":").first
110
- end
111
-
112
- def given_hosts
113
- hosts = @payload["hosts"]
114
- return nil unless hosts
115
- hosts = [hosts] if hosts.is_a?(String)
116
- hosts
117
- end
118
-
119
- def report_status
120
- @response["value"] = status(@payload["key"].to_sym)
121
- end
122
-
123
- def join
124
- type = @payload["type"]
125
- log("type = #{type}")
126
- case type
127
- when "replica"
128
- join_as_replica
84
+ Remote::AbsorbData
85
+ else
86
+ nil
129
87
  end
130
88
  end
131
89
 
132
- def join_as_replica
133
- source_node = @payload["source"]
134
- return unless source_node
135
-
136
- log("source_node = #{source_node}")
137
-
138
- source_host = source_node.split(":").first
139
-
140
- # fetch_port = @payload["fetch_port"]
141
- # catalog = fetch_catalog(source_node, fetch_port)
142
- catalog = JSON.parse(Path.catalog.read) #XXX workaround until "fetch" become available
143
-
144
- generator = create_current_catalog_generator(catalog)
145
- dataset = generator.dataset_for_host(source_host) ||
146
- generator.dataset_for_host(host)
147
- return unless dataset
148
-
149
- # restart self with the fetched catalog.
150
- SafeFileWriter.write(Path.catalog, JSON.pretty_generate(catalog))
151
-
152
- dataset_name = dataset.name
153
- tag = dataset.replicas.tag
154
- port = dataset.replicas.port
155
- other_hosts = dataset.replicas.hosts
156
-
157
- log("dataset = #{dataset_name}")
158
- log("port = #{port}")
159
- log("tag = #{tag}")
160
-
161
- if @payload["copy"]
162
- log("starting to copy data from #{source_host}")
163
-
164
- modify_catalog do |modifier|
165
- modifier.datasets[dataset_name].replicas.hosts = [host]
166
- end
167
- sleep(5) #TODO: wait for restart. this should be done more safely, to avoid starting of absorbing with old catalog.json.
168
-
169
- save_status(:absorbing, true)
170
- DataAbsorber.absorb(:dataset => dataset_name,
171
- :source_host => source_host,
172
- :destination_host => host,
173
- :port => port,
174
- :tag => tag)
175
- delete_status(:absorbing)
176
- sleep(1)
177
- end
178
-
179
- log("joining to the cluster: update myself")
180
-
181
- modify_catalog do |modifier|
182
- modifier.datasets[dataset_name].replicas.hosts += other_hosts
183
- modifier.datasets[dataset_name].replicas.hosts.uniq!
184
- end
185
- end
186
-
187
- def fetch_catalog(source_node, port)
188
- source_host = source_node.split(":").first
189
-
190
- url = "http://#{source_host}:#{port}"
191
- connection = Faraday.new(url) do |builder|
192
- builder.response(:follow_redirects)
193
- builder.adapter(Faraday.default_adapter)
194
- end
195
- response = connection.get("/catalog.json")
196
- catalog = response.body
197
-
198
- JSON.parse(catalog)
199
- end
200
-
201
- def publish_catalog
202
- port = @payload["port"]
203
- return unless port
204
-
205
- env = {}
206
- publisher_command_line = [
207
- "droonga-engine-data-publisher",
208
- "--base-dir", Path.base.to_s,
209
- "--port", port.to_s,
210
- "--published-file", Path.catalog.to_s
211
- ]
212
- pid = spawn(env, *publisher_command_line)
213
- Process.detach(pid)
214
- sleep(1) # wait until the directory is published
215
-
216
- published_dir = Path.published(port)
217
- pid_file = published_dir + ".pid"
218
-
219
- File.open(pid_file.to_s, "w") do |file|
220
- file.puts(pid)
221
- end
222
- end
223
-
224
- def unpublish_catalog
225
- port = @payload["port"]
226
- return unless port
227
-
228
- published_dir = Path.published(port)
229
- pid_file = published_dir + ".pid"
230
- pid = pid_file.read.to_i
231
-
232
- Process.kill("INT", pid)
233
- end
234
-
235
- def set_replicas
236
- dataset = @payload["dataset"]
237
- return unless dataset
238
-
239
- hosts = given_hosts
240
- return unless hosts
241
-
242
- log("new replicas: #{hosts.join(",")}")
243
-
244
- modify_catalog do |modifier|
245
- modifier.datasets[dataset].replicas.hosts = hosts
246
- end
247
- end
248
-
249
- def add_replicas
250
- dataset = @payload["dataset"]
251
- return unless dataset
252
-
253
- hosts = given_hosts
254
- return unless hosts
255
-
256
- hosts -= [host]
257
- return if hosts.empty?
258
-
259
- log("adding replicas: #{hosts.join(",")}")
260
-
261
- modify_catalog do |modifier|
262
- modifier.datasets[dataset].replicas.hosts += hosts
263
- modifier.datasets[dataset].replicas.hosts.uniq!
264
- end
265
- end
266
-
267
- def remove_replicas
268
- dataset = @payload["dataset"]
269
- return unless dataset
270
-
271
- hosts = given_hosts
272
- return unless hosts
273
-
274
- log("removing replicas: #{hosts.join(",")}")
275
-
276
- modify_catalog do |modifier|
277
- modifier.datasets[dataset].replicas.hosts -= hosts
278
- end
279
- end
280
-
281
- def modify_catalog
282
- generator = create_current_catalog_generator
283
- yield(generator)
284
- SafeFileWriter.write(Path.catalog, JSON.pretty_generate(generator.generate))
285
- end
286
-
287
- def create_current_catalog_generator(current_catalog=nil)
288
- current_catalog ||= JSON.parse(Path.catalog.read)
289
- generator = CatalogGenerator.new
290
- generator.load(current_catalog)
291
- end
292
-
293
- def absorb_data
294
- source = @payload["source"]
295
- return unless source
296
-
297
- log("start to absorb data from #{source}")
298
-
299
- dataset_name = @payload["dataset"]
300
- port = @payload["port"]
301
- tag = @payload["tag"]
302
-
303
- if dataset_name.nil? or port.nil? or tag.nil?
304
- current_catalog = JSON.parse(Path.catalog.read)
305
- generator = CatalogGenerator.new
306
- generator.load(current_catalog)
307
-
308
- dataset = generator.dataset_for_host(source)
309
- return unless dataset
310
-
311
- dataset_name = dataset.name
312
- port = dataset.replicas.port
313
- tag = dataset.replicas.tag
314
- end
315
-
316
- log("dataset = #{dataset_name}")
317
- log("port = #{port}")
318
- log("tag = #{tag}")
319
-
320
- save_status(:absorbing, true)
321
- DataAbsorber.absorb(:dataset => dataset_name,
322
- :source_host => source,
323
- :destination_host => host,
324
- :port => port,
325
- :tag => tag,
326
- :client => "droonga-send")
327
- delete_status(:absorbing)
328
- end
329
-
330
- def live_nodes
331
- Serf.live_nodes(@serf_name)
332
- end
333
-
334
- def output_live_nodes
335
- path = Path.live_nodes
336
- nodes = live_nodes
337
- file_contents = JSON.pretty_generate(nodes)
338
- SafeFileWriter.write(path, file_contents)
339
- end
340
-
341
- def status(key)
342
- Serf.status(key)
343
- end
344
-
345
- def save_status(key, value)
346
- status = Serf.load_status
347
- status[key] = value
348
- SafeFileWriter.write(Serf.status_file, JSON.pretty_generate(status))
349
- end
350
-
351
- def delete_status(key)
352
- status = Serf.load_status
353
- status.delete(key)
354
- SafeFileWriter.write(Serf.status_file, JSON.pretty_generate(status))
355
- end
356
-
357
- def log(message)
358
- @response["log"] << message
90
+ def output_response(response)
91
+ puts JSON.generate(response)
359
92
  end
360
93
  end
361
94
  end
@@ -69,7 +69,20 @@ module Droonga
69
69
  @farm.start
70
70
  end
71
71
 
72
- def shutdown
72
+ def stop_gracefully(&on_stop)
73
+ logger.trace("stop_gracefully: start")
74
+ @collector_runners.each_value do |collector_runner|
75
+ collector_runner.shutdown
76
+ end
77
+ @adapter_runners.each_value do |adapter_runner|
78
+ adapter_runner.shutdown
79
+ end
80
+ @farm.stop_gracefully(&on_stop)
81
+ logger.trace("stop_gracefully: done")
82
+ end
83
+
84
+ def stop_immediately
85
+ logger.trace("stop_immediately: start")
73
86
  @collector_runners.each_value do |collector_runner|
74
87
  collector_runner.shutdown
75
88
  end
@@ -77,6 +90,7 @@ module Droonga
77
90
  adapter_runner.shutdown
78
91
  end
79
92
  @farm.shutdown
93
+ logger.trace("stop_immediately: done")
80
94
  end
81
95
 
82
96
  def process_message(message)
@@ -15,6 +15,6 @@
15
15
 
16
16
  module Droonga
17
17
  class Engine
18
- VERSION = "1.0.5"
18
+ VERSION = "1.0.6"
19
19
  end
20
20
  end
@@ -57,14 +57,19 @@ module Droonga
57
57
  logger.trace("stop_gracefully: start")
58
58
  @live_nodes_list_observer.stop
59
59
  on_finish = lambda do
60
+ logger.trace("stop_gracefully/on_finish: start")
60
61
  output_last_processed_timestamp
61
- @dispatcher.shutdown
62
- @state.shutdown
63
- yield
62
+ @dispatcher.stop_gracefully do
63
+ @state.shutdown
64
+ yield
65
+ end
66
+ logger.trace("stop_gracefully/on_finish: done")
64
67
  end
65
68
  if @state.have_session?
69
+ logger.trace("stop_gracefully/having sessions")
66
70
  @state.on_finish = on_finish
67
71
  else
72
+ logger.trace("stop_gracefully/no session")
68
73
  on_finish.call
69
74
  end
70
75
  logger.trace("stop_gracefully: done")
@@ -75,7 +80,7 @@ module Droonga
75
80
  logger.trace("stop_immediately: start")
76
81
  output_last_processed_timestamp
77
82
  @live_nodes_list_observer.stop
78
- @dispatcher.shutdown
83
+ @dispatcher.stop_immediately
79
84
  @state.shutdown
80
85
  logger.trace("stop_immediately: done")
81
86
  end
@@ -112,11 +117,13 @@ module Droonga
112
117
  end
113
118
 
114
119
  def output_last_processed_timestamp
120
+ logger.trace("output_last_processed_timestamp: start")
115
121
  path = Path.last_processed_timestamp
116
122
  FileUtils.mkdir_p(path.dirname.to_s)
117
123
  path.open("w") do |file|
118
124
  file.write(@last_processed_timestamp)
119
125
  end
126
+ logger.trace("output_last_processed_timestamp: done")
120
127
  end
121
128
 
122
129
  def effective_message?(message)
@@ -90,6 +90,7 @@ module Droonga
90
90
 
91
91
  def register_session(id, session)
92
92
  @sessions[id] = session
93
+ logger.trace("new session #{id} is registered. rest sessions=#{@sessions.size}")
93
94
  end
94
95
 
95
96
  def unregister_session(id)
@@ -97,6 +98,7 @@ module Droonga
97
98
  unless have_session?
98
99
  @on_finish.call if @on_finish
99
100
  end
101
+ logger.trace("session #{id} is unregistered. rest sessions=#{@sessions.size}")
100
102
  end
101
103
 
102
104
  def have_session?
data/lib/droonga/farm.rb CHANGED
@@ -49,14 +49,23 @@ module Droonga
49
49
  end
50
50
  end
51
51
 
52
- def shutdown
53
- threads = []
52
+ def stop_gracefully
53
+ n_slices = @slices.size
54
+ n_done_slices = 0
54
55
  @slices.each_value do |slice|
55
- threads << Thread.new do
56
- slice.shutdown
56
+ slice.stop_gracefully do
57
+ n_done_slices += 1
58
+ if n_done_slices == n_slices
59
+ yield if block_given?
60
+ end
57
61
  end
58
62
  end
59
- threads.each(&:join)
63
+ end
64
+
65
+ def stop_immediately
66
+ @slices.each_value do |slice|
67
+ slice.stop_immediately
68
+ end
60
69
  end
61
70
 
62
71
  def process(slice_name, message)
@@ -43,6 +43,7 @@ module Droonga
43
43
  def stop_gracefully
44
44
  logger.trace("stop_gracefully: start")
45
45
  shutdown_heartbeat_receiver
46
+ logger.trace("stop_gracefully: middle")
46
47
  shutdown_server
47
48
  logger.trace("stop_gracefully: done")
48
49
  end
@@ -54,6 +55,12 @@ module Droonga
54
55
  logger.trace("stop_immediately: done")
55
56
  end
56
57
 
58
+ def shutdown_clients
59
+ @clients.dup.each do |client|
60
+ client.close
61
+ end
62
+ end
63
+
57
64
  private
58
65
  def start_heartbeat_receiver
59
66
  logger.trace("start_heartbeat_receiver: start")
@@ -76,6 +83,9 @@ module Droonga
76
83
  client = Client.new(connection) do |tag, time, record|
77
84
  @on_message.call(tag, time, record)
78
85
  end
86
+ client.on_close = lambda do
87
+ @clients.delete(client)
88
+ end
79
89
  @clients << client
80
90
  end
81
91
  @loop.attach(@server)
@@ -88,13 +98,9 @@ module Droonga
88
98
  end
89
99
 
90
100
  def shutdown_server
101
+ logger.trace("shutdown_server: start")
91
102
  @server.close
92
- end
93
-
94
- def shutdown_clients
95
- @clients.each do |client|
96
- client.close
97
- end
103
+ logger.trace("shutdown_server: done")
98
104
  end
99
105
 
100
106
  def log_tag
@@ -152,16 +158,27 @@ module Droonga
152
158
  class Client
153
159
  include Loggable
154
160
 
161
+ attr_accessor :on_close
155
162
  def initialize(io, &on_message)
156
163
  @io = io
157
164
  @on_message = on_message
165
+ @on_close = nil
158
166
  @unpacker = MessagePack::Unpacker.new
167
+
159
168
  on_read = lambda do |data|
160
169
  feed(data)
161
170
  end
162
171
  @io.on_read do |data|
163
172
  on_read.call(data)
164
173
  end
174
+
175
+ on_close = lambda do
176
+ @io = nil
177
+ @on_close.call if @on_close
178
+ end
179
+ @io.on_close do
180
+ on_close.call
181
+ end
165
182
  end
166
183
 
167
184
  def close
@@ -34,6 +34,7 @@ module Droonga
34
34
  @host = host
35
35
  @port = port
36
36
  @socket = nil
37
+ @packer = MessagePackPacker.new
37
38
  @buffering = options[:buffering]
38
39
  end
39
40
 
@@ -114,7 +115,10 @@ module Droonga
114
115
 
115
116
  def create_packed_fluent_message(tag, data)
116
117
  fluent_message = [tag, Time.now.to_i, data]
117
- MessagePackPacker.pack(fluent_message)
118
+ @packer.pack(fluent_message)
119
+ packed_fluent_message = @packer.to_s
120
+ @packer.clear
121
+ packed_fluent_message
118
122
  end
119
123
 
120
124
  def log_tag
@@ -0,0 +1,67 @@
1
+ # Copyright (C) 2014 Droonga Project
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License version 2.1 as published by the Free Software Foundation.
6
+ #
7
+ # This library is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10
+ # Lesser General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU Lesser General Public
13
+ # License along with this library; if not, write to the Free Software
14
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
+
16
+ require "json"
17
+ require "droonga/path"
18
+ require "droonga/safe_file_writer"
19
+
20
+ module Droonga
21
+ class NodeStatus
22
+ def initialize
23
+ @status = load
24
+ end
25
+
26
+ def have?(key)
27
+ key = normalize_key(key)
28
+ @status.include?(key)
29
+ end
30
+
31
+ def get(key)
32
+ key = normalize_key(key)
33
+ @status[key]
34
+ end
35
+
36
+ def set(key, value)
37
+ key = normalize_key(key)
38
+ @status[key] = value
39
+ SafeFileWriter.write(status_file, JSON.pretty_generate(@status))
40
+ end
41
+
42
+ def delete(key)
43
+ key = normalize_key(key)
44
+ @status.delete(key)
45
+ SafeFileWriter.write(status_file, JSON.pretty_generate(@status))
46
+ end
47
+
48
+ private
49
+ def normalize_key(key)
50
+ key.to_sym
51
+ end
52
+
53
+ def status_file
54
+ @status_file ||= Path.state + "status_file"
55
+ end
56
+
57
+ def load
58
+ if status_file.exist?
59
+ contents = status_file.read
60
+ unless contents.empty?
61
+ return JSON.parse(contents, :symbolize_names => true)
62
+ end
63
+ end
64
+ {}
65
+ end
66
+ end
67
+ end