solr_wrapper 0.0.7 → 1.0.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.
@@ -2,33 +2,64 @@ require 'digest'
2
2
  require 'fileutils'
3
3
  require 'json'
4
4
  require 'open-uri'
5
- require 'ruby-progressbar'
6
5
  require 'securerandom'
6
+ require 'socket'
7
7
  require 'stringio'
8
8
  require 'tmpdir'
9
9
  require 'zip'
10
+ require 'erb'
11
+ require 'yaml'
10
12
 
11
13
  module SolrWrapper
12
14
  class Instance
13
- attr_reader :options
15
+ attr_reader :config, :md5
14
16
 
15
17
  ##
16
18
  # @param [Hash] options
17
19
  # @option options [String] :url
18
- # @option options [String] :version
19
- # @option options [String] :port
20
- # @option options [String] :version_file
21
- # @option options [String] :instance_dir
22
- # @option options [String] :download_path
23
- # @option options [String] :md5sum
24
- # @option options [Boolean] :verbose
25
- # @option options [Boolean] :managed
20
+ # @option options [String] :instance_dir Directory to store the solr index files
21
+ # @option options [String] :version Solr version to download and install
22
+ # @option options [String] :port port to run Solr on
23
+ # @option options [Boolean] :cloud Run solr in cloud mode
24
+ # @option options [String] :version_file Local path to store the currently installed version
25
+ # @option options [String] :download_dir Local directory to store the downloaded Solr zip and its md5 file in (overridden by :solr_zip_path)
26
+ # @option options [String] :solr_zip_path Local path for storing the downloaded Solr zip file
27
+ # @option options [Boolean] :validate Should solr_wrapper download a new md5 and (re-)validate the zip file? (default: trueF)
28
+ # @option options [String] :md5sum Path/URL to MD5 checksum
29
+ # @option options [String] :solr_xml Path to Solr configuration
30
+ # @option options [String] :extra_lib_dir Path to directory containing extra libraries to copy into instance_dir/lib
31
+ # @option options [Boolean] :verbose return verbose info when running solr commands
26
32
  # @option options [Boolean] :ignore_md5sum
27
- def initialize options = {}
28
- @options = options
33
+ # @option options [Hash] :solr_options
34
+ # @option options [Hash] :env
35
+ # @option options [String] :config
36
+ def initialize(options = {})
37
+ @config = Settings.new(Configuration.new(options))
38
+ @md5 = MD5.new(@config)
29
39
  end
30
40
 
31
- def wrap &block
41
+ def host
42
+ config.host
43
+ end
44
+
45
+ def port
46
+ config.port
47
+ end
48
+
49
+ def url
50
+ config.url
51
+ end
52
+
53
+ def instance_dir
54
+ config.instance_dir
55
+ end
56
+
57
+ def version
58
+ config.version
59
+ end
60
+
61
+ def wrap(&_block)
62
+ extract_and_configure
32
63
  start
33
64
  yield self
34
65
  ensure
@@ -38,37 +69,58 @@ module SolrWrapper
38
69
  ##
39
70
  # Start Solr and wait for it to become available
40
71
  def start
41
- extract
42
- if managed?
43
- exec('start', p: port)
72
+ extract_and_configure
73
+ if config.managed?
74
+ exec('start', p: port, c: config.cloud)
44
75
 
45
76
  # Wait for solr to start
46
77
  unless status
47
- sleep 1
78
+ sleep config.poll_interval
48
79
  end
80
+
81
+ after_start
49
82
  end
50
83
  end
51
84
 
52
85
  ##
53
86
  # Stop Solr and wait for it to finish exiting
54
87
  def stop
55
- if managed? and started?
56
-
88
+ if config.managed? && started?
57
89
  exec('stop', p: port)
58
- # Wait for solr to stop
59
- while status
60
- sleep 1
61
- end
90
+ wait
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Stop Solr and wait for it to finish exiting
96
+ def restart
97
+ if config.managed? && started?
98
+ exec('restart', p: port, c: config.cloud)
62
99
  end
63
100
  end
64
101
 
65
102
  ##
66
103
  # Check the status of a managed Solr service
67
104
  def status
68
- return true unless managed?
105
+ return true unless config.managed?
69
106
 
70
- out = exec('status', output: true).read
107
+ out = exec('status').read
71
108
  out =~ /running on port #{port}/
