Branch data Line data Source code
1 : : /** @file compactor.cc
2 : : * @brief Compact a database, or merge and compact several.
3 : : */
4 : : /* Copyright (C) 2003,2004,2005,2006,2007,2008,2009,2010,2011 Olly Betts
5 : : * Copyright (C) 2008 Lemur Consulting Ltd
6 : : *
7 : : * This program is free software; you can redistribute it and/or
8 : : * modify it under the terms of the GNU General Public License as
9 : : * published by the Free Software Foundation; either version 2 of the
10 : : * License, or (at your option) any later version.
11 : : *
12 : : * This program is distributed in the hope that it will be useful,
13 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 : : * GNU General Public License for more details.
16 : : *
17 : : * You should have received a copy of the GNU General Public License
18 : : * along with this program; if not, write to the Free Software
19 : : * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
20 : : * USA
21 : : */
22 : :
23 : : #include <config.h>
24 : :
25 : : #include <xapian/compactor.h>
26 : :
27 : : #include "safeerrno.h"
28 : :
29 : : #include <algorithm>
30 : : #include <fstream>
31 : :
32 : : #include <cstdio> // for rename()
33 : : #include <cstdlib>
34 : : #include <cstring>
35 : : #include <ctime>
36 : : #include "safesysstat.h"
37 : : #include <sys/types.h>
38 : :
39 : : #include "safeunistd.h"
40 : : #include "safefcntl.h"
41 : :
42 : : #include "noreturn.h"
43 : : #include "omassert.h"
44 : : #include "fileutils.h"
45 : : #ifdef __WIN32__
46 : : # include "msvc_posix_wrapper.h"
47 : : #endif
48 : : #include "stringutils.h"
49 : : #include "str.h"
50 : : #include "utils.h"
51 : :
52 : : #include "backends/brass/brass_compact.h"
53 : : #include "backends/brass/brass_version.h"
54 : : #include "backends/chert/chert_compact.h"
55 : : #include "backends/chert/chert_version.h"
56 : : #include "backends/flint/flint_compact.h"
57 : : #include "backends/flint/flint_version.h"
58 : :
59 : : #include <xapian.h>
60 : :
61 : : using namespace std;
62 : :
63 : : class CmpByFirstUsed {
64 : : const vector<pair<Xapian::docid, Xapian::docid> > & used_ranges;
65 : :
66 : : public:
67 : 24 : CmpByFirstUsed(const vector<pair<Xapian::docid, Xapian::docid> > & ur)
68 : 24 : : used_ranges(ur) { }
69 : :
70 : 63 : bool operator()(size_t a, size_t b) {
71 : 63 : return used_ranges[a].first < used_ranges[b].first;
72 : : }
73 : : };
74 : :
75 : : static const char * backend_names[] = {
76 : : NULL,
77 : : "brass",
78 : : "chert",
79 : : "flint"
80 : : };
81 : :
82 : : enum { STUB_NO, STUB_FILE, STUB_DIR };
83 : :
84 : : namespace Xapian {
85 : :
86 : 48 : class Compactor::Internal : public Xapian::Internal::RefCntBase {
87 : : friend class Compactor;
88 : :
89 : : string destdir;
90 : : bool renumber;
91 : : bool multipass;
92 : : int compact_to_stub;
93 : : size_t block_size;
94 : : compaction_level compaction;
95 : :
96 : : Xapian::docid tot_off;
97 : : Xapian::docid last_docid;
98 : :
99 : : enum { UNKNOWN, BRASS, CHERT, FLINT } backend;
100 : :
101 : : struct stat sb;
102 : :
103 : : string first_source;
104 : :
105 : : vector<string> sources;
106 : : vector<Xapian::docid> offset;
107 : : vector<pair<Xapian::docid, Xapian::docid> > used_ranges;
108 : : public:
109 : 48 : Internal()
110 : : : renumber(true), multipass(false),
111 : : block_size(8192), compaction(FULL), tot_off(0),
112 : 48 : last_docid(0), backend(UNKNOWN)
113 : : {
114 : 48 : }
115 : :
116 : : void set_destdir(const string & destdir_);
117 : :
118 : : void add_source(const string & srcdir);
119 : :
120 : : void compact(Xapian::Compactor & compactor);
121 : : };
122 : :
123 : 48 : Compactor::Compactor() : internal(new Compactor::Internal()) { }
124 : :
125 [ # # ][ - + ]: 48 : Compactor::~Compactor() { }
[ # # ]
126 : :
127 : : void
128 : 0 : Compactor::set_block_size(size_t block_size)
129 : : {
130 : 0 : internal->block_size = block_size;
131 : 0 : }
132 : :
133 : : void
134 : 27 : Compactor::set_renumber(bool renumber)
135 : : {
136 : 27 : internal->renumber = renumber;
137 : 27 : }
138 : :
139 : : void
140 : 0 : Compactor::set_multipass(bool multipass)
141 : : {
142 : 0 : internal->multipass = multipass;
143 : 0 : }
144 : :
145 : : void
146 : 0 : Compactor::set_compaction_level(compaction_level compaction)
147 : : {
148 : 0 : internal->compaction = compaction;
149 : 0 : }
150 : :
151 : : void
152 : 48 : Compactor::set_destdir(const string & destdir)
153 : : {
154 : 48 : internal->set_destdir(destdir);
155 : 48 : }
156 : :
157 : : void
158 : 93 : Compactor::add_source(const string & srcdir)
159 : : {
160 : 93 : internal->add_source(srcdir);
161 : 93 : }
162 : :
163 : : void
164 : 48 : Compactor::compact()
165 : : {
166 : 48 : internal->compact(*this);
167 : 33 : }
168 : :
169 : : void
170 : 418 : Compactor::set_status(const string & table, const string & status)
171 : : {
172 : : (void)table;
173 : : (void)status;
174 : 418 : }
175 : :
176 : : string
177 : 0 : Compactor::resolve_duplicate_metadata(const string & key,
178 : : size_t num_tags, const std::string tags[])
179 : : {
180 : : (void)key;
181 : : (void)num_tags;
182 : 0 : return tags[0];
183 : : }
184 : :
185 : : }
186 : :
187 : : XAPIAN_NORETURN(
188 : : static void
189 : : backend_mismatch(const string &dbpath1, int backend1,
190 : : const string &dbpath2, int backend2)
191 : : );
192 : : static void
193 : 0 : backend_mismatch(const string &dbpath1, int backend1,
194 : : const string &dbpath2, int backend2)
195 : : {
196 : 0 : string msg = "All databases must be the same type ('";
197 : 0 : msg += dbpath1;
198 : 0 : msg += "' is ";
199 : 0 : msg += backend_names[backend1];
200 : 0 : msg += ", but '";
201 : 0 : msg += dbpath2;
202 : 0 : msg += "' is ";
203 : 0 : msg += backend_names[backend2];
204 : 0 : msg += ')';
205 : 0 : throw Xapian::InvalidArgumentError(msg);
206 : : }
207 : :
208 : : namespace Xapian {
209 : :
210 : : void
211 : 48 : Compactor::Internal::set_destdir(const string & destdir_) {
212 : 48 : destdir = destdir_;
213 : 48 : compact_to_stub = STUB_NO;
214 [ + + + + ]: 48 : if (stat(destdir, &sb) == 0 && S_ISREG(sb.st_mode)) {
[ + + ]
215 : : // Stub file.
216 : 3 : compact_to_stub = STUB_FILE;
217 [ + + ][ + - ]: 45 : } else if (stat(destdir + "/XAPIANDB", &sb) == 0 && S_ISREG(sb.st_mode)) {
[ + - ][ # # ]
[ + + ]
218 : : // Stub directory.
219 : 3 : compact_to_stub = STUB_DIR;
220 : : }
221 : 48 : }
222 : :
223 : : void
224 : 117 : Compactor::Internal::add_source(const string & srcdir)
225 : : {
226 : : // Check destdir isn't the same as any source directory, unless it is a
227 : : // stub database.
228 [ + + ][ - + ]: 117 : if (!compact_to_stub && srcdir == destdir) {
[ - + ]
229 : 0 : throw Xapian::InvalidArgumentError("destination may not be the same as any source directory, unless it is a stub database");
230 : : }
231 : :
232 [ + - ]: 117 : if (stat(srcdir, &sb) == 0) {
233 : 117 : bool is_stub = false;
234 : 117 : string file = srcdir;
235 [ + + ]: 117 : if (S_ISREG(sb.st_mode)) {
236 : : // Stub database file.
237 : 6 : is_stub = true;
238 [ + - ]: 111 : } else if (S_ISDIR(sb.st_mode)) {
239 : 111 : file += "/XAPIANDB";
240 [ + + + - ]: 111 : if (stat(file.c_str(), &sb) == 0 && S_ISREG(sb.st_mode)) {
[ + + ]
241 : : // Stub database directory.
242 : 6 : is_stub = true;
243 : : }
244 : : }
245 [ + + ]: 117 : if (is_stub) {
246 : 12 : ifstream stub(file.c_str());
247 : 12 : string line;
248 : 12 : unsigned int line_no = 0;
249 [ + + ]: 36 : while (getline(stub, line)) {
250 : 24 : ++line_no;
251 [ + - ][ - + ]: 24 : if (line.empty() || line[0] == '#')
[ - + ]
252 : 0 : continue;
253 : 24 : string::size_type space = line.find(' ');
254 [ - + ]: 24 : if (space == string::npos) space = line.size();
255 : :
256 : 24 : string type(line, 0, space);
257 : 24 : line.erase(0, space + 1);
258 : :
259 [ - + # # ]: 24 : if (type == "auto" || type == "chert" || type == "flint" ||
[ # # ][ # # ]
[ + - ]
260 : : type == "brass") {
261 : 24 : resolve_relative_path(line, file);
262 : 24 : add_source(line);
263 : 24 : continue;
264 : : }
265 : :
266 [ # # ][ # # ]: 0 : if (type == "remote" || type == "inmemory") {
[ # # ]
267 : 0 : string msg = "Can't compact stub entry of type '";
268 : 0 : msg += type;
269 : 0 : msg += '\'';
270 : 0 : throw Xapian::InvalidOperationError(msg);
271 : : }
272 : :
273 : 0 : throw Xapian::DatabaseError("Bad line in stub file");
274 : : }
275 : 117 : return;
276 [ + + ]: 117 : }
277 : : }
278 : :
279 [ + + ]: 105 : if (stat(string(srcdir) + "/iamflint", &sb) == 0) {
280 [ + + ]: 35 : if (backend == UNKNOWN) {
281 : 16 : backend = FLINT;
282 [ - + ]: 19 : } else if (backend != FLINT) {
283 : 0 : backend_mismatch(first_source, backend, srcdir, FLINT);
284 : : }
285 [ + + ]: 70 : } else if (stat(string(srcdir) + "/iamchert", &sb) == 0) {
286 [ + + ]: 35 : if (backend == UNKNOWN) {
287 : 16 : backend = CHERT;
288 [ - + ]: 19 : } else if (backend != CHERT) {
289 : 0 : backend_mismatch(first_source, backend, srcdir, CHERT);
290 : : }
291 [ + - ]: 35 : } else if (stat(string(srcdir) + "/iambrass", &sb) == 0) {
292 [ + + ]: 35 : if (backend == UNKNOWN) {
293 : 16 : backend = BRASS;
294 [ - + ]: 19 : } else if (backend != BRASS) {
295 : 0 : backend_mismatch(first_source, backend, srcdir, BRASS);
296 : : }
297 : : } else {
298 : 0 : string msg = srcdir;
299 : 0 : msg += ": not a flint, chert or brass database";
300 : 0 : throw Xapian::InvalidArgumentError(msg);
301 : : }
302 : :
303 [ + + ]: 105 : if (first_source.empty())
304 : 48 : first_source = srcdir;
305 : :
306 : 105 : Xapian::Database db(srcdir);
307 : 105 : Xapian::docid first = 0, last = 0;
308 : :
309 : : // "Empty" databases might have spelling or synonym data so can't
310 : : // just be completely ignored.
311 : 105 : Xapian::doccount num_docs = db.get_doccount();
312 [ + - ]: 105 : if (num_docs != 0) {
313 : 105 : Xapian::PostingIterator it = db.postlist_begin(string());
314 : : // This test should never fail, since db.get_doccount() is
315 : : // non-zero!
316 : : Assert(it != db.postlist_end(string()));
317 : 105 : first = *it;
318 : :
319 [ + + + - ]: 105 : if (renumber && first) {
320 : : // Prune any unused docids off the start of this source
321 : : // database.
322 : : //
323 : : // tot_off could wrap here, but it's unsigned, so that's
324 : : // OK.
325 : 39 : tot_off -= (first - 1);
326 : : }
327 : :
328 : : // There may be unused documents at the end of the range.
329 : : // Binary chop using skip_to to find the last actually used
330 : : // document id.
331 : 105 : last = db.get_lastdocid();
332 : 105 : Xapian::docid last_lbound = first + num_docs - 1;
333 [ + + ]: 714 : while (last_lbound < last) {
334 : : Xapian::docid mid;
335 : 609 : mid = last_lbound + (last - last_lbound + 1) / 2;
336 : 609 : it.skip_to(mid);
337 [ + + ]: 609 : if (it == db.postlist_end(string())) {
338 : 528 : last = mid - 1;
339 : 528 : it = db.postlist_begin(string());
340 : 528 : continue;
341 : : }
342 : 81 : last_lbound = *it;
343 : 105 : }
344 : : }
345 : 105 : offset.push_back(tot_off);
346 [ + + ]: 105 : if (renumber)
347 : 39 : tot_off += last;
348 [ + + ]: 66 : else if (last_docid < db.get_lastdocid())
349 : 45 : last_docid = db.get_lastdocid();
350 : 105 : used_ranges.push_back(make_pair(first, last));
351 : :
352 : 117 : sources.push_back(string(srcdir) + '/');
353 : : }
354 : :
355 : : void
356 : 48 : Compactor::Internal::compact(Xapian::Compactor & compactor)
357 : : {
358 [ + + ]: 48 : if (renumber)
359 : 21 : last_docid = tot_off;
360 : :
361 [ + + ][ + + ]: 48 : if (!renumber && sources.size() > 1) {
[ + + ]
362 : : // We want to process the sources in ascending order of first
363 : : // docid. So we create a vector "order" with ascending integers
364 : : // and then sort so the indirected order is right. Then we reorder
365 : : // the vectors into that order and check the ranges are disjoint.
366 : 24 : vector<size_t> order;
367 : 24 : order.reserve(sources.size());
368 [ + + ]: 87 : for (size_t i = 0; i < sources.size(); ++i)
369 : 63 : order.push_back(i);
370 : :
371 : 24 : sort(order.begin(), order.end(), CmpByFirstUsed(used_ranges));
372 : :
373 : : // Reorder the vectors to be in ascending of first docid, and
374 : : // set all the offsets to 0.
375 : 24 : vector<string> sources_(sources.size());
376 : 24 : vector<pair<Xapian::docid, Xapian::docid> > used_ranges_;
377 : 24 : used_ranges_.reserve(sources.size());
378 : :
379 : 24 : Xapian::docid last_start = 0, last_end = 0;
380 [ + + ]: 63 : for (size_t j = 0; j != order.size(); ++j) {
381 : 54 : size_t n = order[j];
382 : :
383 : 54 : swap(sources_[j], sources[n]);
384 : 54 : used_ranges_.push_back(used_ranges[n]);
385 : :
386 : 54 : const pair<Xapian::docid, Xapian::docid> p = used_ranges[n];
387 : : // Skip empty databases.
388 [ - + # # ]: 54 : if (p.first == 0 && p.second == 0)
389 : 0 : continue;
390 : : // Check for overlap with the previous database's range.
391 [ + + ]: 54 : if (p.first <= last_end) {
392 : 15 : string msg = "when merging databases, --no-renumber is only currently supported if the databases have disjoint ranges of used document ids: ";
393 : 15 : msg += sources[order[j - 1]];
394 : 15 : msg += " has range ";
395 : 15 : msg += str(last_start);
396 : 15 : msg += '-';
397 : 15 : msg += str(last_end);
398 : 15 : msg += ", ";
399 : 15 : msg += sources[n];
400 : 15 : msg += " has range ";
401 : 15 : msg += str(p.first);
402 : 15 : msg += '-';
403 : 15 : msg += str(p.second);
404 : 30 : throw Xapian::InvalidOperationError(msg);
405 : : }
406 : 39 : last_start = p.first;
407 : 39 : last_end = p.second;
408 : : }
409 : :
410 : 9 : swap(sources, sources_);
411 : 54 : swap(used_ranges, used_ranges_);
412 : : }
413 : :
414 : 33 : string stub_file;
415 [ + + ]: 33 : if (compact_to_stub) {
416 : 6 : stub_file = destdir;
417 [ + + ]: 6 : if (compact_to_stub == STUB_DIR) {
418 : 3 : stub_file += "/XAPIANDB";
419 : 3 : destdir += '/';
420 : : } else {
421 : 3 : destdir += '_';
422 : : }
423 : 6 : size_t sfx = destdir.size();
424 : 6 : time_t now = time(NULL);
425 : 0 : while (true) {
426 : 6 : destdir.resize(sfx);
427 : 6 : destdir += str(now++);
428 [ - + ]: 6 : if (mkdir(destdir, 0755) == 0)
429 : : break;
430 [ # # ]: 0 : if (errno != EEXIST) {
431 : 0 : string msg = destdir;
432 : 0 : msg += ": mkdir failed";
433 : 0 : throw Xapian::DatabaseError(msg, errno);
434 : : }
435 : : }
436 : : } else {
437 : : // If the destination database directory doesn't exist, create it.
438 [ - + ]: 27 : if (mkdir(destdir, 0755) < 0) {
439 : : // Check why mkdir failed. It's ok if the directory already
440 : : // exists, but we also get EEXIST if there's an existing file with
441 : : // that name.
442 [ # # ]: 0 : if (errno == EEXIST) {
443 [ # # ][ # # ]: 0 : if (stat(destdir, &sb) == 0 && S_ISDIR(sb.st_mode))
[ # # ]
444 : 0 : errno = 0;
445 : : else
446 : 0 : errno = EEXIST; // stat might have changed it
447 : : }
448 [ # # ]: 0 : if (errno) {
449 : 0 : string msg = destdir;
450 : 0 : msg += ": cannot create directory";
451 : 0 : throw Xapian::DatabaseError(msg, errno);
452 : : }
453 : : }
454 : : }
455 : :
456 [ + + ]: 33 : if (backend == CHERT) {
457 : : #ifdef XAPIAN_HAS_CHERT_BACKEND
458 : : compact_chert(compactor, destdir.c_str(), sources, offset, block_size,
459 : 11 : compaction, multipass, last_docid);
460 : : #else
461 : : throw Xapian::FeatureUnavailableError("Chert backend disabled at build time");
462 : : #endif
463 [ + + ]: 22 : } else if (backend == BRASS) {
464 : : #ifdef XAPIAN_HAS_BRASS_BACKEND
465 : : compact_brass(compactor, destdir.c_str(), sources, offset, block_size,
466 : 11 : compaction, multipass, last_docid);
467 : : #else
468 : : throw Xapian::FeatureUnavailableError("Brass backend disabled at build time");
469 : : #endif
470 : : } else {
471 : : #ifdef XAPIAN_HAS_FLINT_BACKEND
472 : : compact_flint(compactor, destdir.c_str(), sources, offset, block_size,
473 : 11 : compaction, multipass, last_docid);
474 : : #else
475 : : throw Xapian::FeatureUnavailableError("Flint backend disabled at build time");
476 : : #endif
477 : : }
478 : :
479 : : // Create the version file ("iamchert", etc).
480 : : //
481 : : // This file contains a UUID, and we want the copy to have a fresh
482 : : // UUID since its revision counter is reset to 1.
483 [ + + ]: 33 : if (backend == CHERT) {
484 : : #ifdef XAPIAN_HAS_CHERT_BACKEND
485 : 11 : ChertVersion(destdir).create();
486 : : #else
487 : : // Handled above.
488 : : exit(1);
489 : : #endif
490 [ + + ]: 22 : } else if (backend == BRASS) {
491 : : #ifdef XAPIAN_HAS_BRASS_BACKEND
492 : 11 : BrassVersion(destdir).create();
493 : : #else
494 : : // Handled above.
495 : : exit(1);
496 : : #endif
497 : : } else {
498 : : #ifdef XAPIAN_HAS_FLINT_BACKEND
499 : 11 : FlintVersion(destdir).create();
500 : : #else
501 : : // Handled above.
502 : : exit(1);
503 : : #endif
504 : : }
505 : :
506 [ + + ]: 33 : if (compact_to_stub) {
507 : 6 : string new_stub_file = destdir;
508 : 6 : new_stub_file += "/new_stub.tmp";
509 : : {
510 : 6 : ofstream new_stub(new_stub_file.c_str());
511 : : #ifndef __WIN32__
512 : 6 : size_t slash = destdir.find_last_of('/');
513 : : #else
514 : : size_t slash = destdir.find_last_of("/\\");
515 : : #endif
516 : 6 : new_stub << "auto " << destdir.substr(slash + 1) << '\n';
517 : : }
518 : : #ifndef __WIN32__
519 [ - + ]: 6 : if (rename(new_stub_file.c_str(), stub_file.c_str()) < 0) {
520 : : #else
521 : : if (msvc_posix_rename(new_stub_file.c_str(), stub_file.c_str()) < 0) {
522 : : #endif
523 : : // FIXME: try to clean up?
524 : 0 : string msg = "Cannot rename '";
525 : 0 : msg += new_stub_file;
526 : 0 : msg += "' to '";
527 : 0 : msg += stub_file;
528 : 0 : msg += '\'';
529 : 0 : throw Xapian::DatabaseError(msg, errno);
530 : 6 : }
531 : 33 : }
532 : 33 : }
533 : :
534 : : }
535 : :
|