xapian-core  2.0.0
api_replicate.cc
Go to the documentation of this file.
1 
4 /* Copyright 2008 Lemur Consulting Ltd
5  * Copyright 2009-2022 Olly Betts
6  * Copyright 2010 Richard Boulton
7  * Copyright 2011 Dan Colish
8  *
9  * This program is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU General Public License as
11  * published by the Free Software Foundation; either version 2 of the
12  * License, or (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, see
21  * <https://www.gnu.org/licenses/>.
22  */
23 
24 #include <config.h>
25 
26 #include "api_replicate.h"
27 
28 #include <xapian.h>
29 #include "api/replication.h"
30 
31 #include "apitest.h"
32 #include "dbcheck.h"
33 #include "errno_to_string.h"
34 #include "fd.h"
35 #include "filetests.h"
36 #include "safedirent.h"
37 #include "safefcntl.h"
38 #include "safesysstat.h"
39 #include "safeunistd.h"
40 #include "setenv.h"
41 #include "testsuite.h"
42 #include "testutils.h"
43 #include "unixcmds.h"
44 
45 #include <sys/types.h>
46 
47 #include <cerrno>
48 #include <cstdlib>
49 #include <string>
50 
51 using namespace std;
52 
53 #ifdef XAPIAN_HAS_REMOTE_BACKEND
54 
55 static void rmtmpdir(const string & path) {
56  rm_rf(path);
57 }
58 
59 static void mktmpdir(const string & path) {
60  rmtmpdir(path);
61  if (mkdir(path.c_str(), 0700) == -1 && errno != EEXIST) {
62  FAIL_TEST("Can't make temporary directory");
63  }
64 }
65 
66 static size_t do_read(int fd, char * p, size_t desired)
67 {
68  size_t total = 0;
69  while (desired) {
70  ssize_t c = read(fd, p, desired);
71  if (c == 0) return total;
72  if (c < 0) {
73  if (errno == EINTR) continue;
74  FAIL_TEST("Error reading from file");
75  }
76  p += c;
77  total += c;
78  desired -= c;
79  }
80  return total;
81 }
82 
83 static void do_write(int fd, const char * p, size_t n)
84 {
85  while (n) {
86  ssize_t c = write(fd, p, n);
87  if (c < 0) {
88  if (errno == EINTR) continue;
89  FAIL_TEST("Error writing to file");
90  }
91  p += c;
92  n -= c;
93  }
94 }
95 
96 // Make a truncated copy of a file.
97 static file_size_type
98 truncated_copy(const string& srcpath,
99  const string& destpath,
100  file_size_type tocopy)
101 {
102  FD fdin(open(srcpath.c_str(), O_RDONLY | O_BINARY));
103  if (fdin == -1) {
104  FAIL_TEST("Open failed (when opening '" << srcpath << "')");
105  }
106 
107  FD fdout(open(destpath.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666));
108  if (fdout == -1) {
109  FAIL_TEST("Open failed (when creating '" << destpath << "')");
110  }
111 
112  const int BUFSIZE = 1024;
113  char buf[BUFSIZE];
114  size_t total_bytes = 0;
115  while (tocopy > 0) {
116  size_t thiscopy = tocopy > BUFSIZE ? BUFSIZE : tocopy;
117  size_t bytes = do_read(fdin, buf, thiscopy);
118  if (thiscopy != bytes) {
119  FAIL_TEST("Couldn't read desired number of bytes from changeset");
120  }
121  tocopy -= bytes;
122  total_bytes += bytes;
123  do_write(fdout, buf, bytes);
124  }
125 
126  if (close(fdout) == -1)
127  FAIL_TEST("Error closing file");
128 
129  return total_bytes;
130 }
131 
132 static void
133 get_changeset(const string & changesetpath,
134  Xapian::DatabaseMaster & master,
135  Xapian::DatabaseReplica & replica,
136  int expected_changesets,
137  int expected_fullcopies,
138  bool expected_changed,
139  bool full_copy = false)
140 {
141  FD fd(open(changesetpath.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666));
142  if (fd == -1) {
143  FAIL_TEST("Open failed (when creating a new changeset file at '"
144  << changesetpath << "')");
145  }
147  master.write_changesets_to_fd(fd,
148  full_copy ? "" : replica.get_revision_info(),
149  &info1);
150 
151  TEST_EQUAL(info1.changeset_count, expected_changesets);
152  TEST_EQUAL(info1.fullcopy_count, expected_fullcopies);
153  TEST_EQUAL(info1.changed, expected_changed);
154 }
155 
156 static int
157 apply_changeset(const string & changesetpath,
158  Xapian::DatabaseReplica & replica,
159  int expected_changesets,
160  int expected_fullcopies,
161  bool expected_changed)
162 {
163  FD fd(open(changesetpath.c_str(), O_RDONLY | O_BINARY));
164  if (fd == -1) {
165  FAIL_TEST("Open failed (when reading changeset file at '"
166  << changesetpath << "')");
167  }
168 
169  int count = 1;
170  replica.set_read_fd(fd);
173  bool client_changed = false;
174  while (replica.apply_next_changeset(&info2, 0)) {
175  ++count;
176  info1.changeset_count += info2.changeset_count;
177  info1.fullcopy_count += info2.fullcopy_count;
178  if (info2.changed)
179  client_changed = true;
180  }
181  info1.changeset_count += info2.changeset_count;
182  info1.fullcopy_count += info2.fullcopy_count;
183  if (info2.changed)
184  client_changed = true;
185 
186  TEST_EQUAL(info1.changeset_count, expected_changesets);
187  TEST_EQUAL(info1.fullcopy_count, expected_fullcopies);
188  TEST_EQUAL(client_changed, expected_changed);
189  return count;
190 }
191 
192 // Replicate from the master to the replica.
193 // Returns the number of changesets which were applied.
194 static int
196  Xapian::DatabaseReplica & replica,
197  const string & tempdir,
198  int expected_changesets,
199  int expected_fullcopies,
200  bool expected_changed,
201  bool full_copy = false)
202 {
203  string changesetpath = tempdir + "/changeset";
204  get_changeset(changesetpath, master, replica,
205  expected_changesets,
206  expected_fullcopies,
207  expected_changed,
208  full_copy);
209  return apply_changeset(changesetpath, replica,
210  expected_changesets,
211  expected_fullcopies,
212  expected_changed);
213 }
214 
215 // Check that the databases held at the given path are identical.
216 static void
217 check_equal_dbs(const string & masterpath, const string & replicapath)
218 {
219  Xapian::Database master(masterpath);
220  Xapian::Database replica(replicapath);
221 
222  TEST_EQUAL(master.get_uuid(), master.get_uuid());
223  dbcheck(replica, master.get_doccount(), master.get_lastdocid());
224 
225  for (Xapian::TermIterator t = master.allterms_begin();
226  t != master.allterms_end(); ++t) {
227  TEST_EQUAL(postlist_to_string(master, *t),
228  postlist_to_string(replica, *t));
229  }
230 }
231 
232 #define set_max_changesets(N) setenv("XAPIAN_MAX_CHANGESETS", #N, 1)
233 
237 };
238 
239 // Ensure that we don't leave generation of changesets on for the next
240 // testcase, even if this one exits with an exception.
241 #define UNSET_MAX_CHANGESETS_AFTERWARDS unset_max_changesets_helper_ ezlxq
242 
243 #endif
244 
245 // #######################################################################
246 // # Tests start here
247 
248 // Basic test of replication functionality.
249 DEFINE_TESTCASE(replicate1, replicas) {
250 #ifdef XAPIAN_HAS_REMOTE_BACKEND
252  string tempdir = ".replicatmp";
253  mktmpdir(tempdir);
254  string masterpath = get_named_writable_database_path("master");
255 
256  set_max_changesets(10);
257 
258  Xapian::Document doc1;
259  doc1.set_data(string("doc1"));
260  doc1.add_posting("doc", 1);
261  doc1.add_posting("one", 1);
262 
264  Xapian::DatabaseMaster master(masterpath);
265  string replicapath = tempdir + "/replica";
266  {
267  Xapian::DatabaseReplica replica(replicapath);
268 
269  // Add a document to the original database.
270  orig.add_document(doc1);
271  orig.commit();
272 
273  // Apply the replication - we don't have changesets stored, so this
274  // should just do a database copy, and return a count of 1.
275  int count = replicate(master, replica, tempdir, 0, 1, true);
276  TEST_EQUAL(count, 1);
277  {
278  Xapian::Database dbcopy(replicapath);
279  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
280  }
281 
282  // Repeating the replication should return a count of 1, since no
283  // further changes should need to be applied.
284  count = replicate(master, replica, tempdir, 0, 0, false);
285  TEST_EQUAL(count, 1);
286  {
287  Xapian::Database dbcopy(replicapath);
288  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
289  }
290  }
291  {
292  // Regression test - if the replica was reopened, a full copy always
293  // used to occur, whether it was needed or not. Fixed in revision
294  // #10117.
295  Xapian::DatabaseReplica replica(replicapath);
296  int count = replicate(master, replica, tempdir, 0, 0, false);
297  TEST_EQUAL(count, 1);
298  {
299  Xapian::Database dbcopy(replicapath);
300  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
301  }
302 
303  orig.add_document(doc1);
304  orig.commit();
305  orig.add_document(doc1);
306  orig.commit();
307 
308  count = replicate(master, replica, tempdir, 2, 0, true);
309  TEST_EQUAL(count, 3);
310  {
311  Xapian::Database dbcopy(replicapath);
312  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
313  }
314 
315  check_equal_dbs(masterpath, replicapath);
316 
317  // We need this inner scope to we close the replica before we remove
318  // the temporary directory on Windows.
319  }
320 
321  TEST_EQUAL(Xapian::Database::check(masterpath), 0);
322 
323  rmtmpdir(tempdir);
324 #endif
325 }
326 
327 // Test replication from a replicated copy.
328 DEFINE_TESTCASE(replicate2, replicas) {
329 #ifdef XAPIAN_HAS_REMOTE_BACKEND
330  SKIP_TEST_FOR_BACKEND("glass"); // Glass doesn't currently support this.
332 
333  string tempdir = ".replicatmp";
334  mktmpdir(tempdir);
335  string masterpath = get_named_writable_database_path("master");
336 
337  set_max_changesets(10);
338 
339  {
341  Xapian::DatabaseMaster master(masterpath);
342  string replicapath = tempdir + "/replica";
343  Xapian::DatabaseReplica replica(replicapath);
344 
345  Xapian::DatabaseMaster master2(replicapath);
346  string replica2path = tempdir + "/replica2";
347  Xapian::DatabaseReplica replica2(replica2path);
348 
349  // Add a document to the original database.
350  Xapian::Document doc1;
351  doc1.set_data(string("doc1"));
352  doc1.add_posting("doc", 1);
353  doc1.add_posting("one", 1);
354  orig.add_document(doc1);
355  orig.commit();
356 
357  // Apply the replication - we don't have changesets stored, so this
358  // should just do a database copy, and return a count of 1.
359  TEST_EQUAL(replicate(master, replica, tempdir, 0, 1, true), 1);
360  check_equal_dbs(masterpath, replicapath);
361 
362  // Replicate from the replica.
363  TEST_EQUAL(replicate(master2, replica2, tempdir, 0, 1, true), 1);
364  check_equal_dbs(masterpath, replica2path);
365 
366  orig.add_document(doc1);
367  orig.commit();
368  orig.add_document(doc1);
369  orig.commit();
370 
371  // Replicate from the replica - should have no changes.
372  TEST_EQUAL(replicate(master2, replica2, tempdir, 0, 0, false), 1);
373  check_equal_dbs(replicapath, replica2path);
374 
375  // Replicate, and replicate from the replica - should have 2 changes.
376  TEST_EQUAL(replicate(master, replica, tempdir, 2, 0, 1), 3);
377  check_equal_dbs(masterpath, replicapath);
378  TEST_EQUAL(replicate(master2, replica2, tempdir, 2, 0, 1), 3);
379  check_equal_dbs(masterpath, replica2path);
380 
381  // Stop writing changesets, and make a modification
383  orig.close();
385  orig.add_document(doc1);
386  orig.commit();
387 
388  // Replication should do a full copy.
389  TEST_EQUAL(replicate(master, replica, tempdir, 0, 1, true), 1);
390  check_equal_dbs(masterpath, replicapath);
391  TEST_EQUAL(replicate(master2, replica2, tempdir, 0, 1, true), 1);
392  check_equal_dbs(masterpath, replica2path);
393 
394  // Start writing changesets, but only keep 1 in history, and make a
395  // modification.
397  orig.close();
399  orig.add_document(doc1);
400  orig.commit();
401 
402  // Replicate, and replicate from the replica - should have 1 changes.
403  TEST_EQUAL(replicate(master, replica, tempdir, 1, 0, 1), 2);
404  check_equal_dbs(masterpath, replicapath);
405  TEST_EQUAL(replicate(master2, replica2, tempdir, 1, 0, 1), 2);
406  check_equal_dbs(masterpath, replica2path);
407 
408  // Make two changes - only one changeset should be preserved.
409  orig.add_document(doc1);
410  orig.commit();
411 
412  // Replication should do a full copy, since one of the needed
413  // changesets is missing.
414 
415  // FIXME - the following tests are commented out because the backends
416  // don't currently tidy up old changesets correctly.
417  // TEST_EQUAL(replicate(master, replica, tempdir, 0, 1, true), 1);
418  // check_equal_dbs(masterpath, replicapath);
419  // TEST_EQUAL(replicate(master2, replica2, tempdir, 0, 1, true), 1);
420  // check_equal_dbs(masterpath, replica2path);
421 
422  // We need this inner scope to we close the replicas before we remove
423  // the temporary directory on Windows.
424  }
425 
426  rmtmpdir(tempdir);
427 #endif
428 }
429 
430 #ifdef XAPIAN_HAS_REMOTE_BACKEND
431 static void
433  Xapian::DatabaseReplica & replica,
434  const string & tempdir,
435  int expected_changesets,
436  int expected_fullcopies,
437  bool expected_changed)
438 {
439  string changesetpath = tempdir + "/changeset";
440  get_changeset(changesetpath, master, replica,
441  1, 0, 1);
442 
443  // Try applying truncated changesets of various different lengths.
444  string brokenchangesetpath = tempdir + "/changeset_broken";
445  auto filesize = file_size(changesetpath);
446  if (errno) {
447  FAIL_TEST("Can't stat '" << changesetpath << "'");
448  }
449  file_size_type len = 10;
450  file_size_type copylen;
451  while (len < filesize) {
452  copylen = truncated_copy(changesetpath, brokenchangesetpath, len);
453  TEST_EQUAL(copylen, len);
454  tout << "Trying replication with a changeset truncated to " << len <<
455  " bytes, from " << filesize << " bytes\n";
457  apply_changeset(brokenchangesetpath, replica,
458  expected_changesets, expected_fullcopies,
459  expected_changed));
460  if (len < 30 || len >= filesize - 10) {
461  // For lengths near the beginning and end, increment size by 1
462  ++len;
463  } else {
464  // Don't bother incrementing by small amounts in the middle of
465  // the changeset.
466  len += 1000;
467  if (len >= filesize - 10) {
468  len = filesize - 10;
469  }
470  }
471  }
472 }
473 #endif
474 
475 // Test changesets which are truncated (and therefore invalid).
476 DEFINE_TESTCASE(replicate3, replicas) {
477 #ifdef XAPIAN_HAS_REMOTE_BACKEND
479  string tempdir = ".replicatmp";
480  mktmpdir(tempdir);
481  string masterpath = get_named_writable_database_path("master");
482 
483  set_max_changesets(10);
484 
485  {
487  Xapian::DatabaseMaster master(masterpath);
488  string replicapath = tempdir + "/replica";
489  Xapian::DatabaseReplica replica(replicapath);
490 
491  // Add a document to the original database.
492  Xapian::Document doc1;
493  doc1.set_data(string("doc1"));
494  doc1.add_posting("doc", 1);
495  doc1.add_posting("one", 1);
496  orig.add_document(doc1);
497  orig.commit();
498 
499  TEST_EQUAL(replicate(master, replica, tempdir, 0, 1, true), 1);
500  check_equal_dbs(masterpath, replicapath);
501 
502  // Make a changeset.
503  orig.add_document(doc1);
504  orig.commit();
505 
506  replicate_with_brokenness(master, replica, tempdir, 1, 0, true);
507  // Although it throws an error, the final replication in
508  // replicate_with_brokenness() updates the database, since it's just
509  // the end-of-replication message which is missing its body.
510  check_equal_dbs(masterpath, replicapath);
511 
512  // Check that the earlier broken replications didn't cause any problems
513  // for the next replication.
514  orig.add_document(doc1);
515  orig.commit();
516  TEST_EQUAL(replicate(master, replica, tempdir, 1, 0, true), 2);
517 
518  // We need this inner scope to we close the replica before we remove
519  // the temporary directory on Windows.
520  }
521 
522  rmtmpdir(tempdir);
523 #endif
524 }
525 
526 // Tests for max_changesets
527 DEFINE_TESTCASE(replicate4, replicas) {
528 #ifdef XAPIAN_HAS_REMOTE_BACKEND
530  string tempdir = ".replicatmp";
531  mktmpdir(tempdir);
532  string masterpath = get_named_writable_database_path("master");
533 
535 
536  {
538  Xapian::DatabaseMaster master(masterpath);
539  string replicapath = tempdir + "/replica";
540  Xapian::DatabaseReplica replica(replicapath);
541 
542  // Add a document with no positions to the original database.
543  Xapian::Document doc1;
544  doc1.set_data(string("doc1"));
545  doc1.add_term("nopos");
546  orig.add_document(doc1);
547  orig.commit();
548 
549  // Apply the replication - we don't have changesets stored, so this
550  // should just do a database copy, and return a count of 1.
551  int count = replicate(master, replica, tempdir, 0, 1, true);
552  TEST_EQUAL(count, 1);
553  {
554  Xapian::Database dbcopy(replicapath);
555  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
556  }
557 
558  // Add a document with positional information to the original database.
559  doc1.add_posting("pos", 1);
560  orig.add_document(doc1);
561  orig.commit();
562 
563  // Replicate, and check that we have the positional information.
564  count = replicate(master, replica, tempdir, 1, 0, true);
565  TEST_EQUAL(count, 2);
566  {
567  Xapian::Database dbcopy(replicapath);
568  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
569  }
570  check_equal_dbs(masterpath, replicapath);
571 
572  // Add a document with no positions to the original database.
573  Xapian::Document doc2;
574  doc2.set_data(string("doc2"));
575  doc2.add_term("nopos");
576  orig.add_document(doc2);
577  if (get_dbtype() == "glass") {
578  // FIXME: Needs to be pre-commit for glass
580  }
581  orig.commit();
582 
583  // Replicate, and check that we have the positional information.
584  count = replicate(master, replica, tempdir, 1, 0, true);
585  TEST_EQUAL(count, 2);
586  {
587  Xapian::Database dbcopy(replicapath);
588  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
589  }
590  check_equal_dbs(masterpath, replicapath);
591  TEST(!file_exists(masterpath + "/changes1"));
592 
593  // Turn off replication, make sure we don't write anything.
594  if (get_dbtype() != "glass") {
596  }
597 
598  // Add a document with no positions to the original database.
599  Xapian::Document doc3;
600  doc3.set_data(string("doc3"));
601  doc3.add_term("nonopos");
602  orig.add_document(doc3);
603  orig.commit();
604 
605  // Replicate, and check that we have the positional information.
606  count = replicate(master, replica, tempdir, 0, 1, true);
607  TEST_EQUAL(count, 1);
608  {
609  Xapian::Database dbcopy(replicapath);
610  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
611  }
612  // Should have pulled a full copy
613  check_equal_dbs(masterpath, replicapath);
614  TEST(!file_exists(masterpath + "/changes3"));
615 
616  // We need this inner scope to we close the replica before we remove
617  // the temporary directory on Windows.
618  }
619 
620  rmtmpdir(tempdir);
621 #endif
622 }
623 
624 // Tests for max_changesets
625 DEFINE_TESTCASE(replicate5, replicas) {
626 #ifdef XAPIAN_HAS_REMOTE_BACKEND
628  string tempdir = ".replicatmp";
629  mktmpdir(tempdir);
630  string masterpath = get_named_writable_database_path("master");
631 
633 
634  {
636  Xapian::DatabaseMaster master(masterpath);
637  string replicapath = tempdir + "/replica";
638  Xapian::DatabaseReplica replica(replicapath);
639 
640  // Add a document with no positions to the original database.
641  Xapian::Document doc1;
642  doc1.set_data(string("doc1"));
643  doc1.add_term("nopos");
644  orig.add_document(doc1);
645  orig.commit();
646 
647  // Apply the replication - we don't have changesets stored, so this
648  // should just do a database copy, and return a count of 1.
649  int count = replicate(master, replica, tempdir, 0, 1, true);
650  TEST_EQUAL(count, 1);
651  {
652  Xapian::Database dbcopy(replicapath);
653  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
654  }
655 
656  // Add a document with positional information to the original database.
657  doc1.add_posting("pos", 1);
658  orig.add_document(doc1);
659  orig.commit();
660 
661  // Replicate, and check that we have the positional information.
662  count = replicate(master, replica, tempdir, 1, 0, true);
663  TEST_EQUAL(count, 2);
664  {
665  Xapian::Database dbcopy(replicapath);
666  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
667  }
668  check_equal_dbs(masterpath, replicapath);
669 
670  // Add a document with no positions to the original database.
671  Xapian::Document doc2;
672  doc2.set_data(string("doc2"));
673  doc2.add_term("nopos");
674  orig.add_document(doc2);
675  orig.commit();
676 
677  // Replicate, and check that we have the positional information.
678  count = replicate(master, replica, tempdir, 1, 0, true);
679  TEST_EQUAL(count, 2);
680  {
681  Xapian::Database dbcopy(replicapath);
682  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
683  }
684  check_equal_dbs(masterpath, replicapath);
685 
686  // Add a document with no positions to the original database.
687  Xapian::Document doc3;
688  doc3.set_data(string("doc3"));
689  doc3.add_term("nonopos");
690  orig.add_document(doc3);
691  orig.commit();
692 
693  // Replicate, and check that we have the positional information.
694  count = replicate(master, replica, tempdir, 1, 0, true);
695  TEST_EQUAL(count, 2);
696  {
697  Xapian::Database dbcopy(replicapath);
698  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
699  }
700  check_equal_dbs(masterpath, replicapath);
701 
702  // Ensure that only these changesets exists
703  TEST(!file_exists(masterpath + "/changes1"));
704  TEST(file_exists(masterpath + "/changes2"));
705  TEST(file_exists(masterpath + "/changes3"));
706 
708  masterpath = get_named_writable_database_path("master");
709 
710  // Add a document with no positions to the original database.
711  Xapian::Document doc4;
712  doc4.set_data(string("doc4"));
713  doc4.add_term("nononopos");
714  orig.add_document(doc4);
715  orig.commit();
716 
717  // Replicate, and check that we have the positional information.
718  count = replicate(master, replica, tempdir, 1, 0, true);
719  TEST_EQUAL(count, 2);
720  {
721  Xapian::Database dbcopy(replicapath);
722  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
723  }
724  check_equal_dbs(masterpath, replicapath);
725 
726  // Add a document with no positions to the original database.
727  Xapian::Document doc5;
728  doc5.set_data(string("doc5"));
729  doc5.add_term("nonononopos");
730  orig.add_document(doc5);
731  orig.commit();
732 
733  // Replicate, and check that we have the positional information.
734  count = replicate(master, replica, tempdir, 1, 0, true);
735  TEST_EQUAL(count, 2);
736  {
737  Xapian::Database dbcopy(replicapath);
738  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
739  }
740  check_equal_dbs(masterpath, replicapath);
741 
742  TEST(!file_exists(masterpath + "/changes2"));
743  TEST(file_exists(masterpath + "/changes3"));
744  TEST(file_exists(masterpath + "/changes4"));
745  TEST(file_exists(masterpath + "/changes5"));
746 
747  // We need this inner scope to we close the replica before we remove
748  // the temporary directory on Windows.
749  }
750 
751  rmtmpdir(tempdir);
752 #endif
753 }
754 
756 DEFINE_TESTCASE(replicate6, replicas) {
757 #ifdef XAPIAN_HAS_REMOTE_BACKEND
759  string tempdir = ".replicatmp";
760  mktmpdir(tempdir);
761  string masterpath = get_named_writable_database_path("master");
762 
763  set_max_changesets(10);
764 
765  {
767  Xapian::DatabaseMaster master(masterpath);
768  string replicapath = tempdir + "/replica";
769  Xapian::DatabaseReplica replica(replicapath);
770 
771  // Add a document to the original database.
772  Xapian::Document doc1;
773  doc1.set_data(string("doc1"));
774  doc1.add_posting("doc", 1);
775  doc1.add_posting("one", 1);
776  orig.add_document(doc1);
777  orig.commit();
778 
779  rm_rf(masterpath + "1");
780  cp_R(masterpath, masterpath + "1");
781 
782  orig.add_document(doc1);
783  orig.commit();
784 
785  // Apply the replication - we don't have changesets stored, so this
786  // should just do a database copy, and return a count of 1.
787  int count = replicate(master, replica, tempdir, 0, 1, true);
788  TEST_EQUAL(count, 1);
789  {
790  Xapian::Database dbcopy(replicapath);
791  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
792  }
793 
794  Xapian::DatabaseMaster master1(masterpath + "1");
795 
796  // Try to replicate an older version of the master.
797  count = replicate(master1, replica, tempdir, 0, 0, false);
798  TEST_EQUAL(count, 1);
799 
800  // Force a full copy.
801  count = replicate(master1, replica, tempdir, 0, 1, true, true);
802  TEST_EQUAL(count, 1);
803 
804  // Test we can still replicate.
805  orig.add_document(doc1);
806  orig.commit();
807 
808  count = replicate(master, replica, tempdir, 2, 0, true);
809  TEST_EQUAL(count, 3);
810 
811  check_equal_dbs(masterpath, replicapath);
812 
813  // We need this inner scope to we close the replica before we remove
814  // the temporary directory on Windows.
815  }
816 
817  rmtmpdir(tempdir);
818 #endif
819 }
820 
822 DEFINE_TESTCASE(replicate7, replicas) {
823 #ifdef XAPIAN_HAS_REMOTE_BACKEND
825  string tempdir = ".replicatmp";
826  mktmpdir(tempdir);
827  string masterpath = get_named_writable_database_path("master");
828 
829  set_max_changesets(10);
830 
832  Xapian::DatabaseMaster master(masterpath);
833  string replicapath = tempdir + "/replica";
834  {
835  Xapian::DatabaseReplica replica(replicapath);
836 
837  // Add a document to the original database.
838  Xapian::Document doc1;
839  doc1.set_data(string("doc1"));
840  doc1.add_posting("doc", 1);
841  doc1.add_posting("one", 1);
842  orig.add_document(doc1);
843  orig.commit();
844 
845  orig.add_document(doc1);
846  orig.commit();
847 
848  // Apply the replication - we don't have changesets stored, so this
849  // should just do a database copy, and return a count of 1.
850  int count = replicate(master, replica, tempdir, 0, 1, true);
851  TEST_EQUAL(count, 1);
852  {
853  Xapian::Database dbcopy(replicapath);
854  TEST_EQUAL(orig.get_uuid(), dbcopy.get_uuid());
855  }
856  }
857 
858  {
859  // Corrupt replica by truncating all the files to size 0.
860  string d = replicapath;
861  d += "/replica_1";
862  DIR * dir = opendir(d.c_str());
863  TEST(dir != NULL);
864  while (true) {
865  errno = 0;
866  struct dirent * entry = readdir(dir);
867  if (!entry) {
868  if (errno == 0)
869  break;
870  FAIL_TEST("readdir failed: " << errno_to_string(errno));
871  }
872 
873  // Skip '.' and '..'.
874  if (entry->d_name[0] == '.') continue;
875 
876  string file = d;
877  file += '/';
878  file += entry->d_name;
879  int fd = open(file.c_str(), O_WRONLY|O_TRUNC);
880  TEST(fd != -1);
881  TEST(close(fd) == 0);
882  }
883  closedir(dir);
884  }
885 
886  {
887  Xapian::DatabaseReplica replica(replicapath);
888 
889  // Replication should succeed and perform a full copy.
890  int count = replicate(master, replica, tempdir, 0, 1, true);
891  TEST_EQUAL(count, 1);
892 
893  check_equal_dbs(masterpath, replicapath);
894 
895  // We need this inner scope to we close the replica before we remove
896  // the temporary directory on Windows.
897  }
898 
899  rmtmpdir(tempdir);
900 #endif
901 }
static int apply_changeset(const string &changesetpath, Xapian::DatabaseReplica &replica, int expected_changesets, int expected_fullcopies, bool expected_changed)
#define UNSET_MAX_CHANGESETS_AFTERWARDS
static void mktmpdir(const string &path)
static void do_write(int fd, const char *p, size_t n)
static void get_changeset(const string &changesetpath, Xapian::DatabaseMaster &master, Xapian::DatabaseReplica &replica, int expected_changesets, int expected_fullcopies, bool expected_changed, bool full_copy=false)
static size_t do_read(int fd, char *p, size_t desired)
static file_size_type truncated_copy(const string &srcpath, const string &destpath, file_size_type tocopy)
#define set_max_changesets(N)
static int replicate(Xapian::DatabaseMaster &master, Xapian::DatabaseReplica &replica, const string &tempdir, int expected_changesets, int expected_fullcopies, bool expected_changed, bool full_copy=false)
static void replicate_with_brokenness(Xapian::DatabaseMaster &master, Xapian::DatabaseReplica &replica, const string &tempdir, int expected_changesets, int expected_fullcopies, bool expected_changed)
static void check_equal_dbs(const string &masterpath, const string &replicapath)
static void rmtmpdir(const string &path)
DEFINE_TESTCASE(replicate1, replicas)
std::string get_dbtype()
Definition: apitest.cc:41
std::string get_named_writable_database_path(const std::string &name)
Definition: apitest.cc:98
Xapian::WritableDatabase get_writable_database_again()
Definition: apitest.cc:132
Xapian::WritableDatabase get_named_writable_database(const std::string &name, const std::string &source)
Definition: apitest.cc:92
test functionality of the Xapian API
#define SKIP_TEST_FOR_BACKEND(B)
Definition: apitest.h:84
Definition: fd.h:30
Access to a master database for replication.
Definition: replication.h:56
void write_changesets_to_fd(int fd, const std::string &start_revision, ReplicationInfo *info) const
Write a set of changesets for upgrading the database to a file.
Definition: replication.cc:69
Access to a database replica, for applying replication to it.
Definition: replication.h:109
bool apply_next_changeset(ReplicationInfo *info, double reader_close_time)
Read and apply the next changeset.
Definition: replication.cc:263
void set_read_fd(int fd)
Set the file descriptor to read changesets from.
Definition: replication.cc:256
std::string get_revision_info() const
Get a string describing the current revision of the replica.
Definition: replication.cc:249
An indexed database of documents.
Definition: database.h:75
static size_t check(std::string_view path, int opts=0, std::ostream *out=NULL)
Check the integrity of a database or database table.
Definition: database.h:669
void close()
Close the database.
Definition: database.cc:99
TermIterator allterms_end(std::string_view={}) const noexcept
End iterator corresponding to allterms_begin(prefix).
Definition: database.h:307
Xapian::doccount get_doccount() const
Get the number of documents in the database.
Definition: database.cc:233
Xapian::docid get_lastdocid() const
Get the highest document id which has been used in the database.
Definition: database.cc:239
TermIterator allterms_begin(std::string_view prefix={}) const
Start iterating all terms in the database with a given prefix.
Definition: database.cc:209
std::string get_uuid() const
Get the UUID for the database.
Definition: database.cc:505
Class representing a document.
Definition: document.h:64
void set_data(std::string_view data)
Set the document data.
Definition: document.cc:81
void add_term(std::string_view term, Xapian::termcount wdf_inc=1)
Add a term to this document.
Definition: document.cc:87
void add_posting(std::string_view term, Xapian::termpos term_pos, Xapian::termcount wdf_inc=1)
Add a posting for a term.
Definition: document.cc:111
Indicates a problem communicating with a remote database.
Definition: error.h:791
Class for iterating over a list of terms.
Definition: termiterator.h:41
This class provides read/write access to a database.
Definition: database.h:964
void commit()
Commit pending modifications.
Definition: database.cc:543
Xapian::docid add_document(const Xapian::Document &doc)
Add a document to the database.
Definition: database.cc:561
PositionList * p
test database contents and consistency.
void errno_to_string(int e, string &s)
Convert errno value to std::string, thread-safe if possible.
Wrapper class around a file descriptor to avoid leaks.
int close(FD &fd)
Definition: fd.h:63
Utility functions for testing files.
std::make_unsigned_t< off_t > file_size_type
Unsigned return type of file_size() function.
Definition: filetests.h:57
bool file_exists(const char *path)
Test if a file exists.
Definition: filetests.h:40
file_size_type file_size(const char *path)
Returns the size of a file.
Definition: filetests.h:76
int closedir(DIR *)
struct DIR DIR
Definition: msvc_dirent.h:32
struct dirent * readdir(DIR *)
DIR * opendir(const char *)
Database open(std::string_view host, unsigned int port, unsigned timeout=10000, unsigned connect_timeout=10000)
Construct a Database object for read-only access to a remote database accessed via a TCP connection.
Replication support for Xapian databases.
include <dirent.h>, with alternative implementation for windows.
include <fcntl.h>, but working around broken platforms.
#define O_BINARY
Definition: safefcntl.h:80
include <sys/stat.h> with portability enhancements
<unistd.h>, but with compat.
Provide setenv() with compatibility versions.
Information about the steps involved in performing a replication.
Definition: replication.h:32
bool changed
True if and only if the replication corresponds to a change in the live version of the database.
Definition: replication.h:44
int fullcopy_count
Number of times a full database copy was performed.
Definition: replication.h:37
int changeset_count
Number of changesets applied.
Definition: replication.h:34
char * d_name
Definition: msvc_dirent.h:36
void dbcheck(const Xapian::Database &db, Xapian::doccount expected_doccount, Xapian::docid expected_lastdocid)
Check consistency of database and statistics.
Definition: dbcheck.cc:125
string postlist_to_string(const Xapian::Database &db, const string &tname)
Convert the list of postings in a postlist to a string.
Definition: dbcheck.cc:54
std::ostringstream tout
The debug printing stream.
Definition: testsuite.cc:104
a generic test suite engine
#define FAIL_TEST(MSG)
Fail the current testcase with message MSG.
Definition: testsuite.h:65
#define TEST_EQUAL(a, b)
Test for equality of two things.
Definition: testsuite.h:276
#define TEST(a)
Test a condition, without an additional explanation for failure.
Definition: testsuite.h:273
Xapian-specific test helper functions and macros.
#define TEST_EXCEPTION(TYPE, CODE)
Check that CODE throws exactly Xapian exception TYPE.
Definition: testutils.h:112
void cp_R(const std::string &src, const std::string &dest)
Recursively copy a directory.
Definition: unixcmds.cc:67
void rm_rf(const string &filename)
Remove a directory and contents, just like the Unix "rm -rf" command.
Definition: unixcmds.cc:111
C++ function versions of useful Unix commands.
Public interfaces for the Xapian library.