109
+ rescue
110
+ false
111
+ end
112
+
113
+ def pid
114
+ return unless config.managed?
115
+
116
+ @pid ||= begin
117
+ out = exec('status').read
118
+ out.match(/process (?<pid>\d+) running on port #{port}/) do |m|
119
+ m[:pid].to_i
120
+ end
121
+ end
122
+ rescue
123
+ nil
72
124
  end
73
125
 
74
126
  ##
@@ -77,23 +129,73 @@ module SolrWrapper
77
129
  !!status
78
130
  end
79
131
 
132
+ def wait
133
+ while (Process.getpgid(pid) rescue status)
134
+ sleep config.poll_interval
135
+ end
136
+ end
137
+
80
138
  ##
81
139
  # Create a new collection in solr
82
140
  # @param [Hash] options
83
141
  # @option options [String] :name
84
142
  # @option options [String] :dir
85
- def create options = {}
143
+ def create(options = {})
86
144
  options[:name] ||= SecureRandom.hex
87
145
 
88
- exec("create", c: options[:name], d: options[:dir], p: port)
146
+ create_options = { p: port }
147
+ create_options[:c] = options[:name] if options[:name]
148
+ create_options[:n] = options[:config_name] if options[:config_name]
149
+ create_options[:d] = options[:dir] if options[:dir]
150
+
151
+ # short-circuit if we're using persisted data with an existing core/collection
152
+ return if options[:persist] && create_options[:c] && client.exists?(create_options[:c])
153
+
154
+ exec("create", create_options)
89
155
 
90
156
  options[:name]
91
157
  end
92
-
158
+
159
+ ##
160
+ # Update the collection configuration in zookeeper
161
+ # @param [Hash] options
162
+ # @option options [String] :config_name
163
+ # @option options [String] :dir
164
+ def upconfig(options = {})
165
+ options[:name] ||= SecureRandom.hex
166
+ options[:zkhost] ||= zkhost
167
+
168
+ upconfig_options = { upconfig: true, n: options[:name] }
169
+ upconfig_options[:d] = options[:dir] if options[:dir]
170
+ upconfig_options[:z] = options[:zkhost] if options[:zkhost]
171
+
172
+ exec 'zk', upconfig_options
173
+
174
+ options[:name]
175
+ end
176
+
177
+ ##
178
+ # Copy the collection configuration from zookeeper to a local directory
179
+ # @param [Hash] options
180
+ # @option options [String] :config_name
181
+ # @option options [String] :dir
182
+ def downconfig(options = {})
183
+ options[:name] ||= SecureRandom.hex
184
+ options[:zkhost] ||= zkhost
185
+
186
+ downconfig_options = { downconfig: true, n: options[:name] }
187
+ downconfig_options[:d] = options[:dir] if options[:dir]
188
+ downconfig_options[:z] = options[:zkhost] if options[:zkhost]
189
+
190
+ exec 'zk', downconfig_options
191
+
192
+ options[:name]
193
+ end
194
+
93
195
  ##
94
196
  # Create a new collection in solr
95
197
  # @param [String] name collection name
96
- def delete name, options = {}
198
+ def delete(name, _options = {})
97
199
  exec("delete", c: name, p: port)
98
200
  end
99
201
 
@@ -102,35 +204,52 @@ module SolrWrapper
102
204
  # @param [Hash] options
103
205
  # @option options [String] :name
104
206
  # @option options [String] :dir
105
- def with_collection options = {}
207
+ def with_collection(options = {})
208
+ options = config.collection_options.merge(options)
209
+ return yield if options.empty?
210
+
106
211
  name = create(options)
107
212
  begin
108
213
  yield name
109
214
  ensure
110
- delete name
215
+ delete name unless options[:persist]
111
216
  end
112
217
  end
113
218
 
114
- ##
115
- # Get the port this Solr instance is running at
116
- def port
117
- options.fetch(:port, "8983").to_s
118
- end
119
-
120
219
  ##
121
220
  # Clean up any files solr_wrapper may have downloaded
122
221
  def clean!
123
222
  stop
124
- FileUtils.remove_entry(download_path) if File.exists? download_path
125
- FileUtils.remove_entry(tmp_save_dir, true) if File.exists? tmp_save_dir
126
- FileUtils.remove_entry(instance_dir, true) if File.exists? instance_dir
127
- FileUtils.remove_entry(md5sum_path) if File.exists? md5sum_path
128
- FileUtils.remove_entry(version_file) if File.exists? version_file
223
+ remove_instance_dir!
224
+ FileUtils.remove_entry(config.download_dir, true) if File.exist?(config.download_dir)
225
+ FileUtils.remove_entry(config.tmp_save_dir, true) if File.exist? config.tmp_save_dir
226
+ md5.clean!
227
+ FileUtils.remove_entry(config.version_file) if File.exist? config.version_file
129
228
  end
130
229
 
131
- protected
230
+ ##
231
+ # Clean up any files in the Solr instance dir
232
+ def remove_instance_dir!
233
+ FileUtils.remove_entry(instance_dir, true) if File.exist? instance_dir
234
+ end
235
+
236
+ def configure
237
+ raise_error_unless_extracted
238
+ FileUtils.cp config.solr_xml, File.join(config.instance_dir, 'server', 'solr', 'solr.xml') if config.solr_xml
239
+ FileUtils.cp_r File.join(config.extra_lib_dir, '.'), File.join(config.instance_dir, 'server', 'solr', 'lib') if config.extra_lib_dir
240
+ end
241
+
242
+ def extract_and_configure
243
+ extract.tap { configure }
244
+ end
245
+
246
+ # rubocop:disable Lint/RescueException
247
+
248
+ # extract a copy of solr to instance_dir
249
+ # Does noting if solr already exists at instance_dir
250
+ # @return [String] instance_dir Directory where solr has been installed
132
251
  def extract
133
- return solr_dir if File.exists?(solr_binary) and extracted_version == version
252
+ return config.instance_dir if extracted?
134
253
 
135
254
  zip_path = download
136
255
 
@@ -138,196 +257,140 @@ module SolrWrapper
138
257
  Zip::File.open(zip_path) do |zip_file|
139
258
  # Handle entries one by one
140
259
  zip_file.each do |entry|
141
- dest_file = File.join(tmp_save_dir,entry.name)
142
- FileUtils.remove_entry(dest_file,true)
260
+ dest_file = File.join(config.tmp_save_dir, entry.name)
261
+ FileUtils.remove_entry(dest_file, true)
143
262
  entry.extract(dest_file)
144
263
  end
145
264
  end
146
265
 
147
266
  rescue Exception => e
148
- abort "Unable to unzip #{zip_path} into #{tmp_save_dir}: #{e.message}"
267
+ abort "Unable to unzip #{zip_path} into #{config.tmp_save_dir}: #{e.message}"
149
268
  end
150
269
 
151
- begin
152
- FileUtils.remove_dir(solr_dir,true)
153
- FileUtils.cp_r File.join(tmp_save_dir, File.basename(default_url, ".zip")), solr_dir
154
- self.extracted_version = version
155
- FileUtils.chmod 0755, solr_binary
270
+ begin
271
+ FileUtils.remove_dir(config.instance_dir, true)
272
+ FileUtils.cp_r File.join(config.tmp_save_dir, File.basename(config.download_url, ".zip")), config.instance_dir
273
+ self.extracted_version = config.version
274
+ FileUtils.chmod 0755, config.solr_binary
156
275
  rescue Exception => e
157
- abort "Unable to copy #{tmp_save_dir} to #{solr_dir}: #{e.message}"
276
+ abort "Unable to copy #{config.tmp_save_dir} to #{config.instance_dir}: #{e.message}"
158
277
  end
159
278
 
160
- solr_dir
279
+ config.instance_dir
161
280
  ensure
162
- FileUtils.remove_entry tmp_save_dir if File.exists? tmp_save_dir
281
+ FileUtils.remove_entry config.tmp_save_dir if File.exist? config.tmp_save_dir
163
282
  end
283
+ # rubocop:enable Lint/RescueException
164
284
 
165
- def download
166
- unless File.exists? download_path and validate? download_path
167
- fetch_with_progressbar url, download_path
168
- validate! download_path
169
- end
170
-
171
- download_path
172
- end
285
+ protected
173
286
 
174
- def validate? file
175
- Digest::MD5.file(file).hexdigest == expected_md5sum
287
+ def extracted?
288
+ File.exist?(config.solr_binary) && extracted_version == config.version
176
289
  end
177
290
 
178
- def validate! file
179
- unless validate? file
180
- raise "MD5 mismatch" unless options[:ignore_md5sum]
291
+ def download
292
+ unless File.exist?(config.solr_zip_path) && md5.validate?(config.solr_zip_path)
293
+ Downloader.fetch_with_progressbar config.download_url, config.solr_zip_path
294
+ md5.validate! config.solr_zip_path
181
295
  end
296
+ config.solr_zip_path
182
297
  end
183
298
 
184
299
  ##
185
300
  # Run a bin/solr command
301
+ # @param [String] cmd command to run
302
+ # @param [Hash] options key-value pairs to transform into command line arguments
303
+ # @return [StringIO] an IO object for the executed shell command
186
304
  # @see https://github.com/apache/lucene-solr/blob/trunk/solr/bin/solr
187
- def exec cmd, options = {}
188
- output = !!options.delete(:output)
189
- args = [solr_binary, cmd] + options.map { |k,v| ["-#{k}", "#{v}"] }.flatten
305
+ # If you want to pass a boolean flag, include it in the +options+ hash with its value set to +true+
306
+ # the key will be converted into a boolean flag for you.
307
+ # @example start solr in cloud mode on port 8983
308
+ # exec('start', {p: '8983', c: true})
309
+ def exec(cmd, options = {})
310
+ silence_output = !options.delete(:output)
311
+
312
+ args = [config.solr_binary, cmd] + config.solr_options.merge(options).map do |k, v|
313
+ case v
314
+ when true
315
+ "-#{k}"
316
+ when false, nil
317
+ # don't return anything
318
+ else
319
+ ["-#{k}", "#{v}"]
320
+ end
321
+ end.flatten.compact
190
322
 
191
323
  if IO.respond_to? :popen4
192
324
  # JRuby
193
- pid, input, output, error = IO.popen4(args.join(" "))
194
-
325
+ env_str = config.env.map { |k, v| "#{Shellwords.escape(k)}=#{Shellwords.escape(v)}" }.join(" ")
326
+ pid, input, output, error = IO.popen4(env_str + " " + args.join(" "))
195
327
  stringio = StringIO.new
196
- if verbose? and !output
197
- IO.copy_stream(output,$stderr)
198
- IO.copy_stream(error,$stderr)
328
+ if config.verbose? && !silence_output
329
+ IO.copy_stream(output, $stderr)
330
+ IO.copy_stream(error, $stderr)
199
331
  else
200
332
  IO.copy_stream(output, stringio)
201
333
  IO.copy_stream(error, stringio)
202
334
  end
335
+
203
336
  input.close
204
337
  output.close
205
338
  error.close
206
-
207
- stringio.rewind
208
-
209
- if $? != 0
210
- raise "Failed to execute solr #{cmd}: #{stringio.read}"
211
- end
212
-
213
- stringio
339
+ exit_status = Process.waitpid2(pid).last
214
340
  else
215
- IO.popen(args + [err: [:child, :out]]) do |io|
341
+ IO.popen(config.env, args + [err: [:child, :out]]) do |io|
216
342
  stringio = StringIO.new
217
- if verbose? and !output
218
- IO.copy_stream(io,$stderr)
343
+
344
+ if config.verbose? && !silence_output
345
+ IO.copy_stream(io, $stderr)
219
346
  else
220
347
  IO.copy_stream(io, stringio)
221
348
  end
222
- _, exit_status = Process.wait2(io.pid)
223
-
224
- stringio.rewind
225
-
226
- if exit_status != 0
227
- raise "Failed to execute solr #{cmd}: #{stringio.read}"
228
- end
229
349
 
230
- stringio
350
+ _, exit_status = Process.wait2(io.pid)
231
351
  end
232
352
  end
233
- end
234
-
235
- private
236
- def url
237
- @download_url ||= options.fetch(:url, default_url)
238
- end
239
353
 
240
- def default_url
241
- @default_url ||= begin
242
- mirror_url = "http://www.apache.org/dyn/closer.cgi/lucene/solr/#{version}/solr-#{version}.zip?asjson=true"
243
- json = open(mirror_url).read
244
- doc = JSON.parse(json)
245
- doc['preferred'] + doc['path_info']
354
+ stringio.rewind
355
+ if exit_status != 0 && cmd != 'status'
356
+ raise "Failed to execute solr #{cmd}: #{stringio.read}. Further information may be available in #{instance_dir}/logs"
246
357
  end
247
- end
248
-
249
- def md5url
250
- "http://www.us.apache.org/dist/lucene/solr/#{version}/solr-#{version}.zip.md5"
251
- end
252
-
253
- def version
254
- @version ||= options.fetch(:version, default_solr_version)
255
- end
256
358
 
257
- def default_solr_version
258
- SolrWrapper.default_solr_version
359
+ stringio
259
360
  end
260
361
 
261
- def download_path
262
- @download_path ||= options.fetch(:download_path, default_download_path)
263
- end
264
-
265
- def default_download_path
266
- File.join(Dir.tmpdir, File.basename(default_url))
267
- end
268
-
269
- def solr_dir
270
- @solr_dir ||= options.fetch(:instance_dir, File.join(Dir.tmpdir, File.basename(default_url, ".zip")))
271
- end
272
-
273
- def verbose?
274
- !!options.fetch(:verbose, false)
275
- end
362
+ private
276
363
 
277
- def managed?
278
- !!options.fetch(:managed, true)
364
+ def after_start
365
+ create_configsets if config.cloud
279
366
  end
280
367
 
281
- def version_file
282
- options.fetch(:version_file, File.join(solr_dir, "VERSION"))
283
- end
284
-
285
- def expected_md5sum
286
- @md5sum ||= options.fetch(:md5sum, open(md5file).read.split(" ").first)
287
- end
288
-
289
- def solr_binary
290
- File.join(solr_dir, "bin", "solr")
291
- end
292
-
293
- def md5sum_path
294
- File.join(Dir.tmpdir, File.basename(md5url))
368
+ def create_configsets
369
+ config.configsets.each do |configset|
370
+ upconfig(configset)
371
+ end
295
372
  end
296
373
 
297
- def tmp_save_dir
298
- @tmp_save_dir ||= Dir.mktmpdir
374
+ def extracted_version
375
+ File.read(config.version_file).strip if File.exist? config.version_file
299
376
  end
300
377
 
301
- def fetch_with_progressbar url, output
302
- pbar = ProgressBar.create(title: File.basename(url), total: nil, format: "%t: |%B| %p%% (%e )")
303
- open(url, content_length_proc: lambda {|t|
304
- if t && 0 < t
305
- pbar.total = t
306
- end
307
- },
308
- progress_proc: lambda {|s|
309
- pbar.progress = s
310
- }) do |io|
311
- IO.copy_stream(io,output)
378
+ def extracted_version=(version)
379
+ File.open(config.version_file, "w") do |f|
380
+ f.puts version
312
381
  end
313
382
  end
314
383
 
315
- def md5file
316
- unless File.exists? md5sum_path
317
- fetch_with_progressbar md5url, md5sum_path
318
- end
319
-
320
- md5sum_path
384
+ def zkhost
385
+ "#{config.zookeeper_host}:#{config.zookeeper_port}" if config.cloud
321
386
  end
322
387
 
323
- def extracted_version
324
- File.read(version_file).strip if File.exists? version_file
388
+ def client
389
+ SolrWrapper::Client.new(url)
325
390
  end
326
391
 
327
- def extracted_version= version
328
- File.open(version_file, "w") do |f|
329
- f.puts version
330
- end
392
+ def raise_error_unless_extracted
393
+ raise RuntimeError, "there is no solr instance at #{config.instance_dir}. Run SolrWrapper.extract first." unless extracted?
331
394
  end
332
395
  end
333
- end
396
+ end
@@ -0,0 +1,43 @@
1
+ module SolrWrapper
2
+ class MD5
3
+ attr_reader :config
4
+ def initialize(config)
5
+ @config = config
6
+ end
7
+
8
+ def clean!
9
+ FileUtils.remove_entry(config.md5sum_path) if File.exist? config.md5sum_path
10
+ end
11
+
12
+ def validate?(file)
13
+ return true if config.validate == false
14
+
15
+ Digest::MD5.file(file).hexdigest == expected_sum
16
+ end
17
+
18
+ def validate!(file)
19
+ unless validate? file
20
+ raise "MD5 mismatch" unless config.ignore_md5sum
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def expected_sum
27
+ @md5sum ||= config.md5sum
28
+ @md5sum ||= read_file
29
+ end
30
+
31
+ def read_file
32
+ open(md5file).read.split(" ").first
33
+ end
34
+
35
+ def md5file
36
+ unless File.exist? config.md5sum_path
37
+ Downloader.fetch_with_progressbar config.md5url, config.md5sum_path
38
+ end
39
+
40
+ config.md5sum_path
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ module SolrWrapper
2
+ module RakeTask
3
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),"tasks/*.rake"))].each { |ext| load ext } if defined?(Rake)
4
+ end
5
+ end