summaryrefslogtreecommitdiffstats
path: root/memfilepersistence/src/tests
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /memfilepersistence/src/tests
Publish
Diffstat (limited to 'memfilepersistence/src/tests')
-rw-r--r--memfilepersistence/src/tests/.gitignore8
-rw-r--r--memfilepersistence/src/tests/CMakeLists.txt14
-rw-r--r--memfilepersistence/src/tests/conformance/.gitignore2
-rw-r--r--memfilepersistence/src/tests/conformance/CMakeLists.txt6
-rw-r--r--memfilepersistence/src/tests/conformance/memfileconformancetest.cpp36
-rw-r--r--memfilepersistence/src/tests/device/.gitignore2
-rw-r--r--memfilepersistence/src/tests/device/CMakeLists.txt10
-rw-r--r--memfilepersistence/src/tests/device/devicemanagertest.cpp129
-rw-r--r--memfilepersistence/src/tests/device/devicemappertest.cpp51
-rw-r--r--memfilepersistence/src/tests/device/devicestest.cpp70
-rw-r--r--memfilepersistence/src/tests/device/mountpointlisttest.cpp255
-rw-r--r--memfilepersistence/src/tests/device/partitionmonitortest.cpp204
-rw-r--r--memfilepersistence/src/tests/init/.gitignore2
-rw-r--r--memfilepersistence/src/tests/init/CMakeLists.txt6
-rw-r--r--memfilepersistence/src/tests/init/filescannertest.cpp492
-rw-r--r--memfilepersistence/src/tests/mapper/.gitignore0
-rw-r--r--memfilepersistence/src/tests/spi/.gitignore2
-rw-r--r--memfilepersistence/src/tests/spi/CMakeLists.txt20
-rw-r--r--memfilepersistence/src/tests/spi/basicoperationhandlertest.cpp735
-rw-r--r--memfilepersistence/src/tests/spi/buffer_test.cpp75
-rw-r--r--memfilepersistence/src/tests/spi/buffered_file_writer_test.cpp78
-rw-r--r--memfilepersistence/src/tests/spi/iteratorhandlertest.cpp940
-rw-r--r--memfilepersistence/src/tests/spi/joinoperationhandlertest.cpp504
-rw-r--r--memfilepersistence/src/tests/spi/logginglazyfile.h88
-rw-r--r--memfilepersistence/src/tests/spi/memcachetest.cpp412
-rw-r--r--memfilepersistence/src/tests/spi/memfileautorepairtest.cpp411
-rw-r--r--memfilepersistence/src/tests/spi/memfiletest.cpp987
-rw-r--r--memfilepersistence/src/tests/spi/memfiletestutils.cpp455
-rw-r--r--memfilepersistence/src/tests/spi/memfiletestutils.h294
-rw-r--r--memfilepersistence/src/tests/spi/memfilev1serializertest.cpp1110
-rw-r--r--memfilepersistence/src/tests/spi/memfilev1verifiertest.cpp501
-rw-r--r--memfilepersistence/src/tests/spi/options_builder.h52
-rw-r--r--memfilepersistence/src/tests/spi/providerconformancetest.cpp74
-rw-r--r--memfilepersistence/src/tests/spi/shared_data_location_tracker_test.cpp111
-rw-r--r--memfilepersistence/src/tests/spi/simplememfileiobuffertest.cpp663
-rw-r--r--memfilepersistence/src/tests/spi/simulatedfailurefile.h78
-rw-r--r--memfilepersistence/src/tests/spi/splitoperationhandlertest.cpp213
-rw-r--r--memfilepersistence/src/tests/testhelper.cpp124
-rw-r--r--memfilepersistence/src/tests/testhelper.h54
-rw-r--r--memfilepersistence/src/tests/testrunner.cpp15
-rw-r--r--memfilepersistence/src/tests/tools/.gitignore2
-rw-r--r--memfilepersistence/src/tests/tools/CMakeLists.txt7
-rw-r--r--memfilepersistence/src/tests/tools/dumpslotfiletest.cpp138
-rw-r--r--memfilepersistence/src/tests/tools/vdsdisktooltest.cpp108
44 files changed, 9538 insertions, 0 deletions
diff --git a/memfilepersistence/src/tests/.gitignore b/memfilepersistence/src/tests/.gitignore
new file mode 100644
index 00000000000..b8a959a31c5
--- /dev/null
+++ b/memfilepersistence/src/tests/.gitignore
@@ -0,0 +1,8 @@
+/.depend
+/Makefile
+/dirconfig.tmp
+/test.vlog
+/testfile.0
+/testrunner
+/vdsroot
+memfilepersistence_testrunner_app
diff --git a/memfilepersistence/src/tests/CMakeLists.txt b/memfilepersistence/src/tests/CMakeLists.txt
new file mode 100644
index 00000000000..ee0cea9e1a5
--- /dev/null
+++ b/memfilepersistence/src/tests/CMakeLists.txt
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_executable(memfilepersistence_testrunner_app
+ SOURCES
+ testhelper.cpp
+ testrunner.cpp
+ DEPENDS
+ memfilepersistence_testconformance
+ memfilepersistence_testdevices
+ memfilepersistence_testinit
+ memfilepersistence_testspi
+ memfilepersistence_testtools
+ memfilepersistence
+)
+vespa_add_test(NAME memfilepersistence_testrunner_app COMMAND memfilepersistence_testrunner_app)
diff --git a/memfilepersistence/src/tests/conformance/.gitignore b/memfilepersistence/src/tests/conformance/.gitignore
new file mode 100644
index 00000000000..7e7c0fe7fae
--- /dev/null
+++ b/memfilepersistence/src/tests/conformance/.gitignore
@@ -0,0 +1,2 @@
+/.depend
+/Makefile
diff --git a/memfilepersistence/src/tests/conformance/CMakeLists.txt b/memfilepersistence/src/tests/conformance/CMakeLists.txt
new file mode 100644
index 00000000000..378f5751931
--- /dev/null
+++ b/memfilepersistence/src/tests/conformance/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_library(memfilepersistence_testconformance
+ SOURCES
+ memfileconformancetest.cpp
+ DEPENDS
+)
diff --git a/memfilepersistence/src/tests/conformance/memfileconformancetest.cpp b/memfilepersistence/src/tests/conformance/memfileconformancetest.cpp
new file mode 100644
index 00000000000..18a12788945
--- /dev/null
+++ b/memfilepersistence/src/tests/conformance/memfileconformancetest.cpp
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/log/log.h>
+#include <vespa/memfilepersistence/spi/memfilepersistence.h>
+#include <vespa/persistence/conformancetest/conformancetest.h>
+
+LOG_SETUP(".test.conformance");
+
+using namespace storage::spi;
+
+namespace storage {
+namespace memfile {
+
+ /*
+struct MemFileConformanceTest : public ConformanceTest {
+ struct Factory : public PersistenceFactory {
+
+ PersistenceSPI::UP getPersistenceImplementation() {
+ return PersistenceSPI::UP(new MemFilePersistence);
+ }
+ };
+
+ MemFileConformanceTest()
+ : ConformanceTest(PersistenceFactory::UP(new Factory)) {}
+
+ CPPUNIT_TEST_SUITE(MemFileConformanceTest);
+ DEFINE_CONFORMANCE_TESTS();
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(MemFileConformanceTest);
+*/
+
+} // memfile
+} // storage
diff --git a/memfilepersistence/src/tests/device/.gitignore b/memfilepersistence/src/tests/device/.gitignore
new file mode 100644
index 00000000000..7e7c0fe7fae
--- /dev/null
+++ b/memfilepersistence/src/tests/device/.gitignore
@@ -0,0 +1,2 @@
+/.depend
+/Makefile
diff --git a/memfilepersistence/src/tests/device/CMakeLists.txt b/memfilepersistence/src/tests/device/CMakeLists.txt
new file mode 100644
index 00000000000..845c70ae8e3
--- /dev/null
+++ b/memfilepersistence/src/tests/device/CMakeLists.txt
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_library(memfilepersistence_testdevices
+ SOURCES
+ mountpointlisttest.cpp
+ devicemanagertest.cpp
+ devicestest.cpp
+ devicemappertest.cpp
+ partitionmonitortest.cpp
+ DEPENDS
+)
diff --git a/memfilepersistence/src/tests/device/devicemanagertest.cpp b/memfilepersistence/src/tests/device/devicemanagertest.cpp
new file mode 100644
index 00000000000..eeb5007f452
--- /dev/null
+++ b/memfilepersistence/src/tests/device/devicemanagertest.cpp
@@ -0,0 +1,129 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/device/devicemanager.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/vespalib/util/exception.h>
+#include <sys/errno.h>
+#include <vespa/storageframework/defaultimplementation/clock/fakeclock.h>
+
+namespace storage {
+
+namespace memfile {
+
+class DeviceManagerTest : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(DeviceManagerTest);
+ CPPUNIT_TEST(testEventClass);
+ CPPUNIT_TEST(testEventSending);
+ CPPUNIT_TEST(testXml);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void testEventClass();
+ void testEventSending();
+ void testXml();
+
+ framework::defaultimplementation::FakeClock _clock;
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(DeviceManagerTest);
+
+void DeviceManagerTest::testEventClass()
+{
+ // Test that creation various IO events through common errno errors
+ // generates understandable errors.
+ {
+ IOEvent e(IOEvent::createEventFromErrno(1, ENOTDIR, "/mypath"));
+ CPPUNIT_ASSERT_EQUAL(
+ std::string("IOEvent(PATH_FAILURE, Not a directory: /mypath, time 1)"),
+ e.toString(true));
+ CPPUNIT_ASSERT_EQUAL(Device::PATH_FAILURE, e.getState());
+ }
+ {
+ IOEvent e(IOEvent::createEventFromErrno(2, EACCES, "/mypath"));
+ CPPUNIT_ASSERT_EQUAL(
+ std::string("IOEvent(NO_PERMISSION, Permission denied: /mypath, time 2)"),
+ e.toString(true));
+ CPPUNIT_ASSERT_EQUAL(Device::NO_PERMISSION, e.getState());
+ }
+ {
+ IOEvent e(IOEvent::createEventFromErrno(3, EIO, "/mypath"));
+ CPPUNIT_ASSERT_EQUAL(
+ std::string("IOEvent(IO_FAILURE, Input/output error: /mypath, time 3)"),
+ e.toString(true));
+ CPPUNIT_ASSERT_EQUAL(Device::IO_FAILURE, e.getState());
+ }
+ {
+ IOEvent e(
+ IOEvent::createEventFromErrno(4, EBADF, "/mypath", VESPA_STRLOC));
+ CPPUNIT_ASSERT_PREFIX(
+ std::string("IOEvent(INTERNAL_FAILURE, Bad file descriptor: /mypath"
+ ", testEventClass in"),
+ e.toString(true));
+ CPPUNIT_ASSERT_EQUAL(Device::INTERNAL_FAILURE, e.getState());
+ }
+}
+
+namespace {
+
+ struct Listener : public IOEventListener {
+ std::ostringstream ost;
+
+ Listener() : ost() { ost << "\n"; }
+ virtual ~Listener() {}
+
+ virtual void handleDirectoryEvent(Directory& dir, const IOEvent& e) {
+ ost << "Dir " << dir.getPath() << ": " << e.toString(true) << "\n";
+ }
+ virtual void handlePartitionEvent(Partition& part, const IOEvent& e) {
+ ost << "Partition " << part.getMountPoint() << ": "
+ << e.toString(true) << "\n";
+ }
+ virtual void handleDiskEvent(Disk& disk, const IOEvent& e) {
+ ost << "Disk " << disk.getId() << ": " << e.toString(true) << "\n";
+ }
+
+ };
+
+}
+
+void DeviceManagerTest::testEventSending()
+{
+ // Test that adding events to directories in the manager actually sends
+ // these events on to listeners.
+ DeviceManager manager(DeviceMapper::UP(new SimpleDeviceMapper), _clock);
+ Listener l;
+ manager.addIOEventListener(l);
+ Directory::LP dir(manager.getDirectory("/home/foo/var", 0));
+ // IO failures are disk events. Will mark all partitions and
+ // directories on that disk bad
+ dir->addEvent(IOEvent::createEventFromErrno(1, EIO, "/home/foo/var/foo"));
+ dir->addEvent(IOEvent::createEventFromErrno(2, EBADF, "/home/foo/var/bar"));
+ dir->addEvent(IOEvent::createEventFromErrno(3, EACCES, "/home/foo/var/car"));
+ dir->addEvent(IOEvent::createEventFromErrno(4, EISDIR, "/home/foo/var/var"));
+ std::string expected("\n"
+ "Disk 1: IOEvent(IO_FAILURE, Input/output error: "
+ "/home/foo/var/foo, time 1)\n"
+ "Dir /home/foo/var: IOEvent(INTERNAL_FAILURE, Bad file "
+ "descriptor: /home/foo/var/bar, time 2)\n"
+ "Dir /home/foo/var: IOEvent(NO_PERMISSION, Permission denied: "
+ "/home/foo/var/car, time 3)\n"
+ "Dir /home/foo/var: IOEvent(PATH_FAILURE, Is a directory: "
+ "/home/foo/var/var, time 4)\n"
+ );
+ CPPUNIT_ASSERT_EQUAL(expected, l.ost.str());
+}
+
+void DeviceManagerTest::testXml()
+{
+ DeviceManager manager(DeviceMapper::UP(new SimpleDeviceMapper), _clock);
+ Directory::LP dir(manager.getDirectory("/home/", 0));
+ dir->getPartition().initializeMonitor();
+ std::string xml = manager.toXml(" ");
+ CPPUNIT_ASSERT_MSG(xml,
+ xml.find("<partitionmonitor>") != std::string::npos);
+}
+
+}
+
+}
diff --git a/memfilepersistence/src/tests/device/devicemappertest.cpp b/memfilepersistence/src/tests/device/devicemappertest.cpp
new file mode 100644
index 00000000000..a78554a6342
--- /dev/null
+++ b/memfilepersistence/src/tests/device/devicemappertest.cpp
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/device/devicemapper.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/vespalib/util/exceptions.h>
+#include <sys/errno.h>
+
+namespace storage {
+
+namespace memfile {
+
+class DeviceMapperTest : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(DeviceMapperTest);
+ CPPUNIT_TEST(testSimpleDeviceMapper);
+ CPPUNIT_TEST(testAdvancedDeviceMapper);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void testSimpleDeviceMapper();
+ void testAdvancedDeviceMapper();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(DeviceMapperTest);
+
+void DeviceMapperTest::testSimpleDeviceMapper()
+{
+ SimpleDeviceMapper mapper;
+ CPPUNIT_ASSERT_EQUAL(uint64_t(1), mapper.getDeviceId("whatever&�"));
+ CPPUNIT_ASSERT_EQUAL(uint64_t(1), mapper.getDeviceId("whatever&�"));
+ CPPUNIT_ASSERT_EQUAL(uint64_t(2), mapper.getDeviceId("whatnot"));
+ std::string expected("Whatever& �=)/%#)=");
+ CPPUNIT_ASSERT_EQUAL(expected, mapper.getMountPoint(expected));
+}
+
+void DeviceMapperTest::testAdvancedDeviceMapper()
+{
+ AdvancedDeviceMapper mapper;
+ try{
+ mapper.getDeviceId("/doesnotexist");
+ CPPUNIT_FAIL("Expected exception");
+ } catch (vespalib::Exception& e) {
+ std::string what(e.what());
+ CPPUNIT_ASSERT_CONTAIN(
+ "Failed to run stat to find data on file /doesnotexist", what);
+ }
+}
+
+}
+
+} // storage
diff --git a/memfilepersistence/src/tests/device/devicestest.cpp b/memfilepersistence/src/tests/device/devicestest.cpp
new file mode 100644
index 00000000000..bd6898cb7ac
--- /dev/null
+++ b/memfilepersistence/src/tests/device/devicestest.cpp
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/device/devicemanager.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/vespalib/util/exceptions.h>
+#include <sys/errno.h>
+#include <vespa/storageframework/defaultimplementation/clock/fakeclock.h>
+
+namespace storage {
+
+namespace memfile {
+
+class DevicesTest : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(DevicesTest);
+ CPPUNIT_TEST(testDisk);
+ CPPUNIT_TEST(testPartition);
+ CPPUNIT_TEST(testDirectory);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void testDisk();
+ void testPartition();
+ void testDirectory();
+
+ framework::defaultimplementation::FakeClock _clock;
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(DevicesTest);
+
+void DevicesTest::testDisk()
+{
+ DeviceManager manager(DeviceMapper::UP(new SimpleDeviceMapper), _clock);
+ Disk::LP disk1(manager.getDisk("/something/on/disk"));
+ Disk::LP disk2(manager.getDisk("/something/on/disk"));
+ CPPUNIT_ASSERT_EQUAL(disk1->getId(), disk2->getId());
+ CPPUNIT_ASSERT_EQUAL(disk1.get(), disk2.get());
+ Disk::LP disk3(manager.getDisk("/something/on/disk2"));
+ CPPUNIT_ASSERT(disk2->getId() != disk3->getId());
+ disk3->toString(); // Add code coverage
+}
+
+void DevicesTest::testPartition()
+{
+ DeviceManager manager(DeviceMapper::UP(new SimpleDeviceMapper), _clock);
+ Partition::LP part(manager.getPartition("/etc"));
+ CPPUNIT_ASSERT_EQUAL(std::string("/etc"), part->getMountPoint());
+ part->toString(); // Add code coverage
+}
+
+void DevicesTest::testDirectory()
+{
+ DeviceManager manager(DeviceMapper::UP(new SimpleDeviceMapper), _clock);
+ Directory::LP dir1(manager.getDirectory("/on/disk", 0));
+ CPPUNIT_ASSERT_EQUAL(std::string("/on/disk"), dir1->getPath());
+ CPPUNIT_ASSERT(dir1->getLastEvent() == 0);
+ CPPUNIT_ASSERT_EQUAL(Device::OK, dir1->getState());
+ CPPUNIT_ASSERT(dir1->isOk());
+ CPPUNIT_ASSERT_EQUAL(std::string("/on/disk 0"), dir1->toString());
+
+ dir1->addEvent(Device::IO_FAILURE, "Ouch", "");
+ CPPUNIT_ASSERT(!dir1->isOk());
+ CPPUNIT_ASSERT(dir1->getLastEvent() != 0);
+ CPPUNIT_ASSERT_EQUAL(std::string("/on/disk 5 0 Ouch"), dir1->toString());
+ dir1->toString(); // Add code coverage
+}
+
+}
+
+} // storage
diff --git a/memfilepersistence/src/tests/device/mountpointlisttest.cpp b/memfilepersistence/src/tests/device/mountpointlisttest.cpp
new file mode 100644
index 00000000000..4cb5822ceb7
--- /dev/null
+++ b/memfilepersistence/src/tests/device/mountpointlisttest.cpp
@@ -0,0 +1,255 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <fstream>
+#include <vespa/memfilepersistence/device/mountpointlist.h>
+#include <vespa/vespalib/io/fileutil.h>
+#include <vespa/storageframework/defaultimplementation/clock/fakeclock.h>
+
+using vespalib::LinkedPtr;
+using vespalib::fileExists;
+using vespalib::isDirectory;
+using vespalib::isSymLink;
+using vespalib::readLink;
+
+namespace storage {
+
+namespace memfile {
+
+class MountPointList_Test : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(MountPointList_Test);
+ CPPUNIT_TEST(testScanning);
+ CPPUNIT_TEST(testStatusFile);
+ CPPUNIT_TEST(testInitDisks);
+ CPPUNIT_TEST_SUITE_END();
+
+ static const std::string _prefix;
+
+public:
+ void testScanning();
+ void testStatusFile();
+ void testInitDisks();
+
+ void init();
+ void tearDown();
+
+ framework::defaultimplementation::FakeClock _clock;
+
+private:
+ LinkedPtr<DeviceManager> newDeviceManager() {
+ return LinkedPtr<DeviceManager>(
+ new DeviceManager(
+ DeviceMapper::UP(new SimpleDeviceMapper),
+ _clock));
+ }
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(MountPointList_Test);
+
+const std::string MountPointList_Test::_prefix("./vdsroot");
+
+namespace {
+ void run(const std::string& cmd) {
+ CPPUNIT_ASSERT_MESSAGE(cmd, system(cmd.c_str()) == 0);
+ }
+}
+
+void MountPointList_Test::init()
+{
+ tearDown();
+ run("rm -rf "+_prefix);
+ run("mkdir -p "+_prefix+"/disks");
+
+ run("mkdir "+_prefix+"/disks/d0"); // Regular dir
+ run("mkdir "+_prefix+"/disks/d1"); // Inaccessible dir
+ run("chmod 000 "+_prefix+"/disks/d1");
+ run("mkdir "+_prefix+"/disks/D2"); // Wrongly named dir
+ run("mkdir "+_prefix+"/disks/d3"); // Regular non-empty dir
+ run("touch "+_prefix+"/disks/d3/foo");
+ run("touch "+_prefix+"/disks/d4"); // Not a dir
+ run("ln -s D2 "+_prefix+"/disks/d5"); // Symlink to dir
+ run("ln -s d4 "+_prefix+"/disks/d6"); // Symlink to file
+}
+
+void MountPointList_Test::tearDown()
+{
+ try{
+ if (fileExists(_prefix+"/disks/d1")) {
+ run("chmod 755 "+_prefix+"/disks/d1");
+ }
+ } catch (std::exception& e) {
+ std::cerr << "Failed to clean up: " << e.what() << "\n";
+ }
+}
+
+void MountPointList_Test::testScanning()
+{
+ init();
+ MountPointList list(_prefix,
+ std::vector<vespalib::string>(),
+ vespalib::LinkedPtr<DeviceManager>(
+ new DeviceManager(
+ DeviceMapper::UP(new SimpleDeviceMapper),
+ _clock)));
+ list.scanForDisks();
+
+ // Check that we got the expected entries.
+ CPPUNIT_ASSERT_EQUAL(7u, list.getSize());
+
+ for (uint32_t i=0; i<7u; ++i) {
+ std::ostringstream ost;
+ ost << _prefix << "/disks/d" << i;
+ CPPUNIT_ASSERT_EQUAL(ost.str(), list[i].getPath());
+ }
+
+ // Note.. scanForDisks() should not in any circumstances access the
+ // disks. Thus it should not know that d1 is inaccessible, or that d6
+ // is actually a symlink to a file
+ CPPUNIT_ASSERT_EQUAL(Device::OK, list[0].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::OK, list[1].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::NOT_FOUND, list[2].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::OK, list[3].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::PATH_FAILURE, list[4].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::OK, list[5].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::OK, list[6].getState());
+
+ list.verifyHealthyDisks(-1);
+ CPPUNIT_ASSERT_EQUAL(Device::OK, list[0].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::NO_PERMISSION, list[1].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::NOT_FOUND, list[2].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::INTERNAL_FAILURE, list[3].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::PATH_FAILURE, list[4].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::OK, list[5].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::PATH_FAILURE, list[6].getState());
+}
+
+void MountPointList_Test::testStatusFile()
+{
+ init();
+ std::string statusFileName(_prefix + "/disks.status");
+
+ // Try reading non-existing file, and writing a file
+ {
+ MountPointList list(_prefix,
+ std::vector<vespalib::string>(),
+ vespalib::LinkedPtr<DeviceManager>(
+ new DeviceManager(
+ DeviceMapper::UP(new SimpleDeviceMapper),
+ _clock)));
+
+ _clock.setAbsoluteTimeInSeconds(5678);
+ list.scanForDisks();
+
+ // File does not currently exist, that should be ok though.
+ list.readFromFile();
+ list.verifyHealthyDisks(-1);
+ CPPUNIT_ASSERT_EQUAL(7u, list.getSize());
+ list[5].addEvent(IOEvent(1234, Device::IO_FAILURE, "Argh", "Hmm"));
+ CPPUNIT_ASSERT_EQUAL(Device::IO_FAILURE, list[5].getState());
+
+ // Write to file.
+ list.writeToFile();
+ }
+
+ // Check contents of file.
+ {
+ std::ifstream in(statusFileName.c_str());
+ std::string line;
+ CPPUNIT_ASSERT(std::getline(in, line));
+
+ CPPUNIT_ASSERT_PREFIX(
+ std::string(_prefix + "/disks/d1 3 5678 IoException: NO PERMISSION: "
+ "open(./vdsroot/disks/d1/chunkinfo, 0x1): Failed, "
+ "errno(13): Permission denied"),
+ line);
+ CPPUNIT_ASSERT(std::getline(in, line));
+ CPPUNIT_ASSERT_PREFIX(
+ std::string(_prefix +"/disks/d2 1 5678 Disk not found during scanning of "
+ "disks directory"),
+ line);
+ CPPUNIT_ASSERT(std::getline(in, line));
+ CPPUNIT_ASSERT_PREFIX(
+ std::string(_prefix + "/disks/d3 4 5678 Foreign data in mountpoint. New "
+ "mountpoints added should be empty."),
+ line);
+ CPPUNIT_ASSERT(std::getline(in, line));
+ CPPUNIT_ASSERT_PREFIX(
+ std::string(_prefix + "/disks/d4 2 5678 File d4 in disks directory is not "
+ "a directory."),
+ line);
+ CPPUNIT_ASSERT(std::getline(in, line));
+ CPPUNIT_ASSERT_PREFIX(std::string(_prefix + "/disks/d5 5 1234 Argh"),
+ line);
+ CPPUNIT_ASSERT(std::getline(in, line));
+ CPPUNIT_ASSERT_PREFIX(
+ std::string(_prefix + "/disks/d6 2 5678 The path exist, but is not a "
+ "directory."),
+ line);
+ CPPUNIT_ASSERT(std::getline(in, line));
+ CPPUNIT_ASSERT_EQUAL(std::string("EOF"), line);
+ }
+
+ // Starting over to get new device instances.
+ // Scan disk, read file, and check that erronious disks are not used.
+ {
+ MountPointList list(_prefix,
+ std::vector<vespalib::string>(),
+ vespalib::LinkedPtr<DeviceManager>(
+ new DeviceManager(
+ DeviceMapper::UP(new SimpleDeviceMapper),
+ _clock)));
+ list.scanForDisks();
+ list.readFromFile();
+ // Check that we got the expected entries.
+ CPPUNIT_ASSERT_EQUAL(7u, list.getSize());
+
+ // Note.. scanForDisks() should not under any circumstance access the
+ // disks. Thus it should not know that d1 is inaccessible.
+ CPPUNIT_ASSERT_EQUAL(Device::OK, list[0].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::NO_PERMISSION, list[1].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::NOT_FOUND, list[2].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::INTERNAL_FAILURE, list[3].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::PATH_FAILURE, list[4].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::IO_FAILURE, list[5].getState());
+ CPPUNIT_ASSERT_EQUAL(Device::PATH_FAILURE, list[6].getState());
+ }
+}
+
+void MountPointList_Test::testInitDisks()
+{
+ vespalib::string d3target = "d3target";
+ vespalib::string foodev = _prefix + "/foodev";
+ vespalib::string bardev = _prefix + "/bardev";
+
+ tearDown();
+ run("rm -rf " + _prefix);
+ run("mkdir -p " + _prefix + "/disks/d2");
+ run("ln -s " + d3target + " " + _prefix + "/disks/d3");
+
+ std::vector<vespalib::string> diskPaths {
+ // disks/d0 should become a regular directory
+ _prefix + "/disks/d0",
+ // disks/d1 should be a symlink to /foo
+ foodev,
+ // disks/d2 should already be a directory
+ "/ignored",
+ // disks/d3 should already be a symlink
+ "/ignored2"
+ };
+
+ MountPointList list(_prefix, diskPaths, newDeviceManager());
+ list.initDisks();
+
+ CPPUNIT_ASSERT(isDirectory(_prefix + "/disks"));
+ CPPUNIT_ASSERT(isDirectory(_prefix + "/disks/d0"));
+ CPPUNIT_ASSERT(isSymLink(_prefix + "/disks/d1"));
+ CPPUNIT_ASSERT_EQUAL(foodev, readLink(_prefix + "/disks/d1"));
+ CPPUNIT_ASSERT(isDirectory(_prefix + "/disks/d2"));
+ CPPUNIT_ASSERT(isSymLink(_prefix + "/disks/d3"));
+ CPPUNIT_ASSERT_EQUAL(d3target, readLink(_prefix + "/disks/d3"));
+}
+
+} // memfile
+
+} // storage
diff --git a/memfilepersistence/src/tests/device/partitionmonitortest.cpp b/memfilepersistence/src/tests/device/partitionmonitortest.cpp
new file mode 100644
index 00000000000..1a016edcc83
--- /dev/null
+++ b/memfilepersistence/src/tests/device/partitionmonitortest.cpp
@@ -0,0 +1,204 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/device/partitionmonitor.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+
+namespace storage {
+
+namespace memfile {
+
+struct PartitionMonitorTest : public CppUnit::TestFixture
+{
+ void testNormalUsage();
+ void testHighInodeFillrate();
+ void testAlwaysStatPolicy();
+ void testPeriodPolicy();
+ void testStatOncePolicy();
+ void testDynamicPolicy();
+ void testIsFull();
+
+ CPPUNIT_TEST_SUITE(PartitionMonitorTest);
+ CPPUNIT_TEST(testNormalUsage);
+ CPPUNIT_TEST(testHighInodeFillrate);
+ CPPUNIT_TEST(testAlwaysStatPolicy);
+ CPPUNIT_TEST(testPeriodPolicy);
+ CPPUNIT_TEST(testStatOncePolicy);
+ CPPUNIT_TEST(testDynamicPolicy);
+ CPPUNIT_TEST(testIsFull);
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(PartitionMonitorTest);
+
+struct FakeStatter : public PartitionMonitor::Statter {
+ struct statvfs _info;
+
+ FakeStatter() {
+ _info.f_bsize = 4096;
+ _info.f_frsize = 4096;
+ _info.f_blocks = 1000;
+ _info.f_bfree = 500;
+ _info.f_bavail = 400;
+ _info.f_files = 64;
+ _info.f_ffree = 32;
+ _info.f_favail = 30;
+ _info.f_fsid = 13;
+ _info.f_namemax = 256;
+ }
+ void removeData(uint32_t size) {
+ _info.f_bavail += (size / _info.f_bsize);
+ _info.f_bfree += (size / _info.f_bsize);
+ }
+ void addData(uint32_t size) {
+ _info.f_bavail -= (size / _info.f_bsize);
+ _info.f_bfree -= (size / _info.f_bsize);
+ }
+
+ virtual void statFileSystem(const std::string&, struct statvfs& info) {
+ info = _info;
+ }
+};
+
+void PartitionMonitorTest::testNormalUsage()
+{
+ PartitionMonitor monitor("testrunner.cpp");
+ FakeStatter* statter = new FakeStatter();
+ monitor.setStatter(std::unique_ptr<PartitionMonitor::Statter>(statter));
+ std::string expected(
+ "PartitionMonitor(testrunner.cpp, STAT_PERIOD(100), "
+ "2048000/3686400 used - 55.5556 % full)");
+ CPPUNIT_ASSERT_EQUAL(expected, monitor.toString(false));
+ expected =
+ "PartitionMonitor(testrunner.cpp) {\n"
+ " Fill rate: 55.5556 %\n"
+ " Inode fill rate: 51.6129 %\n"
+ " Detected block size: 4096\n"
+ " File system id: 13\n"
+ " Total size: 3686400 (3600 kB)\n"
+ " Used size: 2048000 (2000 kB)\n"
+ " Queries since last stat: 0\n"
+ " Monitor policy: STAT_PERIOD(100)\n"
+ " Root only ratio 0\n"
+ " Max fill rate 98 %\n"
+ "}";
+ CPPUNIT_ASSERT_EQUAL(expected, monitor.toString(true));
+ CPPUNIT_ASSERT(monitor.getFillRate() > 0.55);
+}
+
+void PartitionMonitorTest::testHighInodeFillrate()
+{
+ PartitionMonitor monitor("testrunner.cpp");
+ FakeStatter* statter = new FakeStatter();
+ statter->_info.f_favail = 2;
+ monitor.setStatter(std::unique_ptr<PartitionMonitor::Statter>(statter));
+ std::string expected(
+ "PartitionMonitor(testrunner.cpp, STAT_PERIOD(100), "
+ "2048000/3686400 used - 94.1176 % full (inodes))");
+ CPPUNIT_ASSERT_EQUAL(expected, monitor.toString(false));
+ expected =
+ "PartitionMonitor(testrunner.cpp) {\n"
+ " Fill rate: 55.5556 %\n"
+ " Inode fill rate: 94.1176 %\n"
+ " Detected block size: 4096\n"
+ " File system id: 13\n"
+ " Total size: 3686400 (3600 kB)\n"
+ " Used size: 2048000 (2000 kB)\n"
+ " Queries since last stat: 0\n"
+ " Monitor policy: STAT_PERIOD(100)\n"
+ " Root only ratio 0\n"
+ " Max fill rate 98 %\n"
+ "}";
+ CPPUNIT_ASSERT_EQUAL(expected, monitor.toString(true));
+ CPPUNIT_ASSERT(monitor.getFillRate() > 0.94);
+}
+
+void PartitionMonitorTest::testAlwaysStatPolicy()
+{
+ PartitionMonitor monitor("testrunner.cpp");
+ FakeStatter* statter = new FakeStatter();
+ monitor.setStatter(std::unique_ptr<PartitionMonitor::Statter>(statter));
+ monitor.setAlwaysStatPolicy();
+ for (uint32_t i=0; i<10; ++i) {
+ monitor.getFillRate();
+ CPPUNIT_ASSERT_EQUAL(0u, monitor._queriesSinceStat);
+ }
+}
+
+void PartitionMonitorTest::testPeriodPolicy()
+{
+ PartitionMonitor monitor("testrunner.cpp");
+ FakeStatter* statter = new FakeStatter();
+ monitor.setStatter(std::unique_ptr<PartitionMonitor::Statter>(statter));
+ monitor.setStatPeriodPolicy(4);
+ for (uint32_t i=1; i<16; ++i) {
+ monitor.getFillRate();
+ CPPUNIT_ASSERT_EQUAL(i % 4, monitor._queriesSinceStat);
+ }
+}
+
+void PartitionMonitorTest::testStatOncePolicy()
+{
+ PartitionMonitor monitor("testrunner.cpp");
+ FakeStatter* statter = new FakeStatter();
+ monitor.setStatter(std::unique_ptr<PartitionMonitor::Statter>(statter));
+ monitor.setStatOncePolicy();
+ for (uint32_t i=1; i<16; ++i) {
+ monitor.getFillRate();
+ CPPUNIT_ASSERT_EQUAL(i, monitor._queriesSinceStat);
+ }
+}
+
+void PartitionMonitorTest::testDynamicPolicy()
+{
+ PartitionMonitor monitor("testrunner.cpp");
+ FakeStatter* statter = new FakeStatter();
+ monitor.setStatter(std::unique_ptr<PartitionMonitor::Statter>(statter));
+ monitor.setStatDynamicPolicy(2);
+ // Add some data, such that we see that period goes down
+ CPPUNIT_ASSERT_EQUAL(uint64_t(3698), monitor.calcDynamicPeriod());
+ CPPUNIT_ASSERT_EQUAL(55, (int) (100 * monitor.getFillRate()));
+ monitor.addingData(256 * 1024);
+ CPPUNIT_ASSERT_EQUAL(uint64_t(2592), monitor.calcDynamicPeriod());
+ CPPUNIT_ASSERT_EQUAL(62, (int) (100 * monitor.getFillRate()));
+ monitor.addingData(512 * 1024);
+ CPPUNIT_ASSERT_EQUAL(uint64_t(968), monitor.calcDynamicPeriod());
+ CPPUNIT_ASSERT_EQUAL(76, (int) (100 * monitor.getFillRate()));
+ // Add such that we hint that we have more data than possible on disk
+ monitor.addingData(1024 * 1024);
+ // Let fake stat just have a bit more data than before
+ statter->addData(256 * 1024);
+ // With high fill rate, we should check stat each time
+ CPPUNIT_ASSERT_EQUAL(uint64_t(1), monitor.calcDynamicPeriod());
+ // As period is 1, we will now do a new stat, it should find we
+ // actually have less fill rate
+ CPPUNIT_ASSERT_EQUAL(62, (int) (100 * monitor.getFillRate()));
+}
+
+void PartitionMonitorTest::testIsFull()
+{
+ PartitionMonitor monitor("testrunner.cpp");
+ monitor.setMaxFillness(0.85);
+ FakeStatter* statter = new FakeStatter();
+ monitor.setStatOncePolicy();
+ monitor.setStatter(std::unique_ptr<PartitionMonitor::Statter>(statter));
+
+ CPPUNIT_ASSERT_EQUAL(55, (int) (100 * monitor.getFillRate()));
+ CPPUNIT_ASSERT(!monitor.isFull());
+ monitor.addingData(512 * 1024);
+ CPPUNIT_ASSERT_EQUAL(69, (int) (100 * monitor.getFillRate()));
+ CPPUNIT_ASSERT(!monitor.isFull());
+ monitor.addingData(600 * 1024);
+ CPPUNIT_ASSERT_EQUAL(86, (int) (100 * monitor.getFillRate()));
+ CPPUNIT_ASSERT(monitor.isFull());
+ monitor.removingData(32 * 1024);
+ CPPUNIT_ASSERT_EQUAL(85, (int) (100 * monitor.getFillRate()));
+ CPPUNIT_ASSERT(monitor.isFull());
+ monitor.removingData(32 * 1024);
+ CPPUNIT_ASSERT_EQUAL(84, (int) (100 * monitor.getFillRate()));
+ CPPUNIT_ASSERT(!monitor.isFull());
+}
+
+}
+
+} // storage
diff --git a/memfilepersistence/src/tests/init/.gitignore b/memfilepersistence/src/tests/init/.gitignore
new file mode 100644
index 00000000000..7e7c0fe7fae
--- /dev/null
+++ b/memfilepersistence/src/tests/init/.gitignore
@@ -0,0 +1,2 @@
+/.depend
+/Makefile
diff --git a/memfilepersistence/src/tests/init/CMakeLists.txt b/memfilepersistence/src/tests/init/CMakeLists.txt
new file mode 100644
index 00000000000..ebc4738a8c4
--- /dev/null
+++ b/memfilepersistence/src/tests/init/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_library(memfilepersistence_testinit
+ SOURCES
+ filescannertest.cpp
+ DEPENDS
+)
diff --git a/memfilepersistence/src/tests/init/filescannertest.cpp b/memfilepersistence/src/tests/init/filescannertest.cpp
new file mode 100644
index 00000000000..8b49a21dad0
--- /dev/null
+++ b/memfilepersistence/src/tests/init/filescannertest.cpp
@@ -0,0 +1,492 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/document/bucket/bucketid.h>
+#include <iomanip>
+#include <vespa/memfilepersistence/device/devicemanager.h>
+#include <vespa/memfilepersistence/init/filescanner.h>
+#include <vespa/memfilepersistence/mapper/bucketdirectorymapper.h>
+#include <vespa/storageframework/defaultimplementation/component/componentregisterimpl.h>
+#include <vespa/storageframework/defaultimplementation/clock/realclock.h>
+#include <vespa/vdslib/state/nodestate.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/vespalib/io/fileutil.h>
+#include <vespa/vespalib/util/exceptions.h>
+#include <vespa/vespalib/util/random.h>
+#include <sys/errno.h>
+
+namespace storage {
+namespace memfile {
+
+struct FileScannerTest : public CppUnit::TestFixture {
+ struct TestParameters {
+ uint32_t filesPerDisk;
+ uint32_t diskCount;
+ uint32_t bucketSplitBits;
+ uint32_t dirLevels;
+ uint32_t dirSpread;
+ uint32_t parts;
+ std::set<uint32_t> disksDown;
+ bool diskDownWithBrokenSymlink;
+ bool bucketWrongDir;
+ bool bucketMultipleDirs;
+ bool bucketMultipleDisks;
+ bool addTemporaryFiles;
+ bool addAlienFiles;
+ bool dirWithNoListPermission;
+ bool dirWithNoWritePermission;
+ bool dirWithNoExecutePermission;
+ bool fileWithNoReadPermission;
+ bool fileWithNoWritePermission;
+
+ TestParameters()
+ : filesPerDisk(10), diskCount(5), bucketSplitBits(20),
+ dirLevels(1), dirSpread(16), parts(1), disksDown(),
+ diskDownWithBrokenSymlink(false),
+ bucketWrongDir(false), bucketMultipleDirs(false),
+ bucketMultipleDisks(false),
+ addTemporaryFiles(false), addAlienFiles(false),
+ dirWithNoListPermission(false),
+ dirWithNoWritePermission(false),
+ dirWithNoExecutePermission(false),
+ fileWithNoReadPermission(false),
+ fileWithNoWritePermission(false) {}
+ void addAllComplexities() {
+ disksDown.insert(0);
+ disksDown.insert(2);
+ disksDown.insert(4);
+ bucketWrongDir = true;
+ bucketMultipleDirs = true;
+ bucketMultipleDisks = true;
+ parts = 7;
+ addTemporaryFiles = true;
+ addAlienFiles = true;
+ dirWithNoWritePermission = true;
+ fileWithNoWritePermission = true;
+ fileWithNoReadPermission = true;
+ }
+ };
+
+ void testNormalUsage() {
+ TestParameters params;
+ runTest(params);
+ }
+ void testMultipleParts() {
+ TestParameters params;
+ params.parts = 3;
+ runTest(params);
+ }
+ void testBucketInWrongDirectory() {
+ TestParameters params;
+ params.bucketWrongDir = true;
+ runTest(params);
+ }
+ void testBucketInMultipleDirectories() {
+ TestParameters params;
+ params.bucketMultipleDirs = true;
+ runTest(params);
+ }
+ void testZeroDirLevel() {
+ TestParameters params;
+ params.dirLevels = 0;
+ runTest(params);
+ }
+ void testSeveralDirLevels() {
+ TestParameters params;
+ params.dirLevels = 3;
+ runTest(params);
+ }
+ void testNonStandardDirSpread() {
+ TestParameters params;
+ params.dirSpread = 63;
+ runTest(params);
+ }
+ void testDiskDown() {
+ TestParameters params;
+ params.disksDown.insert(1);
+ runTest(params);
+ }
+ void testDiskDownBrokenSymlink() {
+ TestParameters params;
+ params.disksDown.insert(1);
+ params.disksDown.insert(3);
+ params.diskDownWithBrokenSymlink = true;
+ runTest(params);
+ }
+ void testRemoveTemporaryFile() {
+ TestParameters params;
+ params.addTemporaryFiles = true;
+ runTest(params);
+ }
+ void testAlienFile() {
+ TestParameters params;
+ params.addAlienFiles = true;
+ runTest(params);
+ }
+ void testUnlistableDirectory() {
+ TestParameters params;
+ params.dirWithNoListPermission = true;
+ runTest(params);
+ }
+ void testDirWithNoWritePermission() {
+ TestParameters params;
+ params.dirWithNoWritePermission = true;
+ runTest(params);
+ }
+ void testDirWithNoExecutePermission() {
+ TestParameters params;
+ params.dirWithNoWritePermission = true;
+ runTest(params);
+ }
+ void testFileWithNoReadPermission() {
+ TestParameters params;
+ params.bucketWrongDir = true;
+ params.fileWithNoReadPermission = true;
+ runTest(params);
+ }
+ void testFileWithNoWritePermission() {
+ TestParameters params;
+ params.bucketWrongDir = true;
+ params.fileWithNoWritePermission = true;
+ runTest(params);
+ }
+ void testAllFailuresCombined() {
+ TestParameters params;
+ params.addAllComplexities();
+ runTest(params);
+ }
+
+ CPPUNIT_TEST_SUITE(FileScannerTest);
+ CPPUNIT_TEST(testNormalUsage);
+ CPPUNIT_TEST(testMultipleParts);
+ CPPUNIT_TEST(testBucketInWrongDirectory);
+ CPPUNIT_TEST(testBucketInMultipleDirectories);
+ CPPUNIT_TEST(testZeroDirLevel);
+ CPPUNIT_TEST(testSeveralDirLevels);
+ CPPUNIT_TEST(testNonStandardDirSpread);
+ CPPUNIT_TEST(testDiskDown);
+ CPPUNIT_TEST(testDiskDownBrokenSymlink);
+ CPPUNIT_TEST(testRemoveTemporaryFile);
+ CPPUNIT_TEST(testAlienFile);
+ CPPUNIT_TEST(testUnlistableDirectory);
+ CPPUNIT_TEST(testDirWithNoWritePermission);
+ CPPUNIT_TEST(testDirWithNoExecutePermission);
+ CPPUNIT_TEST(testFileWithNoReadPermission);
+ CPPUNIT_TEST(testFileWithNoWritePermission);
+ CPPUNIT_TEST(testAllFailuresCombined);
+ CPPUNIT_TEST_SUITE_END();
+
+ // Actual implementation of the tests.
+
+ /** Run a console command and fail test if it fails. */
+ void run(std::string cmd);
+
+ /** Struct containing metadata for a single bucket. */
+ struct BucketData {
+ document::BucketId bucket;
+ uint32_t disk;
+ std::vector<uint32_t> directory;
+ bool shouldExist; // Set to false for buckets that won't exist due to
+ // some failure.
+
+ BucketData() : shouldExist(true) {}
+
+ bool sameDir(BucketData& other) const {
+ return (disk == other.disk && directory == other.directory);
+ }
+ };
+
+ /**
+ * Create an overview of the buckets we're gonna use in the test.
+ * (Without any failures introduced)
+ */
+ std::vector<BucketData> createBuckets(const TestParameters& params);
+
+ /**
+ * Create the data in the bucket map and introduce the failures specified
+ * in the test. Mark buckets in bucket list that won't exist due to the
+ * failures so we know how to verify result of test.
+ */
+ void createData(const TestParameters&, std::vector<BucketData>& buckets,
+ std::vector<std::string>& tempFiles,
+ std::vector<std::string>& alienFiles);
+
+ /**
+ * Run a test with a given set of parameters, calling createData to set up
+ * the data, and then using a file scanner to actually list the files.
+ */
+ void runTest(const TestParameters&);
+
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(FileScannerTest);
+
+void
+FileScannerTest::run(std::string cmd)
+{
+ int result = system(cmd.c_str());
+ if (result != 0) {
+ CPPUNIT_FAIL("Failed to run command '" + cmd + "'.");
+ }
+}
+
+std::vector<FileScannerTest::BucketData>
+FileScannerTest::createBuckets(const TestParameters& params)
+{
+ std::vector<BucketData> buckets;
+ BucketDirectoryMapper dirMapper(params.dirLevels, params.dirSpread);
+ for (uint32_t i=0; i<params.diskCount; ++i) {
+ if (params.disksDown.find(i) != params.disksDown.end()) {
+ continue;
+ }
+ for (uint32_t j=0; j<params.filesPerDisk; ++j) {
+ BucketData data;
+ data.bucket = document::BucketId(params.bucketSplitBits,
+ params.filesPerDisk * i + j);
+ data.disk = i;
+ data.directory = dirMapper.getPath(data.bucket);
+ buckets.push_back(data);
+ }
+ }
+ return buckets;
+}
+
+void
+FileScannerTest::createData(const TestParameters& params,
+ std::vector<BucketData>& buckets,
+ std::vector<std::string>& tempFiles,
+ std::vector<std::string>& alienFiles)
+{
+ if (params.bucketWrongDir) {
+ CPPUNIT_ASSERT(params.dirLevels > 0);
+ buckets[0].directory[0] = (buckets[0].directory[0] + 1)
+ % params.dirSpread;
+ }
+ if (params.bucketMultipleDirs) {
+ CPPUNIT_ASSERT(params.dirLevels > 0);
+ BucketData copy(buckets[1]);
+ copy.directory[0] = (buckets[1].directory[0] + 1) % params.dirSpread;
+ buckets.push_back(copy);
+ }
+ if (params.bucketMultipleDisks && params.dirLevels > 0) {
+ BucketData copy(buckets[2]);
+ uint32_t disk = 0;
+ for (; disk<params.diskCount; ++disk) {
+ if (disk == copy.disk) continue;
+ if (params.disksDown.find(disk) == params.disksDown.end()) break;
+ }
+ CPPUNIT_ASSERT(disk < params.diskCount);
+ copy.disk = disk;
+ buckets.push_back(copy);
+ }
+
+ run("mkdir -p vdsroot");
+ run("chmod -R a+rwx vdsroot");
+ run("rm -rf vdsroot");
+ run("mkdir -p vdsroot/disks");
+ vespalib::RandomGen randomizer;
+ uint32_t diskToHaveBrokenSymlink = (params.disksDown.empty()
+ ? 0 : randomizer.nextUint32(0, params.disksDown.size()));
+ uint32_t downIndex = 0;
+ for (uint32_t i=0; i<params.diskCount; ++i) {
+ if (params.disksDown.find(i) != params.disksDown.end()) {
+ if (downIndex++ == diskToHaveBrokenSymlink
+ && params.diskDownWithBrokenSymlink)
+ {
+ std::ostringstream path;
+ path << "vdsroot/disks/d" << i;
+ run("ln -s /non-existing-dir " + path.str());
+ }
+ } else {
+ std::ostringstream path;
+ path << "vdsroot/disks/d" << i;
+ run("mkdir -p " + path.str());
+ std::ofstream of((path.str() + "/chunkinfo").c_str());
+ of << "#chunkinfo\n" << i << "\n" << params.diskCount << "\n";
+ }
+ }
+ for (uint32_t i=0; i<buckets.size(); ++i) {
+ if (!buckets[i].shouldExist) continue;
+ std::ostringstream path;
+ path << "vdsroot/disks/d" << buckets[i].disk << std::hex;
+ for (uint32_t j=0; j<buckets[i].directory.size(); ++j) {
+ path << '/' << std::setw(4) << std::setfill('0')
+ << buckets[i].directory[j];
+ }
+ run("mkdir -p " + path.str());
+ if (params.dirWithNoListPermission && i == 8) {
+ run("chmod a-r " + path.str());
+ // Scanner will abort with exception, so we don't really know
+ // how many docs will not be found due to this.
+ continue;
+ }
+ if (params.dirWithNoExecutePermission && i == 9) {
+ run("chmod a-x " + path.str());
+ // Scanner will abort with exception, so we don't really know
+ // how many docs will not be found due to this.
+ continue;
+ }
+ path << '/' << std::setw(16) << std::setfill('0')
+ << buckets[i].bucket.getId() << ".0";
+ run("touch " + path.str());
+ if (params.addTemporaryFiles && i == 4) {
+ run("touch " + path.str() + ".tmp");
+ tempFiles.push_back(path.str() + ".tmp");
+ }
+ if (params.addAlienFiles && i == 6) {
+ run("touch " + path.str() + ".alien");
+ alienFiles.push_back(path.str() + ".alien");
+ }
+ if (params.fileWithNoWritePermission && i == 0) {
+ // Overlapping with wrong dir so it would want to move file
+ run("chmod a-w " + path.str());
+ }
+ if (params.fileWithNoReadPermission && i == 0) {
+ // Overlapping with wrong dir so it would want to move file
+ run("chmod a-r " + path.str());
+ }
+ if (params.dirWithNoWritePermission && i == 9) {
+ run("chmod a-w " + path.str());
+ }
+ }
+}
+
+namespace {
+ struct BucketDataFound {
+ uint16_t _disk;
+ bool _checked;
+
+ BucketDataFound() : _disk(65535), _checked(false) {}
+ BucketDataFound(uint32_t disk) : _disk(disk), _checked(false) {}
+ };
+}
+
+void
+FileScannerTest::runTest(const TestParameters& params)
+{
+ std::vector<BucketData> buckets(createBuckets(params));
+ std::vector<std::string> tempFiles;
+ std::vector<std::string> alienFiles;
+ createData(params, buckets, tempFiles, alienFiles);
+
+ framework::defaultimplementation::RealClock clock;
+ framework::defaultimplementation::ComponentRegisterImpl compReg;
+ compReg.setClock(clock);
+
+ MountPointList mountPoints("./vdsroot",
+ std::vector<vespalib::string>(),
+ vespalib::LinkedPtr<DeviceManager>(
+ new DeviceManager(
+ DeviceMapper::UP(new SimpleDeviceMapper),
+ clock)));
+ mountPoints.init(params.diskCount);
+
+ FileScanner scanner(compReg, mountPoints,
+ params.dirLevels, params.dirSpread);
+ std::map<document::BucketId, BucketDataFound> foundBuckets;
+ uint32_t extraBucketsSameDisk = 0;
+ uint32_t extraBucketsOtherDisk = 0;
+ for (uint32_t j=0; j<params.diskCount; ++j) {
+ // std::cerr << "Disk " << j << "\n";
+ if (params.disksDown.find(j) != params.disksDown.end()) continue;
+ for (uint32_t i=0; i<params.parts; ++i) {
+ document::BucketId::List bucketList;
+ try{
+ scanner.buildBucketList(bucketList, j, i, params.parts);
+ for (uint32_t k=0; k<bucketList.size(); ++k) {
+ if (foundBuckets.find(bucketList[k]) != foundBuckets.end())
+ {
+ if (j == foundBuckets[bucketList[k]]._disk) {
+ ++extraBucketsSameDisk;
+ } else {
+ ++extraBucketsOtherDisk;
+ }
+// std::cerr << "Bucket " << bucketList[k]
+// << " on disk " << j << " is already found on disk "
+// << foundBuckets[bucketList[k]]._disk << ".\n";
+ }
+ foundBuckets[bucketList[k]] = BucketDataFound(j);
+ }
+ } catch (vespalib::IoException& e) {
+ if (!(params.dirWithNoListPermission
+ && e.getType() == vespalib::IoException::NO_PERMISSION))
+ {
+ throw;
+ }
+ }
+ }
+ }
+ std::vector<BucketData> notFound;
+ std::vector<BucketData> wasFound;
+ std::vector<BucketDataFound> foundNonExisting;
+ // Verify that found buckets match buckets expected.
+ for (uint32_t i=0; i<buckets.size(); ++i) {
+ std::map<document::BucketId, BucketDataFound>::iterator found(
+ foundBuckets.find(buckets[i].bucket));
+ if (buckets[i].shouldExist && found == foundBuckets.end()) {
+ notFound.push_back(buckets[i]);
+ } else if (!buckets[i].shouldExist && found != foundBuckets.end()) {
+ wasFound.push_back(buckets[i]);
+ }
+ if (found != foundBuckets.end()) { found->second._checked = true; }
+ }
+ for (std::map<document::BucketId, BucketDataFound>::iterator it
+ = foundBuckets.begin(); it != foundBuckets.end(); ++it)
+ {
+ if (!it->second._checked) {
+ foundNonExisting.push_back(it->second);
+ }
+ }
+ if (params.dirWithNoListPermission) {
+ CPPUNIT_ASSERT(!notFound.empty());
+ } else if (!notFound.empty()) {
+ std::ostringstream ost;
+ ost << "Failed to find " << notFound.size() << " of "
+ << buckets.size() << " buckets. Including buckets:";
+ for (uint32_t i=0; i<5 && i<notFound.size(); ++i) {
+ ost << " " << notFound[i].bucket;
+ }
+ CPPUNIT_FAIL(ost.str());
+ }
+ CPPUNIT_ASSERT(wasFound.empty());
+ CPPUNIT_ASSERT(foundNonExisting.empty());
+ if (params.bucketMultipleDirs) {
+ // TODO: Test something else here? This is not correct test, as when
+ // there are two buckets on the same disk, one of them will be ignored by
+ // the bucket lister.
+ // CPPUNIT_ASSERT_EQUAL(1u, extraBucketsSameDisk);
+ } else {
+ CPPUNIT_ASSERT_EQUAL(0u, extraBucketsSameDisk);
+ }
+ if (params.bucketMultipleDisks) {
+ CPPUNIT_ASSERT_EQUAL(1u, extraBucketsOtherDisk);
+ } else {
+ CPPUNIT_ASSERT_EQUAL(0u, extraBucketsOtherDisk);
+ }
+ if (params.addTemporaryFiles) {
+ CPPUNIT_ASSERT_EQUAL(
+ 1, int(scanner.getMetrics()._temporaryFilesDeleted.getValue()));
+ } else {
+ CPPUNIT_ASSERT_EQUAL(
+ 0, int(scanner.getMetrics()._temporaryFilesDeleted.getValue()));
+ }
+ if (params.addAlienFiles) {
+ CPPUNIT_ASSERT_EQUAL(
+ 1, int(scanner.getMetrics()._alienFileCounter.getValue()));
+ } else {
+ CPPUNIT_ASSERT_EQUAL(
+ 0, int(scanner.getMetrics()._alienFileCounter.getValue()));
+ }
+ // We automatically delete temporary files (created by VDS, indicating
+ // an operation that only half finished.
+ for (uint32_t i=0; i<tempFiles.size(); ++i) {
+ CPPUNIT_ASSERT_MSG(tempFiles[i], !vespalib::fileExists(tempFiles[i]));
+ }
+ // We don't automatically delete alien files
+ for (uint32_t i=0; i<alienFiles.size(); ++i) {
+ CPPUNIT_ASSERT_MSG(alienFiles[i], vespalib::fileExists(alienFiles[i]));
+ }
+}
+
+} // memfile
+} // storage
diff --git a/memfilepersistence/src/tests/mapper/.gitignore b/memfilepersistence/src/tests/mapper/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/memfilepersistence/src/tests/mapper/.gitignore
diff --git a/memfilepersistence/src/tests/spi/.gitignore b/memfilepersistence/src/tests/spi/.gitignore
new file mode 100644
index 00000000000..7e7c0fe7fae
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/.gitignore
@@ -0,0 +1,2 @@
+/.depend
+/Makefile
diff --git a/memfilepersistence/src/tests/spi/CMakeLists.txt b/memfilepersistence/src/tests/spi/CMakeLists.txt
new file mode 100644
index 00000000000..d5dade96f57
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/CMakeLists.txt
@@ -0,0 +1,20 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_library(memfilepersistence_testspi
+ SOURCES
+ memfiletestutils.cpp
+ providerconformancetest.cpp
+ memfilev1serializertest.cpp
+ memfilev1verifiertest.cpp
+ basicoperationhandlertest.cpp
+ splitoperationhandlertest.cpp
+ joinoperationhandlertest.cpp
+ iteratorhandlertest.cpp
+ memfiletest.cpp
+ memcachetest.cpp
+ simplememfileiobuffertest.cpp
+ memfileautorepairtest.cpp
+ shared_data_location_tracker_test.cpp
+ buffered_file_writer_test.cpp
+ buffer_test.cpp
+ DEPENDS
+)
diff --git a/memfilepersistence/src/tests/spi/basicoperationhandlertest.cpp b/memfilepersistence/src/tests/spi/basicoperationhandlertest.cpp
new file mode 100644
index 00000000000..2f7913b0e1f
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/basicoperationhandlertest.cpp
@@ -0,0 +1,735 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <tests/spi/memfiletestutils.h>
+#include <tests/spi/simulatedfailurefile.h>
+#include <tests/spi/options_builder.h>
+#include <vespa/document/fieldset/fieldsetrepo.h>
+#include <vespa/document/fieldset/fieldsets.h>
+
+namespace storage {
+namespace memfile {
+namespace {
+ spi::LoadType defaultLoadType(0, "default");
+}
+
+class BasicOperationHandlerTest : public SingleDiskMemFileTestUtils
+{
+ CPPUNIT_TEST_SUITE(BasicOperationHandlerTest);
+ CPPUNIT_TEST(testGetHeaderOnly);
+ CPPUNIT_TEST(testGetFieldFiltering);
+ CPPUNIT_TEST(testRemove);
+ CPPUNIT_TEST(testRemoveWithNonMatchingTimestamp);
+ CPPUNIT_TEST(testRemoveWithNonMatchingTimestampAlwaysPersist);
+ CPPUNIT_TEST(testRemoveForExistingRemoveSameTimestamp);
+ CPPUNIT_TEST(testRemoveForExistingRemoveNewTimestamp);
+ CPPUNIT_TEST(testRemoveForExistingRemoveNewTimestampAlwaysPersist);
+ CPPUNIT_TEST(testRemoveDocumentNotFound);
+ CPPUNIT_TEST(testRemoveDocumentNotFoundAlwaysPersist);
+ CPPUNIT_TEST(testRemoveExistingOlderDocumentVersion);
+ CPPUNIT_TEST(testPutSameTimestampAsRemove);
+ CPPUNIT_TEST(testUpdateBody);
+ CPPUNIT_TEST(testUpdateHeaderOnly);
+ CPPUNIT_TEST(testUpdateTimestampExists);
+ CPPUNIT_TEST(testUpdateForNonExistentDocWillFail);
+ CPPUNIT_TEST(testUpdateMayCreateDoc);
+ CPPUNIT_TEST(testRemoveEntry);
+ CPPUNIT_TEST(testEraseFromCacheOnFlushException);
+ CPPUNIT_TEST(testEraseFromCacheOnMaintainException);
+ CPPUNIT_TEST(testEraseFromCacheOnDeleteBucketException);
+ CPPUNIT_TEST_SUITE_END();
+
+ void doTestRemoveDocumentNotFound(
+ OperationHandler::RemoveType persistRemove);
+ void doTestRemoveWithNonMatchingTimestamp(
+ OperationHandler::RemoveType persistRemove);
+ void doTestRemoveForExistingRemoveNewTimestamp(
+ OperationHandler::RemoveType persistRemove);
+public:
+ void setupTestConfig();
+ void testPutHeadersOnly();
+ void testPutHeadersOnlyDocumentNotFound();
+ void testPutHeadersOnlyTimestampNotFound();
+ void testGetHeaderOnly();
+ void testGetFieldFiltering();
+ void testRemove();
+ void testRemoveWithNonMatchingTimestamp();
+ void testRemoveWithNonMatchingTimestampAlwaysPersist();
+ void testRemoveForExistingRemoveSameTimestamp();
+ void testRemoveForExistingRemoveNewTimestamp();
+ void testRemoveForExistingRemoveNewTimestampAlwaysPersist();
+ void testRemoveDocumentNotFound();
+ void testRemoveDocumentNotFoundAlwaysPersist();
+ void testRemoveExistingOlderDocumentVersion();
+ void testPutSameTimestampAsRemove();
+ void testUpdateBody();
+ void testUpdateHeaderOnly();
+ void testUpdateTimestampExists();
+ void testUpdateForNonExistentDocWillFail();
+ void testUpdateMayCreateDoc();
+ void testRemoveEntry();
+ void testEraseFromCacheOnFlushException();
+ void testEraseFromCacheOnMaintainException();
+ void testEraseFromCacheOnDeleteBucketException();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(BasicOperationHandlerTest);
+
+/**
+ * Test that doing a header-only get gives back a document containing
+ * only the document header
+ */
+void
+BasicOperationHandlerTest::testGetHeaderOnly()
+{
+ document::BucketId bucketId(16, 4);
+
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ doc->setValue(doc->getField("hstringval"), document::StringFieldValue("hypnotoad"));
+ doc->setValue(doc->getField("headerval"), document::IntFieldValue(42));
+
+ doPut(doc, bucketId, Timestamp(4567), 0);
+ flush(bucketId);
+
+ spi::GetResult reply = doGet(bucketId, doc->getId(), document::HeaderFields());
+
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, reply.getErrorCode());
+ CPPUNIT_ASSERT(reply.hasDocument());
+ CPPUNIT_ASSERT_EQUAL(std::string("headerval: 42\nhstringval: hypnotoad\n"),
+ stringifyFields(reply.getDocument()));
+ CPPUNIT_ASSERT_EQUAL(
+ size_t(1),
+ getPersistenceProvider().getMetrics().headerOnlyGets.getValue());
+}
+
+void
+BasicOperationHandlerTest::testGetFieldFiltering()
+{
+ document::BucketId bucketId(16, 4);
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ doc->setValue(doc->getField("headerval"), document::IntFieldValue(42));
+ doc->setValue(doc->getField("hstringval"),
+ document::StringFieldValue("groovy"));
+
+ document::FieldSetRepo repo;
+
+ doPut(doc, bucketId, Timestamp(4567), 0);
+ flush(bucketId);
+ spi::GetResult reply(doGet(bucketId,
+ doc->getId(),
+ *repo.parse(*getTypeRepo(), "testdoctype1:hstringval")));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, reply.getErrorCode());
+ CPPUNIT_ASSERT(reply.hasDocument());
+ CPPUNIT_ASSERT_EQUAL(std::string("hstringval: groovy\n"),
+ stringifyFields(reply.getDocument()));
+ CPPUNIT_ASSERT_EQUAL(
+ size_t(1),
+ getPersistenceProvider().getMetrics().headerOnlyGets.getValue());
+}
+
+void
+BasicOperationHandlerTest::testRemove()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucketId(16, 4);
+
+ document::Document::SP doc = doPut(4, Timestamp(1));
+
+ CPPUNIT_ASSERT_EQUAL(true, doRemove(bucketId,
+ doc->getId(),
+ Timestamp(2),
+ OperationHandler::PERSIST_REMOVE_IF_FOUND));
+
+ getPersistenceProvider().flush(
+ spi::Bucket(bucketId, spi::PartitionId(0)), context);
+
+ env()._cache.clear();
+
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(*doc, *file->getDocument((*file)[0], ALL));
+
+ CPPUNIT_ASSERT_EQUAL(Timestamp(2), (*file)[1].getTimestamp());
+ CPPUNIT_ASSERT((*file)[1].deleted());
+ CPPUNIT_ASSERT_EQUAL(DataLocation(0, 0), (*file)[1].getLocation(BODY));
+ CPPUNIT_ASSERT_EQUAL((*file)[0].getLocation(HEADER),
+ (*file)[1].getLocation(HEADER));
+}
+
+/**
+ * Test that removing a document with a max timestamp for which there
+ * is no matching document does not add a remove slot to the memfile
+ */
+void
+BasicOperationHandlerTest::doTestRemoveWithNonMatchingTimestamp(
+ OperationHandler::RemoveType persistRemove)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucketId(16, 4);
+ document::Document::SP doc = doPut(4, Timestamp(1234));
+
+ CPPUNIT_ASSERT_EQUAL(false, doRemove(bucketId,
+ doc->getId(),
+ Timestamp(1233),
+ persistRemove));
+
+ getPersistenceProvider().flush(
+ spi::Bucket(bucketId, spi::PartitionId(0)), context);
+
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(
+ uint32_t(persistRemove == OperationHandler::ALWAYS_PERSIST_REMOVE
+ ? 2 : 1),
+ file->getSlotCount());
+
+ int i = 0;
+ if (persistRemove == OperationHandler::ALWAYS_PERSIST_REMOVE) {
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1233), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT((*file)[0].deleted());
+ CPPUNIT_ASSERT_EQUAL(DataLocation(0, 0), (*file)[0].getLocation(BODY));
+ CPPUNIT_ASSERT((*file)[0].getLocation(HEADER)
+ != (*file)[1].getLocation(HEADER));
+ CPPUNIT_ASSERT_EQUAL(doc->getId(), file->getDocumentId((*file)[0]));
+ ++i;
+ }
+
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1234), (*file)[i].getTimestamp());
+ CPPUNIT_ASSERT(!(*file)[i].deleted());
+ CPPUNIT_ASSERT(file->getDocument((*file)[i], ALL)->getValue("content").get());
+}
+
+/**
+ * Test that removing a document with a max timestamp for which there
+ * is no matching document does not add a remove slot to the memfile
+ */
+void
+BasicOperationHandlerTest::testRemoveWithNonMatchingTimestamp()
+{
+ doTestRemoveWithNonMatchingTimestamp(
+ OperationHandler::PERSIST_REMOVE_IF_FOUND);
+}
+
+void
+BasicOperationHandlerTest::testRemoveWithNonMatchingTimestampAlwaysPersist()
+{
+ doTestRemoveWithNonMatchingTimestamp(
+ OperationHandler::ALWAYS_PERSIST_REMOVE);
+}
+
+/**
+ * Test that doing a remove with a timestamp for which there already
+ * exists a remove does not add another remove slot
+ */
+void
+BasicOperationHandlerTest::testRemoveForExistingRemoveSameTimestamp()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucketId(16, 4);
+ document::Document::SP doc = doPut(4, Timestamp(1234));
+
+ CPPUNIT_ASSERT_EQUAL(true, doRemove(bucketId,
+ doc->getId(),
+ Timestamp(1235),
+ OperationHandler::PERSIST_REMOVE_IF_FOUND));
+ CPPUNIT_ASSERT_EQUAL(false, doRemove(bucketId,
+ doc->getId(),
+ Timestamp(1235),
+ OperationHandler::PERSIST_REMOVE_IF_FOUND));
+
+ getPersistenceProvider().flush(
+ spi::Bucket(bucketId, spi::PartitionId(0)), context);
+
+ // Should only be one remove entry still
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1234), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT(file->getDocument((*file)[0], ALL)->getValue("content").get());
+
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1235), (*file)[1].getTimestamp());
+ CPPUNIT_ASSERT((*file)[1].deleted());
+}
+
+void
+BasicOperationHandlerTest::doTestRemoveForExistingRemoveNewTimestamp(
+ OperationHandler::RemoveType persistRemove)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucketId(16, 4);
+ document::Document::SP doc = doPut(4, Timestamp(1234));
+
+ CPPUNIT_ASSERT_EQUAL(true, doRemove(bucketId,
+ doc->getId(),
+ Timestamp(1235),
+ OperationHandler::PERSIST_REMOVE_IF_FOUND));
+ CPPUNIT_ASSERT_EQUAL(false, doRemove(bucketId,
+ doc->getId(),
+ Timestamp(1236),
+ persistRemove));
+
+ getPersistenceProvider().flush(
+ spi::Bucket(bucketId, spi::PartitionId(0)), context);
+
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(
+ uint32_t(persistRemove == OperationHandler::ALWAYS_PERSIST_REMOVE
+ ? 3 : 2),
+ file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1234), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT(file->getDocument((*file)[0], ALL)->getValue("content").get());
+
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1235), (*file)[1].getTimestamp());
+ CPPUNIT_ASSERT((*file)[1].deleted());
+
+ if (persistRemove == OperationHandler::ALWAYS_PERSIST_REMOVE) {
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1236), (*file)[2].getTimestamp());
+ CPPUNIT_ASSERT((*file)[2].deleted());
+ }
+}
+
+/**
+ * Test that doing a second remove with a newer timestamp does not add
+ * another remove slot when PERSIST_REMOVE_IF_FOUND is specified
+ */
+void
+BasicOperationHandlerTest::testRemoveForExistingRemoveNewTimestamp()
+{
+ doTestRemoveForExistingRemoveNewTimestamp(
+ OperationHandler::PERSIST_REMOVE_IF_FOUND);
+}
+
+void
+BasicOperationHandlerTest::testRemoveForExistingRemoveNewTimestampAlwaysPersist()
+{
+ doTestRemoveForExistingRemoveNewTimestamp(
+ OperationHandler::ALWAYS_PERSIST_REMOVE);
+}
+
+/**
+ * Test removing an older version of a document. Older version should be removed
+ * in-place without attempting to add a new slot (which would fail).
+ */
+void
+BasicOperationHandlerTest::testRemoveExistingOlderDocumentVersion()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucketId(16, 4);
+ document::Document::SP doc = doPut(4, Timestamp(1234));
+
+ CPPUNIT_ASSERT_EQUAL(true, doRemove(bucketId,
+ doc->getId(),
+ Timestamp(1235),
+ OperationHandler::ALWAYS_PERSIST_REMOVE));
+
+ getPersistenceProvider().flush(
+ spi::Bucket(bucketId, spi::PartitionId(0)), context);
+
+ CPPUNIT_ASSERT_EQUAL(true, doRemove(bucketId,
+ doc->getId(),
+ Timestamp(1234),
+ OperationHandler::ALWAYS_PERSIST_REMOVE));
+
+ getPersistenceProvider().flush(
+ spi::Bucket(bucketId, spi::PartitionId(0)), context);
+
+ // Should now be two remove entries.
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1234), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(doc->getId(), file->getDocumentId((*file)[0]));
+ CPPUNIT_ASSERT((*file)[0].deleted());
+
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1235), (*file)[1].getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(doc->getId(), file->getDocumentId((*file)[1]));
+ CPPUNIT_ASSERT((*file)[1].deleted());
+}
+
+void
+BasicOperationHandlerTest::doTestRemoveDocumentNotFound(
+ OperationHandler::RemoveType persistRemove)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucketId(16, 4);
+ document::DocumentId docId("userdoc:test:4:0");
+ doPut(4, Timestamp(1234));
+
+ CPPUNIT_ASSERT_EQUAL(false,
+ doRemove(bucketId,
+ docId,
+ Timestamp(1235),
+ persistRemove));
+
+ getPersistenceProvider().flush(
+ spi::Bucket(bucketId, spi::PartitionId(0)), context);
+
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(
+ uint32_t(persistRemove == OperationHandler::ALWAYS_PERSIST_REMOVE
+ ? 2 : 1),
+ file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1234), (*file)[0].getTimestamp());
+ if (persistRemove == OperationHandler::ALWAYS_PERSIST_REMOVE) {
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1235), (*file)[1].getTimestamp());
+ CPPUNIT_ASSERT((*file)[1].deleted());
+ CPPUNIT_ASSERT_EQUAL(docId, file->getDocumentId((*file)[1]));
+ }
+/* TODO: Test this in service layer tests.
+ CPPUNIT_ASSERT_EQUAL(
+ uint64_t(1),
+ env()._metrics.remove[documentapi::LoadType::DEFAULT].notFound.getValue());
+*/
+}
+
+/**
+ * Test that removing a non-existing document when PERSIST_EXISTING_ONLY is
+ * specified does not add a remove entry
+ */
+void
+BasicOperationHandlerTest::testRemoveDocumentNotFound()
+{
+ doTestRemoveDocumentNotFound(
+ OperationHandler::PERSIST_REMOVE_IF_FOUND);
+}
+
+void
+BasicOperationHandlerTest::testRemoveDocumentNotFoundAlwaysPersist()
+{
+ doTestRemoveDocumentNotFound(
+ OperationHandler::ALWAYS_PERSIST_REMOVE);
+}
+
+void
+BasicOperationHandlerTest::testPutSameTimestampAsRemove()
+{
+ document::BucketId bucketId(16, 4);
+
+ document::Document::SP doc = doPut(4, Timestamp(1234));
+
+ CPPUNIT_ASSERT_EQUAL(true, doRemove(bucketId,
+ doc->getId(),
+ Timestamp(1235),
+ OperationHandler::PERSIST_REMOVE_IF_FOUND));
+
+ // Flush here to avoid put+remove being thrown away by duplicate timestamp
+ // exception evicting the cache and unpersisted changes.
+ flush(bucketId);
+
+ doPut(4, Timestamp(1235));
+ flush(bucketId);
+
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1234), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT(file->getDocument((*file)[0], ALL)->getValue("content").get());
+
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1235), (*file)[1].getTimestamp());
+ CPPUNIT_ASSERT((*file)[1].deleted());
+}
+
+/**
+ * Test that updating body results in a new memfile slot containing
+ * an updated document
+ */
+void
+BasicOperationHandlerTest::testUpdateBody()
+{
+ document::BucketId bucketId(16, 4);
+ document::StringFieldValue updateValue("foo");
+ document::Document::SP doc = doPut(4, Timestamp(1234));
+ document::Document originalDoc(*doc);
+
+ document::DocumentUpdate::SP update = createBodyUpdate(
+ doc->getId(), updateValue);
+
+ spi::UpdateResult result = doUpdate(bucketId, update, Timestamp(5678));
+ flush(bucketId);
+ CPPUNIT_ASSERT_EQUAL(1234, (int)result.getExistingTimestamp());
+
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1234), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT(file->getDocument((*file)[0], ALL)->getValue("content").get());
+ CPPUNIT_ASSERT_EQUAL(*(originalDoc.getValue("content")),
+ *file->getDocument((*file)[0], ALL)->getValue("content"));
+
+ CPPUNIT_ASSERT_EQUAL(Timestamp(5678), (*file)[1].getTimestamp());
+ CPPUNIT_ASSERT(file->getDocument((*file)[1], ALL)->getValue("content").get());
+ CPPUNIT_ASSERT_EQUAL(updateValue,
+ dynamic_cast<document::StringFieldValue&>(
+ *file->getDocument((*file)[1], ALL)->getValue(
+ "content")));
+ CPPUNIT_ASSERT_EQUAL(
+ size_t(0),
+ getPersistenceProvider().getMetrics().headerOnlyUpdates.getValue());
+}
+
+void
+BasicOperationHandlerTest::testUpdateHeaderOnly()
+{
+ document::BucketId bucketId(16, 4);
+ document::IntFieldValue updateValue(42);
+ document::Document::SP doc = doPut(4, Timestamp(1234));
+
+ document::DocumentUpdate::SP update = createHeaderUpdate(
+ doc->getId(), updateValue);
+
+ spi::UpdateResult result = doUpdate(bucketId, update, Timestamp(5678));
+ flush(bucketId);
+ CPPUNIT_ASSERT_EQUAL(1234, (int)result.getExistingTimestamp());
+
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1234), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT(file->getDocument((*file)[0], ALL)->getValue("headerval").get() ==
+ NULL);
+
+ CPPUNIT_ASSERT_EQUAL(Timestamp(5678), (*file)[1].getTimestamp());
+ CPPUNIT_ASSERT(file->getDocument((*file)[1], ALL)->getValue("headerval").get());
+ CPPUNIT_ASSERT_EQUAL(updateValue,
+ dynamic_cast<document::IntFieldValue&>(
+ *file->getDocument((*file)[1], ALL)->getValue(
+ "headerval")));
+ CPPUNIT_ASSERT_EQUAL(
+ size_t(1),
+ getPersistenceProvider().getMetrics().headerOnlyUpdates.getValue());
+}
+
+void
+BasicOperationHandlerTest::testUpdateTimestampExists()
+{
+ document::BucketId bucketId(16, 4);
+ document::IntFieldValue updateValue(42);
+ document::Document::SP doc = doPut(4, Timestamp(1234));
+
+ document::DocumentUpdate::SP update = createHeaderUpdate(
+ doc->getId(), updateValue);
+
+ spi::UpdateResult result = doUpdate(bucketId, update, Timestamp(1234));
+ flush(bucketId);
+ CPPUNIT_ASSERT_EQUAL(spi::Result::TRANSIENT_ERROR, result.getErrorCode());
+}
+
+void
+BasicOperationHandlerTest::testUpdateForNonExistentDocWillFail()
+{
+ document::BucketId bucketId(16, 4);
+ document::IntFieldValue updateValue(42);
+ Timestamp timestamp(5678);
+
+ // Is there an easier way to get a DocumentId?
+ document::Document::UP doc(
+ createRandomDocumentAtLocation(4, timestamp.getTime()));
+ const DocumentId& documentId = doc->getId();
+
+ document::DocumentUpdate::SP update = createHeaderUpdate(
+ documentId, updateValue);
+
+ spi::UpdateResult result = doUpdate(bucketId, update, timestamp);
+ flush(bucketId);
+ CPPUNIT_ASSERT_EQUAL(0, (int)result.getExistingTimestamp());
+
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(0), file->getSlotCount());
+}
+
+void
+BasicOperationHandlerTest::testUpdateMayCreateDoc()
+{
+ document::BucketId bucketId(16, 4);
+ document::IntFieldValue updateValue(42);
+ Timestamp timestamp(5678);
+
+ // Is there an easier way to get a DocumentId?
+ document::Document::UP doc(
+ createRandomDocumentAtLocation(4, timestamp.getTime()));
+ const DocumentId& documentId = doc->getId();
+
+ document::DocumentUpdate::SP update = createHeaderUpdate(
+ documentId, updateValue);
+ update->setCreateIfNonExistent(true);
+
+ spi::UpdateResult result = doUpdate(bucketId, update, timestamp);
+ flush(bucketId);
+ CPPUNIT_ASSERT_EQUAL(timestamp.getTime(),
+ (uint64_t)result.getExistingTimestamp());
+
+ MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(timestamp, (*file)[0].getTimestamp());
+
+ auto headerval = file->getDocument((*file)[0], ALL)->getValue("headerval");
+ CPPUNIT_ASSERT(headerval.get() != nullptr);
+ CPPUNIT_ASSERT_EQUAL(updateValue,
+ dynamic_cast<document::IntFieldValue&>(*headerval));
+}
+
+void
+BasicOperationHandlerTest::testRemoveEntry()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucketId(16, 4);
+
+ doPut(4, Timestamp(1234));
+ Document::SP doc = doPut(4, Timestamp(2345));
+ doPut(4, Timestamp(3456));
+
+ getPersistenceProvider().removeEntry(
+ spi::Bucket(bucketId, spi::PartitionId(0)),
+ spi::Timestamp(1234), context);
+ getPersistenceProvider().removeEntry(
+ spi::Bucket(bucketId, spi::PartitionId(0)),
+ spi::Timestamp(3456), context);
+ flush(bucketId);
+
+ memfile::MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(2345), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(*doc, *file->getDocument((*file)[0], ALL));
+}
+
+void
+BasicOperationHandlerTest::setupTestConfig()
+{
+ using MemFileConfig = vespa::config::storage::StorMemfilepersistenceConfig;
+ using MemFileConfigBuilder
+ = vespa::config::storage::StorMemfilepersistenceConfigBuilder;
+ MemFileConfigBuilder builder(
+ *env().acquireConfigReadLock().memFilePersistenceConfig());
+ builder.minimumFileMetaSlots = 2;
+ builder.minimumFileHeaderBlockSize = 3000;
+ auto newConfig = std::unique_ptr<MemFileConfig>(new MemFileConfig(builder));
+ env().acquireConfigWriteLock().setMemFilePersistenceConfig(
+ std::move(newConfig));
+}
+
+void
+BasicOperationHandlerTest::testEraseFromCacheOnFlushException()
+{
+ document::BucketId bucketId(16, 4);
+
+ setupTestConfig();
+
+ document::Document::SP doc(
+ createRandomDocumentAtLocation(4, 2345, 1024, 1024));
+ doPut(doc, bucketId, Timestamp(2345));
+ flush(bucketId);
+ // Must throw out cache to re-create lazyfile
+ env()._cache.clear();
+
+ env()._lazyFileFactory =
+ std::unique_ptr<Environment::LazyFileFactory>(
+ new SimulatedFailureLazyFile::Factory);
+
+ // Try partial write, followed by full rewrite
+ for (int i = 0; i < 2; ++i) {
+ for (int j = 0; j < i+1; ++j) {
+ document::Document::SP doc2(
+ createRandomDocumentAtLocation(4, 4000 + j, 1500, 1500));
+ doPut(doc2, bucketId, Timestamp(4000 + j));
+ }
+ spi::Result result = flush(bucketId);
+ CPPUNIT_ASSERT(result.hasError());
+ CPPUNIT_ASSERT(result.getErrorMessage().find("A simulated I/O write")
+ != vespalib::string::npos);
+
+ CPPUNIT_ASSERT(!env()._cache.contains(bucketId));
+
+ // Check that we still have first persisted put
+ memfile::MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(2345), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(*doc, *file->getDocument((*file)[0], ALL));
+ }
+}
+
+void
+BasicOperationHandlerTest::testEraseFromCacheOnMaintainException()
+{
+ document::BucketId bucketId(16, 4);
+
+ setupTestConfig();
+
+ getFakeClock()._absoluteTime = framework::MicroSecTime(2000 * 1000000);
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options)
+ .revertTimePeriod(framework::MicroSecTime(100000ULL * 1000000))
+ .build());
+ // Put a doc twice to allow for revert time compaction to be done
+ document::Document::SP doc1(
+ createRandomDocumentAtLocation(4, 2345, 1024, 1024));
+ document::Document::SP doc2(
+ createRandomDocumentAtLocation(4, 2345, 1024, 1024));
+ doPut(doc1, bucketId, Timestamp(1000 * 1000000));
+ doPut(doc2, bucketId, Timestamp(1500 * 1000000));
+ flush(bucketId);
+ env()._cache.clear();
+
+ options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options)
+ .revertTimePeriod(framework::MicroSecTime(100ULL * 1000000))
+ .build());
+
+ env()._lazyFileFactory =
+ std::unique_ptr<Environment::LazyFileFactory>(
+ new SimulatedFailureLazyFile::Factory);
+
+ spi::Result result = getPersistenceProvider().maintain(
+ spi::Bucket(bucketId, spi::PartitionId(0)),
+ spi::HIGH);
+ CPPUNIT_ASSERT(result.hasError());
+ CPPUNIT_ASSERT(result.getErrorMessage().find("A simulated I/O write")
+ != vespalib::string::npos);
+
+ CPPUNIT_ASSERT(!env()._cache.contains(bucketId));
+
+ // Check that we still have both persisted puts
+ memfile::MemFilePtr file(getMemFile(bucketId));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1000 * 1000000), (*file)[0].getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(*doc1, *file->getDocument((*file)[0], ALL));
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1500 * 1000000), (*file)[1].getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(*doc2, *file->getDocument((*file)[1], ALL));
+}
+
+void
+BasicOperationHandlerTest::testEraseFromCacheOnDeleteBucketException()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucketId(16, 4);
+ document::Document::SP doc(
+ createRandomDocumentAtLocation(4, 2345, 1024, 1024));
+ doPut(doc, bucketId, Timestamp(2345));
+ flush(bucketId);
+ env()._cache.clear();
+
+ SimulatedFailureLazyFile::Factory* factory(
+ new SimulatedFailureLazyFile::Factory);
+ factory->setReadOpsBeforeFailure(0);
+ env()._lazyFileFactory =
+ std::unique_ptr<Environment::LazyFileFactory>(factory);
+
+ // loadFile will fail
+ spi::Result result = getPersistenceProvider().deleteBucket(
+ spi::Bucket(bucketId, spi::PartitionId(0)), context);
+ CPPUNIT_ASSERT(result.hasError());
+ CPPUNIT_ASSERT(result.getErrorMessage().find("A simulated I/O read")
+ != vespalib::string::npos);
+
+ CPPUNIT_ASSERT(!env()._cache.contains(bucketId));
+
+}
+
+}
+
+}
diff --git a/memfilepersistence/src/tests/spi/buffer_test.cpp b/memfilepersistence/src/tests/spi/buffer_test.cpp
new file mode 100644
index 00000000000..a2d917301fc
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/buffer_test.cpp
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/memfilepersistence/mapper/buffer.h>
+
+namespace storage {
+namespace memfile {
+
+class BufferTest : public CppUnit::TestFixture
+{
+public:
+ void getSizeReturnsInitiallyAllocatedSize();
+ void getSizeReturnsUnAlignedSizeForMMappedAllocs();
+ void resizeRetainsExistingDataWhenSizingUp();
+ void resizeRetainsExistingDataWhenSizingDown();
+ void bufferAddressIs512ByteAligned();
+
+ CPPUNIT_TEST_SUITE(BufferTest);
+ CPPUNIT_TEST(getSizeReturnsInitiallyAllocatedSize);
+ CPPUNIT_TEST(getSizeReturnsUnAlignedSizeForMMappedAllocs);
+ CPPUNIT_TEST(resizeRetainsExistingDataWhenSizingUp);
+ CPPUNIT_TEST(resizeRetainsExistingDataWhenSizingDown);
+ CPPUNIT_TEST(bufferAddressIs512ByteAligned);
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(BufferTest);
+
+void
+BufferTest::getSizeReturnsInitiallyAllocatedSize()
+{
+ Buffer buf(1234);
+ CPPUNIT_ASSERT_EQUAL(size_t(1234), buf.getSize());
+}
+
+void
+BufferTest::getSizeReturnsUnAlignedSizeForMMappedAllocs()
+{
+ Buffer buf(vespalib::MMapAlloc::HUGEPAGE_SIZE + 1);
+ CPPUNIT_ASSERT_EQUAL(size_t(vespalib::MMapAlloc::HUGEPAGE_SIZE + 1),
+ buf.getSize());
+}
+
+void
+BufferTest::resizeRetainsExistingDataWhenSizingUp()
+{
+ std::string src = "hello world";
+ Buffer buf(src.size());
+ memcpy(buf.getBuffer(), src.data(), src.size());
+ buf.resize(src.size() * 2);
+ CPPUNIT_ASSERT_EQUAL(src.size() * 2, buf.getSize());
+ CPPUNIT_ASSERT_EQUAL(0, memcmp(buf.getBuffer(), src.data(), src.size()));
+}
+
+void
+BufferTest::resizeRetainsExistingDataWhenSizingDown()
+{
+ std::string src = "hello world";
+ Buffer buf(src.size());
+ memcpy(buf.getBuffer(), src.data(), src.size());
+ buf.resize(src.size() / 2);
+ CPPUNIT_ASSERT_EQUAL(src.size() / 2, buf.getSize());
+ CPPUNIT_ASSERT_EQUAL(0, memcmp(buf.getBuffer(), src.data(), src.size() / 2));
+}
+
+void
+BufferTest::bufferAddressIs512ByteAligned()
+{
+ Buffer buf(32);
+ CPPUNIT_ASSERT(reinterpret_cast<size_t>(buf.getBuffer()) % 512 == 0);
+}
+
+} // memfile
+} // storage
+
diff --git a/memfilepersistence/src/tests/spi/buffered_file_writer_test.cpp b/memfilepersistence/src/tests/spi/buffered_file_writer_test.cpp
new file mode 100644
index 00000000000..b59e8a32258
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/buffered_file_writer_test.cpp
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/memfilepersistence/mapper/bufferedfilewriter.h>
+#include <vespa/memfilepersistence/mapper/buffer.h>
+#include <vespa/vespalib/io/fileutil.h>
+
+namespace storage {
+namespace memfile {
+
+class BufferedFileWriterTest : public CppUnit::TestFixture
+{
+public:
+ void noImplicitFlushingWhenDestructing();
+
+ CPPUNIT_TEST_SUITE(BufferedFileWriterTest);
+ CPPUNIT_TEST(noImplicitFlushingWhenDestructing);
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(BufferedFileWriterTest);
+
+namespace {
+
+// Partial mock of vespalib::File. Unfortunately, there's currently no
+// base interface to implement so have to override a class that already has
+// implementation code present.
+class MockFile : public vespalib::File
+{
+public:
+ bool _didWrite;
+
+ MockFile(const std::string& filename)
+ : File(filename),
+ _didWrite(false)
+ {
+ }
+
+ void open(int flags, bool autoCreateDirectories) override {
+ (void) flags;
+ (void) autoCreateDirectories;
+ // Don't do anything here to prevent us from actually opening a file
+ // on disk.
+ }
+
+ off_t write(const void *buf, size_t bufsize, off_t offset) override {
+ (void) buf;
+ (void) bufsize;
+ (void) offset;
+ _didWrite = true;
+ return 0;
+ }
+};
+
+}
+
+void
+BufferedFileWriterTest::noImplicitFlushingWhenDestructing()
+{
+ MockFile file("foo");
+ {
+ Buffer buffer(1024);
+ BufferedFileWriter writer(file, buffer, buffer.getSize());
+ // Do a buffered write. This fits well within the buffer and should
+ // consequently not be immediately written out to the backing file.
+ writer.write("blarg", 5);
+ // Escape scope without having flushed anything.
+ }
+ // Since BufferedFileWriter is meant to be used with O_DIRECT files,
+ // flushing just implies writing rather than syncing (this is a half truth
+ // since you still sync directories etc to ensure metadata is written, but
+ // this constrained assumption works fine in the context of this test).
+ CPPUNIT_ASSERT(!file._didWrite);
+}
+
+} // memfile
+} // storage
+
diff --git a/memfilepersistence/src/tests/spi/iteratorhandlertest.cpp b/memfilepersistence/src/tests/spi/iteratorhandlertest.cpp
new file mode 100644
index 00000000000..6fea98e3c8e
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/iteratorhandlertest.cpp
@@ -0,0 +1,940 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <set>
+#include <vector>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/memfilepersistence/mapper/simplememfileiobuffer.h>
+#include <tests/spi/memfiletestutils.h>
+#include <tests/spi/simulatedfailurefile.h>
+#include <tests/spi/options_builder.h>
+#include <vespa/document/fieldset/fieldsets.h>
+
+namespace storage {
+namespace memfile {
+namespace {
+ spi::LoadType defaultLoadType(0, "default");
+}
+
+class IteratorHandlerTest : public SingleDiskMemFileTestUtils
+{
+ CPPUNIT_TEST_SUITE(IteratorHandlerTest);
+ CPPUNIT_TEST(testCreateIterator);
+ CPPUNIT_TEST(testSomeSlotsRemovedBetweenInvocations);
+ CPPUNIT_TEST(testAllSlotsRemovedBetweenInvocations);
+ CPPUNIT_TEST(testIterateMetadataOnly);
+ CPPUNIT_TEST(testIterateHeadersOnly);
+ CPPUNIT_TEST(testIterateLargeDocument);
+ CPPUNIT_TEST(testDocumentsRemovedBetweenInvocations);
+ CPPUNIT_TEST(testUnrevertableRemoveBetweenInvocations);
+ CPPUNIT_TEST(testUnrevertableRemoveBetweenInvocationsIncludeRemoves);
+ CPPUNIT_TEST(testMatchTimestampRangeDocAltered);
+ CPPUNIT_TEST(testIterateAllVersions);
+ CPPUNIT_TEST(testFieldSetFiltering);
+ CPPUNIT_TEST(testIteratorInactiveOnException);
+ CPPUNIT_TEST(testDocsCachedBeforeDocumentSelection);
+ CPPUNIT_TEST(testTimestampRangeLimitedPrefetch);
+ CPPUNIT_TEST(testCachePrefetchRequirements);
+ CPPUNIT_TEST(testBucketEvictedFromCacheOnIterateException);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void testCreateIterator();
+ void testSomeSlotsRemovedBetweenInvocations();
+ void testAllSlotsRemovedBetweenInvocations();
+ void testIterateMetadataOnly();
+ void testIterateHeadersOnly();
+ void testIterateLargeDocument();
+ void testDocumentsRemovedBetweenInvocations();
+ void testUnrevertableRemoveBetweenInvocations();
+ void testUnrevertableRemoveBetweenInvocationsIncludeRemoves();
+ void testMatchTimestampRangeDocAltered();
+ void testIterateAllVersions();
+ void testFieldSetFiltering();
+ void testIteratorInactiveOnException();
+ void testDocsCachedBeforeDocumentSelection();
+ void testTimestampRangeLimitedPrefetch();
+ void testCachePrefetchRequirements();
+ void testBucketEvictedFromCacheOnIterateException();
+
+ void setUp();
+ void tearDown();
+
+ struct Chunk
+ {
+ std::vector<spi::DocEntry::LP> _entries;
+ };
+
+private:
+ spi::Selection createSelection(const std::string& docSel) const;
+
+
+ spi::CreateIteratorResult create(
+ const spi::Bucket& b,
+ const spi::Selection& sel,
+ spi::IncludedVersions versions = spi::NEWEST_DOCUMENT_ONLY,
+ const document::FieldSet& fieldSet = document::AllFields())
+ {
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ return getPersistenceProvider().createIterator(b, fieldSet, sel,
+ versions, context);
+ }
+
+ typedef std::pair<Document::SP, spi::Timestamp> DocAndTimestamp;
+
+ std::vector<DocAndTimestamp> feedDocs(size_t numDocs,
+ uint32_t minSize = 110,
+ uint32_t maxSize = 110);
+
+ std::vector<Chunk> doIterate(spi::IteratorId id,
+ uint64_t maxByteSize,
+ size_t maxChunks = 0,
+ bool allowEmptyResult = false);
+
+ void verifyDocs(const std::vector<DocAndTimestamp>& wanted,
+ const std::vector<IteratorHandlerTest::Chunk>& chunks,
+ const std::set<vespalib::string>& removes
+ = std::set<vespalib::string>()) const;
+
+ void doTestUnrevertableRemoveBetweenInvocations(bool includeRemoves);
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(IteratorHandlerTest);
+
+void
+IteratorHandlerTest::setUp()
+{
+ SingleDiskMemFileTestUtils::setUp();
+}
+
+void
+IteratorHandlerTest::tearDown()
+{
+ SingleDiskMemFileTestUtils::tearDown();
+}
+
+spi::Selection
+IteratorHandlerTest::createSelection(const std::string& docSel) const
+{
+ return spi::Selection(spi::DocumentSelection(docSel));
+}
+
+void
+IteratorHandlerTest::testCreateIterator()
+{
+ spi::Bucket b(BucketId(16, 1234), spi::PartitionId(0));
+
+ spi::CreateIteratorResult iter1(create(b, createSelection("true")));
+ CPPUNIT_ASSERT_EQUAL(spi::IteratorId(1), iter1.getIteratorId());
+
+ spi::CreateIteratorResult iter2(create(b, createSelection("true")));
+ CPPUNIT_ASSERT_EQUAL(spi::IteratorId(2), iter2.getIteratorId());
+}
+
+std::vector<IteratorHandlerTest::Chunk>
+IteratorHandlerTest::doIterate(spi::IteratorId id,
+ uint64_t maxByteSize,
+ size_t maxChunks,
+ bool allowEmptyResult)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ std::vector<Chunk> chunks;
+
+ while (true) {
+ std::vector<spi::DocEntry::LP> entries;
+
+ spi::IterateResult result(getPersistenceProvider().iterate(
+ id, maxByteSize, context));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
+ CPPUNIT_ASSERT(result.getEntries().size() > 0 || allowEmptyResult);
+
+ for (size_t i = 0; i < result.getEntries().size(); ++i) {
+ entries.push_back(result.getEntries()[i]);
+ }
+ chunks.push_back(Chunk());
+ chunks.back()._entries.swap(entries);
+ if (result.isCompleted()
+ || (maxChunks != 0 && chunks.size() >= maxChunks))
+ {
+ break;
+ }
+ }
+ return chunks;
+}
+
+namespace {
+
+size_t
+getDocCount(const std::vector<IteratorHandlerTest::Chunk>& chunks)
+{
+ size_t count = 0;
+ for (size_t i=0; i<chunks.size(); ++i) {
+ count += chunks[i]._entries.size();
+ }
+ return count;
+}
+
+size_t
+getRemoveEntryCount(const std::vector<spi::DocEntry::LP>& entries)
+{
+ size_t ret = 0;
+ for (size_t i = 0; i < entries.size(); ++i) {
+ if (entries[i]->isRemove()) {
+ ++ret;
+ }
+ }
+ return ret;
+}
+
+struct DocEntryIndirectTimestampComparator
+{
+ bool operator()(const spi::DocEntry::LP& e1,
+ const spi::DocEntry::LP& e2) const
+ {
+ return e1->getTimestamp() < e2->getTimestamp();
+ }
+};
+
+std::vector<spi::DocEntry::LP>
+getEntriesFromChunks(const std::vector<IteratorHandlerTest::Chunk>& chunks)
+{
+ std::vector<spi::DocEntry::LP> ret;
+ for (size_t chunk = 0; chunk < chunks.size(); ++chunk) {
+ for (size_t i = 0; i < chunks[chunk]._entries.size(); ++i) {
+ ret.push_back(chunks[chunk]._entries[i]);
+ }
+ }
+ std::sort(ret.begin(),
+ ret.end(),
+ DocEntryIndirectTimestampComparator());
+ return ret;
+}
+
+const vespalib::LazyFile&
+getFileHandle(const MemFile& mf1)
+{
+ return static_cast<const SimpleMemFileIOBuffer&>(
+ mf1.getMemFileIO()).getFileHandle();
+}
+
+const LoggingLazyFile&
+getLoggerFile(const MemFile& file)
+{
+ return dynamic_cast<const LoggingLazyFile&>(getFileHandle(file));
+}
+
+}
+
+void
+IteratorHandlerTest::verifyDocs(const std::vector<DocAndTimestamp>& wanted,
+ const std::vector<IteratorHandlerTest::Chunk>& chunks,
+ const std::set<vespalib::string>& removes) const
+{
+ std::vector<spi::DocEntry::LP> retrieved(
+ getEntriesFromChunks(chunks));
+ size_t removeCount = getRemoveEntryCount(retrieved);
+ // Ensure that we've got the correct number of puts and removes
+ CPPUNIT_ASSERT_EQUAL(removes.size(), removeCount);
+ CPPUNIT_ASSERT_EQUAL(wanted.size(), retrieved.size() - removeCount);
+
+ size_t wantedIdx = 0;
+ for (size_t i = 0; i < retrieved.size(); ++i) {
+ spi::DocEntry& entry(*retrieved[i]);
+ if (entry.getDocument() != 0) {
+ if (!(*wanted[wantedIdx].first == *entry.getDocument())) {
+ std::ostringstream ss;
+ ss << "Documents differ! Wanted:\n"
+ << wanted[wantedIdx].first->toString(true)
+ << "\n\nGot:\n"
+ << entry.getDocument()->toString(true);
+ CPPUNIT_FAIL(ss.str());
+ }
+ CPPUNIT_ASSERT_EQUAL(wanted[wantedIdx].second, entry.getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(wanted[wantedIdx].first->serialize()->getLength()
+ + sizeof(spi::DocEntry),
+ size_t(entry.getSize()));
+ ++wantedIdx;
+ } else {
+ // Remove-entry
+ CPPUNIT_ASSERT(entry.getDocumentId() != 0);
+ CPPUNIT_ASSERT_EQUAL(entry.getDocumentId()->getSerializedSize()
+ + sizeof(spi::DocEntry),
+ size_t(entry.getSize()));
+ if (removes.find(entry.getDocumentId()->toString()) == removes.end()) {
+ std::ostringstream ss;
+ ss << "Got unexpected remove entry for document id "
+ << *entry.getDocumentId();
+ CPPUNIT_FAIL(ss.str());
+ }
+ }
+ }
+}
+
+// Feed numDocs documents, starting from timestamp 1000
+std::vector<IteratorHandlerTest::DocAndTimestamp>
+IteratorHandlerTest::feedDocs(size_t numDocs,
+ uint32_t minSize,
+ uint32_t maxSize)
+{
+ std::vector<DocAndTimestamp> docs;
+ for (uint32_t i = 0; i < numDocs; ++i) {
+ docs.push_back(
+ DocAndTimestamp(
+ doPut(4,
+ framework::MicroSecTime(1000 + i),
+ minSize,
+ maxSize),
+ spi::Timestamp(1000 + i)));
+ }
+ flush(document::BucketId(16, 4));
+ return docs;
+}
+
+void
+IteratorHandlerTest::testSomeSlotsRemovedBetweenInvocations()
+{
+ std::vector<DocAndTimestamp> docs = feedDocs(100, 4096, 4096);
+
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ spi::Selection sel(createSelection("true"));
+
+ spi::CreateIteratorResult iter(create(b, sel));
+ CPPUNIT_ASSERT(env()._cache.contains(b.getBucketId()));
+
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 10000, 25);
+ CPPUNIT_ASSERT_EQUAL(size_t(25), chunks.size());
+
+ {
+ MemFilePtr file(getMemFile(b.getBucketId()));
+
+ for (int i = 0 ; i < 2; ++i) {
+ const MemSlot* slot = file->getSlotWithId(docs.front().first->getId());
+ CPPUNIT_ASSERT(slot != 0);
+ file->removeSlot(*slot);
+ docs.erase(docs.begin());
+ }
+ file->flushToDisk();
+ }
+
+ std::vector<Chunk> chunks2 = doIterate(iter.getIteratorId(), 10000);
+ CPPUNIT_ASSERT_EQUAL(size_t(24), chunks2.size());
+ std::copy(chunks2.begin(),
+ chunks2.end(),
+ std::back_insert_iterator<std::vector<Chunk> >(chunks));
+
+ verifyDocs(docs, chunks);
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+
+ // Bucket should not be evicted from cache during normal operation.
+ CPPUNIT_ASSERT(env()._cache.contains(b.getBucketId()));
+}
+
+void
+IteratorHandlerTest::testAllSlotsRemovedBetweenInvocations()
+{
+ std::vector<DocAndTimestamp> docs = feedDocs(100, 4096, 4096);
+
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ spi::Selection sel(createSelection("true"));
+
+ spi::CreateIteratorResult iter(create(b, sel));
+
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 1, 25);
+ CPPUNIT_ASSERT_EQUAL(size_t(25), chunks.size());
+
+ {
+ MemFilePtr file(getMemFile(b.getBucketId()));
+
+ for (int i = 0 ; i < 75; ++i) {
+ const MemSlot* slot = file->getSlotWithId(docs[i].first->getId());
+ CPPUNIT_ASSERT(slot != 0);
+ file->removeSlot(*slot);
+ }
+ file->flushToDisk();
+ docs.erase(docs.begin(), docs.begin() + 75);
+ }
+
+ std::vector<Chunk> chunks2 = doIterate(iter.getIteratorId(), 1, 0, true);
+ CPPUNIT_ASSERT_EQUAL(size_t(0), getDocCount(chunks2));
+ verifyDocs(docs, chunks);
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+}
+
+void
+IteratorHandlerTest::testIterateMetadataOnly()
+{
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ std::vector<DocAndTimestamp> docs = feedDocs(10);
+
+ CPPUNIT_ASSERT(
+ doUnrevertableRemove(b.getBucketId(),
+ docs[docs.size() - 2].first->getId(),
+ Timestamp(1008)));
+
+ CPPUNIT_ASSERT(
+ doRemove(b.getBucketId(),
+ docs[docs.size() - 1].first->getId(),
+ framework::MicroSecTime(3001),
+ OperationHandler::PERSIST_REMOVE_IF_FOUND));
+
+ flush(b.getBucketId());
+
+ spi::Selection sel(createSelection("true"));
+ spi::CreateIteratorResult iter(
+ create(b, sel, spi::NEWEST_DOCUMENT_OR_REMOVE, document::NoFields()));
+
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 4096);
+ std::vector<spi::DocEntry::LP> entries = getEntriesFromChunks(chunks);
+ CPPUNIT_ASSERT_EQUAL(docs.size(), entries.size());
+ std::vector<DocAndTimestamp>::const_iterator docIter(
+ docs.begin());
+ for (size_t i = 0; i < entries.size(); ++i, ++docIter) {
+ const spi::DocEntry& entry = *entries[i];
+
+ CPPUNIT_ASSERT(entry.getDocument() == 0);
+ CPPUNIT_ASSERT(entry.getDocumentId() == 0);
+ if (i == 9) {
+ CPPUNIT_ASSERT(entry.isRemove());
+ CPPUNIT_ASSERT_EQUAL(spi::Timestamp(3001), entry.getTimestamp());
+ } else if (i == 8) {
+ CPPUNIT_ASSERT(entry.isRemove());
+ CPPUNIT_ASSERT_EQUAL(spi::Timestamp(1008), entry.getTimestamp());
+ } else {
+ CPPUNIT_ASSERT(!entry.isRemove());
+ CPPUNIT_ASSERT_EQUAL(docIter->second, entry.getTimestamp());
+ }
+ }
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+}
+
+void
+IteratorHandlerTest::testIterateHeadersOnly()
+{
+ std::vector<DocAndTimestamp> docs = feedDocs(20);
+ // Remove all bodies.
+ for (size_t i = 0; i < docs.size(); ++i) {
+ clearBody(*docs[i].first);
+ }
+
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ spi::Selection sel(createSelection("true"));
+
+ spi::CreateIteratorResult iter(create(b, sel, spi::NEWEST_DOCUMENT_ONLY,
+ document::HeaderFields()));
+
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 1024);
+ verifyDocs(docs, chunks);
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+}
+
+void
+IteratorHandlerTest::testIterateLargeDocument()
+{
+ std::vector<DocAndTimestamp> docs = feedDocs(10, 10000, 10000);
+ std::vector<DocAndTimestamp> largedoc;
+ largedoc.push_back(docs.back());
+
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ spi::Selection sel(createSelection("true"));
+
+ spi::CreateIteratorResult iter(create(b, sel));
+
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 100, 1);
+ verifyDocs(largedoc, chunks);
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+}
+
+void
+IteratorHandlerTest::testDocumentsRemovedBetweenInvocations()
+{
+ int docCount = 100;
+ std::vector<DocAndTimestamp> docs = feedDocs(docCount);
+
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ spi::Selection sel(createSelection("true"));
+
+ spi::CreateIteratorResult iter(create(b, sel));
+
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 1, 25);
+ CPPUNIT_ASSERT_EQUAL(size_t(25), chunks.size());
+
+ // Remove a subset of the documents. We should still get all the
+ // original documents from the iterator, assuming no compactions.
+ std::vector<DocumentId> removedDocs;
+ std::vector<DocAndTimestamp> nonRemovedDocs;
+ for (int i = 0; i < docCount; ++i) {
+ if (i % 3 == 0) {
+ removedDocs.push_back(docs[i].first->getId());
+ CPPUNIT_ASSERT(doRemove(b.getBucketId(),
+ removedDocs.back(),
+ framework::MicroSecTime(2000 + i),
+ OperationHandler::PERSIST_REMOVE_IF_FOUND));
+ } else {
+ nonRemovedDocs.push_back(docs[i]);
+ }
+ }
+ flush(b.getBucketId());
+
+ std::vector<Chunk> chunks2 = doIterate(iter.getIteratorId(), 1);
+ CPPUNIT_ASSERT_EQUAL(size_t(75), chunks2.size());
+ std::copy(chunks2.begin(),
+ chunks2.end(),
+ std::back_insert_iterator<std::vector<Chunk> >(chunks));
+
+ verifyDocs(docs, chunks);
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+}
+
+void
+IteratorHandlerTest::doTestUnrevertableRemoveBetweenInvocations(bool includeRemoves)
+{
+ int docCount = 100;
+ std::vector<DocAndTimestamp> docs = feedDocs(docCount);
+
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ spi::Selection sel(createSelection("true"));
+ spi::CreateIteratorResult iter(
+ create(b, sel,
+ includeRemoves ?
+ spi::NEWEST_DOCUMENT_OR_REMOVE : spi::NEWEST_DOCUMENT_ONLY));
+
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 1, 25);
+ CPPUNIT_ASSERT_EQUAL(size_t(25), chunks.size());
+
+ // Remove a subset of the documents unrevertably.
+ std::vector<DocumentId> removedDocs;
+ std::vector<DocAndTimestamp> nonRemovedDocs;
+ for (int i = 0; i < docCount - 25; ++i) {
+ if (i < 10) {
+ removedDocs.push_back(docs[i].first->getId());
+ CPPUNIT_ASSERT(
+ doUnrevertableRemove(b.getBucketId(),
+ removedDocs.back(),
+ Timestamp(1000+i)));
+ } else {
+ nonRemovedDocs.push_back(docs[i]);
+ }
+ }
+ flush(b.getBucketId());
+
+ std::vector<Chunk> chunks2 = doIterate(iter.getIteratorId(), 1);
+ std::vector<spi::DocEntry::LP> entries = getEntriesFromChunks(chunks2);
+ if (!includeRemoves) {
+ CPPUNIT_ASSERT_EQUAL(nonRemovedDocs.size(), chunks2.size());
+ verifyDocs(nonRemovedDocs, chunks2);
+ } else {
+ CPPUNIT_ASSERT_EQUAL(size_t(75), entries.size());
+ for (int i = 0; i < docCount - 25; ++i) {
+ spi::DocEntry& entry(*entries[i]);
+ if (i < 10) {
+ CPPUNIT_ASSERT(entry.isRemove());
+ } else {
+ CPPUNIT_ASSERT(!entry.isRemove());
+ }
+ }
+ }
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+}
+
+void
+IteratorHandlerTest::testUnrevertableRemoveBetweenInvocations()
+{
+ doTestUnrevertableRemoveBetweenInvocations(false);
+}
+
+void
+IteratorHandlerTest::testUnrevertableRemoveBetweenInvocationsIncludeRemoves()
+{
+ doTestUnrevertableRemoveBetweenInvocations(true);
+}
+
+void
+IteratorHandlerTest::testMatchTimestampRangeDocAltered()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucketId(16, 4);
+ document::StringFieldValue updateValue1("update1");
+ document::StringFieldValue updateValue2("update2");
+
+ Document::SP originalDoc = doPut(4, Timestamp(1234));
+
+ {
+ document::DocumentUpdate::SP update = createBodyUpdate(
+ originalDoc->getId(), updateValue1);
+
+ spi::UpdateResult result = doUpdate(bucketId, update, Timestamp(2345));
+ CPPUNIT_ASSERT_EQUAL(1234, (int)result.getExistingTimestamp());
+ }
+
+ {
+ document::DocumentUpdate::SP update = createBodyUpdate(
+ originalDoc->getId(), updateValue2);
+
+ spi::UpdateResult result = doUpdate(bucketId, update, Timestamp(3456));
+ CPPUNIT_ASSERT_EQUAL(2345, (int)result.getExistingTimestamp());
+ }
+
+ CPPUNIT_ASSERT(
+ doRemove(bucketId,
+ originalDoc->getId(),
+ Timestamp(4567),
+ OperationHandler::PERSIST_REMOVE_IF_FOUND));
+ flush(bucketId);
+
+ spi::Bucket b(bucketId, spi::PartitionId(0));
+
+ {
+ spi::Selection sel(createSelection("true"));
+ sel.setFromTimestamp(spi::Timestamp(0));
+ sel.setToTimestamp(spi::Timestamp(10));
+ spi::CreateIteratorResult iter(create(b, sel));
+
+ spi::IterateResult result(getPersistenceProvider().iterate(
+ iter.getIteratorId(), 4096, context));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
+ CPPUNIT_ASSERT_EQUAL(size_t(0), result.getEntries().size());
+ CPPUNIT_ASSERT(result.isCompleted());
+
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+ }
+
+ {
+ spi::Selection sel(createSelection("true"));
+ sel.setFromTimestamp(spi::Timestamp(10000));
+ sel.setToTimestamp(spi::Timestamp(20000));
+ spi::CreateIteratorResult iter(create(b, sel));
+
+ spi::IterateResult result(getPersistenceProvider().iterate(
+ iter.getIteratorId(), 4096, context));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
+ CPPUNIT_ASSERT_EQUAL(size_t(0), result.getEntries().size());
+ CPPUNIT_ASSERT(result.isCompleted());
+
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+ }
+
+ {
+ spi::Selection sel(createSelection("true"));
+ sel.setFromTimestamp(spi::Timestamp(0));
+ sel.setToTimestamp(spi::Timestamp(1234));
+ spi::CreateIteratorResult iter(create(b, sel));
+
+ spi::IterateResult result(getPersistenceProvider().iterate(
+ iter.getIteratorId(), 4096, context));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
+ CPPUNIT_ASSERT_EQUAL(size_t(1), result.getEntries().size());
+ CPPUNIT_ASSERT(result.isCompleted());
+
+ const Document& receivedDoc(*result.getEntries()[0]->getDocument());
+ if (!(*originalDoc == receivedDoc)) {
+ std::ostringstream ss;
+ ss << "Documents differ! Wanted:\n"
+ << originalDoc->toString(true)
+ << "\n\nGot:\n"
+ << receivedDoc.toString(true);
+ CPPUNIT_FAIL(ss.str());
+ }
+
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+ }
+
+ {
+ spi::Selection sel(createSelection("true"));
+ sel.setFromTimestamp(spi::Timestamp(0));
+ sel.setToTimestamp(spi::Timestamp(2345));
+ spi::CreateIteratorResult iter(create(b, sel));
+
+ spi::IterateResult result(getPersistenceProvider().iterate(
+ iter.getIteratorId(), 4096, context));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
+ CPPUNIT_ASSERT_EQUAL(size_t(1), result.getEntries().size());
+ CPPUNIT_ASSERT(result.isCompleted());
+
+ const Document& receivedDoc(*result.getEntries()[0]->getDocument());
+ CPPUNIT_ASSERT(receivedDoc.getValue("content").get());
+ CPPUNIT_ASSERT_EQUAL(updateValue1,
+ dynamic_cast<document::StringFieldValue&>(
+ *receivedDoc.getValue(
+ "content")));
+
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+ }
+
+ {
+ spi::Selection sel(createSelection("true"));
+ sel.setFromTimestamp(spi::Timestamp(0));
+ sel.setToTimestamp(spi::Timestamp(3456));
+ spi::CreateIteratorResult iter(create(b, sel));
+
+ spi::IterateResult result(getPersistenceProvider().iterate(
+ iter.getIteratorId(), 4096, context));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
+ CPPUNIT_ASSERT_EQUAL(size_t(1), result.getEntries().size());
+ CPPUNIT_ASSERT(result.isCompleted());
+
+ const Document& receivedDoc(*result.getEntries()[0]->getDocument());
+ CPPUNIT_ASSERT(receivedDoc.getValue("content").get());
+ CPPUNIT_ASSERT_EQUAL(updateValue2,
+ dynamic_cast<document::StringFieldValue&>(
+ *receivedDoc.getValue(
+ "content")));
+
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+ }
+}
+
+void
+IteratorHandlerTest::testIterateAllVersions()
+{
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ std::vector<DocAndTimestamp> docs;
+
+ Document::SP originalDoc(createRandomDocumentAtLocation(
+ 4, 1001, 110, 110));
+
+ doPut(originalDoc, framework::MicroSecTime(1001), 0);
+
+ document::StringFieldValue updateValue1("update1");
+ {
+ document::DocumentUpdate::SP update = createBodyUpdate(
+ originalDoc->getId(), updateValue1);
+
+ spi::UpdateResult result = doUpdate(b.getBucketId(), update, Timestamp(2345));
+ CPPUNIT_ASSERT_EQUAL(1001, (int)result.getExistingTimestamp());
+ }
+ flush(b.getBucketId());
+
+ Document::SP updatedDoc(new Document(*originalDoc));
+ updatedDoc->setValue("content", document::StringFieldValue("update1"));
+ docs.push_back(DocAndTimestamp(originalDoc, spi::Timestamp(1001)));
+ docs.push_back(DocAndTimestamp(updatedDoc, spi::Timestamp(2345)));
+
+ spi::Selection sel(createSelection("true"));
+ spi::CreateIteratorResult iter(create(b, sel, spi::ALL_VERSIONS));
+
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 4096);
+ verifyDocs(docs, chunks);
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+}
+
+void
+IteratorHandlerTest::testFieldSetFiltering()
+{
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ Document::SP doc(createRandomDocumentAtLocation(
+ 4, 1001, 110, 110));
+ doc->setValue(doc->getField("headerval"), document::IntFieldValue(42));
+ doc->setValue(doc->getField("hstringval"),
+ document::StringFieldValue("groovy, baby!"));
+ doc->setValue(doc->getField("content"),
+ document::StringFieldValue("fancy content"));
+ doPut(doc, framework::MicroSecTime(1001), 0);
+ flush(b.getBucketId());
+
+ document::FieldSetRepo repo;
+ spi::Selection sel(createSelection("true"));
+ spi::CreateIteratorResult iter(
+ create(b, sel, spi::NEWEST_DOCUMENT_ONLY,
+ *repo.parse(*getTypeRepo(), "testdoctype1:hstringval,content")));
+ std::vector<spi::DocEntry::LP> entries(
+ getEntriesFromChunks(doIterate(iter.getIteratorId(), 4096)));
+ CPPUNIT_ASSERT_EQUAL(size_t(1), entries.size());
+ CPPUNIT_ASSERT_EQUAL(std::string("content: fancy content\n"
+ "hstringval: groovy, baby!\n"),
+ stringifyFields(*entries[0]->getDocument()));
+}
+
+void
+IteratorHandlerTest::testIteratorInactiveOnException()
+{
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ feedDocs(10);
+
+ env()._cache.clear();
+
+ simulateIoErrorsForSubsequentlyOpenedFiles(IoErrors().afterReads(1));
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ spi::CreateIteratorResult iter(create(b, createSelection("true")));
+ spi::IterateResult result(getPersistenceProvider().iterate(
+ iter.getIteratorId(), 100000, context));
+ CPPUNIT_ASSERT(result.hasError());
+ // Check that iterator is marked as inactive
+ const SharedIteratorHandlerState& state(
+ getPersistenceProvider().getIteratorHandler().getState());
+ CPPUNIT_ASSERT(state._iterators.find(iter.getIteratorId().getValue())
+ != state._iterators.end());
+ CPPUNIT_ASSERT(state._iterators.find(iter.getIteratorId().getValue())
+ ->second.isActive() == false);
+
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+}
+
+void
+IteratorHandlerTest::testDocsCachedBeforeDocumentSelection()
+{
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ std::vector<DocAndTimestamp> docs = feedDocs(100, 4096, 4096);
+
+ env()._cache.clear();
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options).maximumReadThroughGap(1024*1024).build());
+ env()._lazyFileFactory = std::unique_ptr<Environment::LazyFileFactory>(
+ new LoggingLazyFile::Factory());
+
+ spi::Selection sel(createSelection("id.user=4"));
+ spi::CreateIteratorResult iter(create(b, sel, spi::NEWEST_DOCUMENT_ONLY,
+ document::BodyFields()));
+
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 4096);
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+ {
+ MemFilePtr file(getMemFile(b.getBucketId()));
+ // Should have 3 read ops; metadata, (precached) headers and bodies
+ CPPUNIT_ASSERT_EQUAL(size_t(3),
+ getLoggerFile(*file).operations.size());
+ }
+}
+
+void
+IteratorHandlerTest::testTimestampRangeLimitedPrefetch()
+{
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ // Feed docs with timestamp range [1000, 1100)
+ feedDocs(100, 4096, 4096);
+
+ env()._cache.clear();
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options).maximumReadThroughGap(512).build());
+ env()._lazyFileFactory = std::unique_ptr<Environment::LazyFileFactory>(
+ new LoggingLazyFile::Factory());
+
+ spi::Selection sel(createSelection("id.user=4"));
+ sel.setFromTimestamp(spi::Timestamp(1050));
+ sel.setToTimestamp(spi::Timestamp(1059));
+ spi::CreateIteratorResult iter(create(b, sel, spi::NEWEST_DOCUMENT_ONLY,
+ document::BodyFields()));
+ std::vector<Chunk> chunks = doIterate(iter.getIteratorId(), 4096);
+ CPPUNIT_ASSERT_EQUAL(size_t(10), getDocCount(chunks));
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().destroyIterator(iter.getIteratorId(), context);
+ // Iterate over all slots, ensuring that only those that fall within the
+ // timestamp range have actually been cached.
+ {
+ MemFilePtr file(getMemFile(b.getBucketId()));
+ // Should have 3 read ops; metadata, (precached) headers and bodies
+ CPPUNIT_ASSERT_EQUAL(size_t(3),
+ getLoggerFile(*file).operations.size());
+ for (size_t i = 0; i < file->getSlotCount(); ++i) {
+ const MemSlot& slot((*file)[i]);
+ if (slot.getTimestamp() >= Timestamp(1050)
+ && slot.getTimestamp() <= Timestamp(1059))
+ {
+ CPPUNIT_ASSERT(file->partAvailable(slot, HEADER));
+ CPPUNIT_ASSERT(file->partAvailable(slot, BODY));
+ } else {
+ CPPUNIT_ASSERT(!file->partAvailable(slot, HEADER));
+ CPPUNIT_ASSERT(!file->partAvailable(slot, BODY));
+ }
+ }
+ }
+}
+
+void
+IteratorHandlerTest::testCachePrefetchRequirements()
+{
+ document::select::Parser parser(
+ env().repo(), env()._bucketFactory);
+ {
+ // No prefetch required.
+ // NOTE: since stuff like id.user=1234 won't work, we have to handle
+ // that explicitly in createIterator based on the assumption that a
+ // non-empty document selection at _least_ requires header to be read.
+ std::unique_ptr<document::select::Node> sel(
+ parser.parse("true"));
+ CachePrefetchRequirements req(
+ CachePrefetchRequirements::createFromSelection(env().repo(),
+ *sel));
+ CPPUNIT_ASSERT(!req.isHeaderPrefetchRequired());
+ CPPUNIT_ASSERT(!req.isBodyPrefetchRequired());
+ }
+
+ {
+ // Header prefetch required.
+ std::unique_ptr<document::select::Node> sel(
+ parser.parse("testdoctype1.hstringval='blarg'"));
+ CachePrefetchRequirements req(
+ CachePrefetchRequirements::createFromSelection(env().repo(),
+ *sel));
+ CPPUNIT_ASSERT(req.isHeaderPrefetchRequired());
+ CPPUNIT_ASSERT(!req.isBodyPrefetchRequired());
+ }
+
+ {
+ // Body prefetch required.
+ std::unique_ptr<document::select::Node> sel(
+ parser.parse("testdoctype1.content='foobar'"));
+ CachePrefetchRequirements req(
+ CachePrefetchRequirements::createFromSelection(env().repo(),
+ *sel));
+ CPPUNIT_ASSERT(!req.isHeaderPrefetchRequired());
+ CPPUNIT_ASSERT(req.isBodyPrefetchRequired());
+ }
+}
+
+void
+IteratorHandlerTest::testBucketEvictedFromCacheOnIterateException()
+{
+ spi::Bucket b(BucketId(16, 4), spi::PartitionId(0));
+ feedDocs(10);
+ env()._cache.clear();
+
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ spi::CreateIteratorResult iter(create(b, createSelection("true")));
+ simulateIoErrorsForSubsequentlyOpenedFiles(IoErrors().afterReads(1));
+ spi::IterateResult result(getPersistenceProvider().iterate(
+ iter.getIteratorId(), 100000, context));
+ CPPUNIT_ASSERT(result.hasError());
+
+ // This test is actually a bit disingenuous since calling iterate will
+ // implicitly invoke maintain() on an IO exception, which will subsequently
+ // evict the bucket due to the exception happening again in its context.
+ CPPUNIT_ASSERT(!env()._cache.contains(b.getBucketId()));
+}
+
+}
+}
diff --git a/memfilepersistence/src/tests/spi/joinoperationhandlertest.cpp b/memfilepersistence/src/tests/spi/joinoperationhandlertest.cpp
new file mode 100644
index 00000000000..78601b461ab
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/joinoperationhandlertest.cpp
@@ -0,0 +1,504 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+
+#include <vespa/document/datatype/documenttype.h>
+#include <tests/spi/memfiletestutils.h>
+#include <tests/spi/simulatedfailurefile.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+
+using document::DocumentType;
+
+namespace storage {
+namespace memfile {
+namespace {
+ spi::LoadType defaultLoadType(0, "default");
+}
+
+class JoinOperationHandlerTest : public MemFileTestUtils
+{
+ CPPUNIT_TEST_SUITE(JoinOperationHandlerTest);
+ CPPUNIT_TEST(testSimple);
+ CPPUNIT_TEST(testTargetExists);
+ CPPUNIT_TEST(testTargetWithOverlap);
+ CPPUNIT_TEST(testMultiDisk);
+ CPPUNIT_TEST(testMultiDiskFlushed);
+ CPPUNIT_TEST(testInternalJoin);
+ CPPUNIT_TEST(testInternalJoinDiskFull);
+ CPPUNIT_TEST(testTargetIoWriteExceptionEvictsTargetFromCache);
+ CPPUNIT_TEST(test1stSourceIoReadExceptionEvictsSourceFromCache);
+ CPPUNIT_TEST(test2ndSourceExceptionEvictsExistingTargetFromCache);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void testSimple();
+ void testTargetExists();
+ void testTargetWithOverlap();
+ void testMultiDisk();
+ void testMultiDiskFlushed();
+ void testInternalJoin();
+ void testInternalJoinDiskFull();
+ void testTargetIoWriteExceptionEvictsTargetFromCache();
+ void test1stSourceIoReadExceptionEvictsSourceFromCache();
+ void test2ndSourceExceptionEvictsExistingTargetFromCache();
+
+ void insertDocumentInBucket(uint64_t location,
+ Timestamp timestamp,
+ document::BucketId bucket);
+
+private:
+ void feedSingleDisk();
+ void feedMultiDisk();
+ std::string getStandardMemFileStatus(uint32_t disk = 0);
+
+ spi::Result doJoin(const document::BucketId to,
+ const document::BucketId from1,
+ const document::BucketId from2);
+};
+
+namespace {
+
+document::BucketId TARGET = document::BucketId(15, 4);
+document::BucketId SOURCE1 = document::BucketId(16, 4);
+document::BucketId SOURCE2 = document::BucketId(16, (uint64_t)4 | ((uint64_t)1 << 15));
+}
+
+CPPUNIT_TEST_SUITE_REGISTRATION(JoinOperationHandlerTest);
+
+void
+JoinOperationHandlerTest::feedSingleDisk()
+{
+ for (uint32_t i = 0; i < 100; i++) {
+ std::ostringstream ost;
+ ost << "userdoc:storage_test:1234:" << i;
+ const DocumentType& type(
+ *getTypeRepo()->getDocumentType("testdoctype1"));
+ document::Document::SP doc(
+ new document::Document(type, document::DocumentId(ost.str())));
+
+ document::BucketId bucket(
+ getBucketIdFactory().getBucketId(doc->getId()));
+ bucket.setUsedBits(33);
+ doPut(doc, Timestamp(1000 + i), 0, 33);
+ flush(bucket);
+ }
+}
+
+void
+JoinOperationHandlerTest::feedMultiDisk()
+{
+ for (uint32_t i = 0; i < 100; i += 2) {
+ doPutOnDisk(7, 4 | (1 << 15), Timestamp(1000 + i));
+ }
+ flush(SOURCE2);
+
+ for (uint32_t i = 1; i < 100; i += 2) {
+ doPutOnDisk(4, 4, Timestamp(1000 + i));
+ }
+ flush(SOURCE1);
+
+ {
+ MemFilePtr file(getMemFile(SOURCE1, 4));
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(4, (int)file->getDisk());
+ }
+
+ {
+ MemFilePtr file(getMemFile(SOURCE2, 7));
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(7, (int)file->getDisk());
+ }
+}
+
+std::string
+JoinOperationHandlerTest::getStandardMemFileStatus(uint32_t disk)
+{
+ std::ostringstream ost;
+
+ ost << getMemFileStatus(TARGET, disk) << "\n"
+ << getMemFileStatus(SOURCE1, disk ) << "\n"
+ << getMemFileStatus(SOURCE2, disk) << "\n";
+
+ return ost.str();
+}
+
+void
+JoinOperationHandlerTest::insertDocumentInBucket(
+ uint64_t location,
+ Timestamp timestamp,
+ document::BucketId bucket)
+{
+ Document::SP doc(
+ createRandomDocumentAtLocation(
+ location, timestamp.getTime(), 100, 100));
+ doPut(doc, bucket, timestamp);
+}
+
+spi::Result
+JoinOperationHandlerTest::doJoin(const document::BucketId to,
+ const document::BucketId from1,
+ const document::BucketId from2)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ return getPersistenceProvider().join(
+ spi::Bucket(from1, spi::PartitionId(0)),
+ spi::Bucket(from2, spi::PartitionId(0)),
+ spi::Bucket(to, spi::PartitionId(0)),
+ context);
+}
+
+void
+JoinOperationHandlerTest::testSimple()
+{
+ setupDisks(1);
+ feedSingleDisk();
+
+ {
+ MemFilePtr file(getMemFile(document::BucketId(33, 1234)));
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ }
+
+ {
+ MemFilePtr file(getMemFile(document::BucketId(33, (uint64_t)1234 | ((uint64_t)1 << 32))));
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ }
+
+ spi::Result result =
+ doJoin(document::BucketId(32, 1234),
+ document::BucketId(33, 1234),
+ document::BucketId(33, (uint64_t)1234 | ((uint64_t)1 << 32)));
+
+ {
+ MemFilePtr file(getMemFile(document::BucketId(32, (uint64_t)1234)));
+ CPPUNIT_ASSERT_EQUAL(100, (int)file->getSlotCount());
+ CPPUNIT_ASSERT(!file->slotsAltered());
+ }
+}
+
+void
+JoinOperationHandlerTest::testTargetExists()
+{
+ setupDisks(1);
+
+ for (uint32_t i = 0; i < 100; i += 2) {
+ doPut(4 | (1 << 15), Timestamp(1000 + i));
+ }
+ flush(SOURCE2);
+
+ for (uint32_t i = 1; i < 100; i += 2) {
+ doPut(4, Timestamp(1000 + i));
+ }
+ flush(SOURCE1);
+
+ for (uint32_t i = 0; i < 100; i++) {
+ uint32_t location = 4;
+ if (i % 2 == 0) {
+ location |= (1 << 15);
+ }
+
+ insertDocumentInBucket(location, Timestamp(500 + i), TARGET);
+ }
+ flush(TARGET);
+
+ doJoin(TARGET, SOURCE1, SOURCE2);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x3c00000000000004): 200,0\n"
+ "BucketId(0x4000000000000004): 0,0\n"
+ "BucketId(0x4000000000008004): 0,0\n"),
+ getStandardMemFileStatus());
+}
+
+void
+JoinOperationHandlerTest::testTargetWithOverlap()
+{
+ setupDisks(1);
+
+ for (uint32_t i = 0; i < 100; i += 2) {
+ doPut(4 | (1 << 15), Timestamp(1000 + i));
+ }
+ flush(SOURCE2);
+
+ for (uint32_t i = 1; i < 100; i += 2) {
+ doPut(4, Timestamp(1000 + i));
+ }
+ flush(SOURCE1);
+
+ for (uint32_t i = 0; i < 100; i++) {
+ uint32_t location = 4;
+ if (i % 2 == 0) {
+ location |= (1 << 15);
+ }
+
+ insertDocumentInBucket(location, Timestamp(950 + i), TARGET);
+ }
+ flush(TARGET);
+
+ doJoin(TARGET, SOURCE1, SOURCE2);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x3c00000000000004): 150,0\n"
+ "BucketId(0x4000000000000004): 0,0\n"
+ "BucketId(0x4000000000008004): 0,0\n"),
+ getStandardMemFileStatus());
+}
+
+void
+JoinOperationHandlerTest::testMultiDisk()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ setupDisks(10);
+ feedMultiDisk();
+
+ getPersistenceProvider().join(spi::Bucket(SOURCE2, spi::PartitionId(7)),
+ spi::Bucket(SOURCE1, spi::PartitionId(4)),
+ spi::Bucket(TARGET, spi::PartitionId(3)),
+ context);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x3c00000000000004): 100,3\n"
+ "BucketId(0x4000000000000004): 0,0\n"
+ "BucketId(0x4000000000008004): 0,0\n"),
+ getStandardMemFileStatus());
+}
+
+void
+JoinOperationHandlerTest::testMultiDiskFlushed()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ setupDisks(10);
+ feedMultiDisk();
+
+ // Flush everything to disk, to check that we can join even
+ // if it's not in cache before.
+ env()._cache.flushDirtyEntries();
+ env()._cache.clear();
+
+ getPersistenceProvider().join(spi::Bucket(SOURCE2, spi::PartitionId(7)),
+ spi::Bucket(SOURCE1, spi::PartitionId(4)),
+ spi::Bucket(TARGET, spi::PartitionId(3)),
+ context);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x3c00000000000004): 100,3\n"
+ "BucketId(0x4000000000000004): 0,3\n"
+ "BucketId(0x4000000000008004): 0,3\n"),
+ getStandardMemFileStatus(3));
+}
+
+void
+JoinOperationHandlerTest::testInternalJoin()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ setupDisks(10);
+
+ for (uint32_t i = 4; i < 6; i++) {
+ for (uint32_t j = 0; j < 10; j++) {
+ uint32_t location = 4;
+ doPutOnDisk(i, location, Timestamp(i * 1000 + j));
+ }
+ flush(document::BucketId(16, 4), i);
+ env()._cache.clear();
+ }
+
+ std::string fileName1 =
+ env().calculatePathInDir(SOURCE1, (*env()._mountPoints)[4]);
+ std::string fileName2 =
+ env().calculatePathInDir(SOURCE1, (*env()._mountPoints)[5]);
+
+ CPPUNIT_ASSERT(vespalib::stat(fileName1).get());
+ vespalib::FileInfo::UP file2(vespalib::stat(fileName2));
+
+ CPPUNIT_ASSERT(file2.get());
+ CPPUNIT_ASSERT(file2->_size > 0);
+
+ PartitionMonitor* mon = env().getDirectory(5).getPartition().getMonitor();
+ // Set disk under 80% full. Over 80%, we shouldn't move buckets to the target.
+ mon->setStatOncePolicy();
+ mon->overrideRealStat(512, 100000, 50000);
+ CPPUNIT_ASSERT(!mon->isFull(0, .80f));
+
+ getPersistenceProvider().join(spi::Bucket(SOURCE1, spi::PartitionId(4)),
+ spi::Bucket(SOURCE1, spi::PartitionId(4)),
+ spi::Bucket(SOURCE1, spi::PartitionId(5)),
+ context);
+
+ env()._cache.clear();
+
+ CPPUNIT_ASSERT(!vespalib::stat(fileName1).get());
+ CPPUNIT_ASSERT(vespalib::stat(fileName2).get());
+}
+
+void
+JoinOperationHandlerTest::testInternalJoinDiskFull()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ setupDisks(10);
+
+ for (uint32_t i = 4; i < 6; i++) {
+ for (uint32_t j = 0; j < 10; j++) {
+ uint32_t location = 4;
+ doPutOnDisk(i, location, Timestamp(i * 1000 + j));
+ }
+ flush(document::BucketId(16, 4), i);
+ env()._cache.clear();
+ }
+
+ std::string fileName1 =
+ env().calculatePathInDir(SOURCE1, (*env()._mountPoints)[4]);
+ std::string fileName2 =
+ env().calculatePathInDir(SOURCE1, (*env()._mountPoints)[5]);
+
+ CPPUNIT_ASSERT(vespalib::stat(fileName1).get());
+ vespalib::FileInfo::UP file2(vespalib::stat(fileName2));
+
+ CPPUNIT_ASSERT(file2.get());
+ CPPUNIT_ASSERT(file2->_size > 0);
+
+ PartitionMonitor* mon = env().getDirectory(5).getPartition().getMonitor();
+ // Set disk to 81% full. Over 80%, we shouldn't move buckets to the target.
+ mon->setStatOncePolicy();
+ mon->overrideRealStat(512, 100000, 81000);
+ CPPUNIT_ASSERT(!mon->isFull());
+ CPPUNIT_ASSERT(mon->isFull(0, .08f));
+
+ spi::Result result =
+ getPersistenceProvider().join(spi::Bucket(SOURCE1, spi::PartitionId(4)),
+ spi::Bucket(SOURCE1, spi::PartitionId(4)),
+ spi::Bucket(SOURCE1, spi::PartitionId(5)),
+ context);
+
+ CPPUNIT_ASSERT(result.hasError());
+}
+
+void
+JoinOperationHandlerTest::testTargetIoWriteExceptionEvictsTargetFromCache()
+{
+ setupDisks(1);
+ feedSingleDisk();
+
+ document::BucketId src1(33, 1234);
+ document::BucketId src2(33, 1234ULL | (1ULL << 32));
+ document::BucketId target(32, 1234);
+
+ CPPUNIT_ASSERT(env()._cache.contains(src1));
+ CPPUNIT_ASSERT(env()._cache.contains(src2));
+ CPPUNIT_ASSERT(!env()._cache.contains(target));
+
+ // Reading existing (fully cached) files will go fine, but writing
+ // new file will not.
+ simulateIoErrorsForSubsequentlyOpenedFiles();
+
+ spi::Result result = doJoin(target, src1, src2);
+ CPPUNIT_ASSERT(result.hasError());
+ CPPUNIT_ASSERT(result.getErrorMessage().find("A simulated I/O write")
+ != vespalib::string::npos);
+
+ CPPUNIT_ASSERT(!env()._cache.contains(target));
+ // NOTE: since we end up renaming src1 -> target during the first
+ // iteration of join, src1 will actually be empty. This should not
+ // matter since the service layer will query the bucket info for
+ // all these afterwards and will thus pick up on this automatically.
+ unSimulateIoErrorsForSubsequentlyOpenedFiles();
+ {
+ MemFilePtr file(getMemFile(src1));
+ CPPUNIT_ASSERT_EQUAL(0, (int)file->getSlotCount());
+ CPPUNIT_ASSERT(!file->slotsAltered());
+ }
+ {
+ MemFilePtr file(getMemFile(src2));
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ CPPUNIT_ASSERT(!file->slotsAltered());
+ }
+ {
+ MemFilePtr file(getMemFile(target));
+ // Renamed from src1
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ CPPUNIT_ASSERT(!file->slotsAltered());
+ }
+}
+
+void
+JoinOperationHandlerTest::test1stSourceIoReadExceptionEvictsSourceFromCache()
+{
+ setupDisks(1);
+ feedSingleDisk();
+
+ document::BucketId src1(33, 1234);
+ document::BucketId src2(33, 1234ULL | (1ULL << 32));
+ document::BucketId target(32, 1234);
+
+ env()._cache.clear();
+ // Allow for reading in initial metadata so that loadFile itself doesn't
+ // fail. This could otherwise cause a false negative since that happens
+ // during initial cache lookup on a cache miss, at which point any
+ // exception will always stop a file from being added to the cache. Here
+ // we want to test the case where a file has been successfully hoisted
+ // out of the cache initially.
+ simulateIoErrorsForSubsequentlyOpenedFiles(IoErrors().afterReads(1));
+
+ spi::Result result = doJoin(target, src1, src2);
+ CPPUNIT_ASSERT(result.hasError());
+ CPPUNIT_ASSERT(result.getErrorMessage().find("A simulated I/O read")
+ != vespalib::string::npos);
+
+ CPPUNIT_ASSERT(!env()._cache.contains(src1));
+ CPPUNIT_ASSERT(!env()._cache.contains(src2));
+ CPPUNIT_ASSERT(!env()._cache.contains(target));
+}
+
+/**
+ * It must be exception safe for any source bucket to throw an exception during
+ * processing. Otherwise the node will core due to cache sanity checks.
+ *
+ * See VESPA-674 for context. In this scenario, it was not possible to write
+ * to the target file when attempting to join in the 2nd source bucket due to
+ * the disk fill ratio exceeding configured limits.
+ */
+void
+JoinOperationHandlerTest::test2ndSourceExceptionEvictsExistingTargetFromCache()
+{
+ setupDisks(1);
+ feedSingleDisk();
+
+ constexpr uint64_t location = 1234;
+
+ document::BucketId src1(33, location);
+ document::BucketId src2(33, location | (1ULL << 32));
+ document::BucketId target(32, location);
+
+ // Ensure target file is _not_ empty so that copySlots is triggered for
+ // each source bucket (rather than just renaming the file, which does not
+ // invoke the file read/write paths).
+ insertDocumentInBucket(location, Timestamp(100000), target);
+ flush(target);
+
+ env()._cache.clear();
+ // File rewrites are buffered before ever reaching the failure simulation
+ // layer, so only 1 actual write is used to flush the target file after
+ // the first source file has been processed. Attempting to flush the writes
+ // for the second source file should fail with an exception.
+ simulateIoErrorsForSubsequentlyOpenedFiles(
+ IoErrors().afterReads(INT_MAX).afterWrites(1));
+
+ spi::Result result = doJoin(target, src1, src2);
+ CPPUNIT_ASSERT(result.hasError());
+ CPPUNIT_ASSERT(result.getErrorMessage().find("A simulated I/O write")
+ != vespalib::string::npos);
+
+ CPPUNIT_ASSERT(!env()._cache.contains(src1));
+ CPPUNIT_ASSERT(!env()._cache.contains(src2));
+ CPPUNIT_ASSERT(!env()._cache.contains(target));
+}
+
+}
+
+}
diff --git a/memfilepersistence/src/tests/spi/logginglazyfile.h b/memfilepersistence/src/tests/spi/logginglazyfile.h
new file mode 100644
index 00000000000..e54753f7c3e
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/logginglazyfile.h
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include <vespa/vespalib/io/fileutil.h>
+#include <iostream>
+
+namespace storage {
+
+namespace memfile {
+
+class LoggingLazyFile : public vespalib::LazyFile {
+public:
+ class Factory : public Environment::LazyFileFactory {
+ public:
+ vespalib::LazyFile::UP createFile(const std::string& fileName) const {
+ return vespalib::LazyFile::UP(
+ new LoggingLazyFile(fileName, vespalib::File::DIRECTIO));
+ }
+ };
+
+ enum OpType {
+ READ = 0,
+ WRITE
+ };
+
+ struct Entry {
+ OpType opType;
+ size_t bufsize;
+ off_t offset;
+
+ std::string toString() const {
+ std::ostringstream ost;
+ ost << (opType == READ ? "Reading " : "Writing ")
+ << bufsize
+ << " bytes at "
+ << offset;
+ return ost.str();
+ }
+ };
+
+ mutable std::vector<Entry> operations;
+
+ LoggingLazyFile(const std::string& filename, int flags)
+ : LazyFile(filename, flags) {};
+
+ size_t getOperationCount() const {
+ return operations.size();
+ }
+
+ virtual off_t write(const void *buf, size_t bufsize, off_t offset) {
+ Entry e;
+ e.opType = WRITE;
+ e.bufsize = bufsize;
+ e.offset = offset;
+
+ operations.push_back(e);
+
+ return vespalib::LazyFile::write(buf, bufsize, offset);
+ }
+
+ virtual size_t read(void *buf, size_t bufsize, off_t offset) const {
+ Entry e;
+ e.opType = READ;
+ e.bufsize = bufsize;
+ e.offset = offset;
+
+ operations.push_back(e);
+
+ return vespalib::LazyFile::read(buf, bufsize, offset);
+ }
+
+ std::string toString() const {
+ std::ostringstream ost;
+ for (uint32_t i = 0; i < operations.size(); i++) {
+ ost << operations[i].toString() << "\n";
+ }
+
+ return ost.str();
+ }
+
+
+
+};
+
+}
+
+}
+
diff --git a/memfilepersistence/src/tests/spi/memcachetest.cpp b/memfilepersistence/src/tests/spi/memcachetest.cpp
new file mode 100644
index 00000000000..d34159ce3f4
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/memcachetest.cpp
@@ -0,0 +1,412 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/memfile/memfilecache.h>
+#include <vespa/storageframework/defaultimplementation/memory/simplememorylogic.h>
+#include <tests/spi/memfiletestutils.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+
+
+namespace storage {
+namespace memfile {
+
+class MemCacheTest : public SingleDiskMemFileTestUtils
+{
+ CPPUNIT_TEST_SUITE(MemCacheTest);
+ CPPUNIT_TEST(testSimpleLRU);
+ CPPUNIT_TEST(testCacheSize);
+ CPPUNIT_TEST(testEvictBody);
+ CPPUNIT_TEST(testEvictHeader);
+ CPPUNIT_TEST(testKeepBodyWhenLessThanOneFourth);
+ CPPUNIT_TEST(testComplexEviction);
+ CPPUNIT_TEST(testEraseEmptyOnReturn);
+ CPPUNIT_TEST(testDeleteDoesNotReAddMemoryUsage);
+ CPPUNIT_TEST(testEraseDoesNotReAddMemoryUsage);
+ CPPUNIT_TEST(testGetWithNoCreation);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void testSimpleLRU();
+ void testCacheSize();
+ void testReduceCacheSizeCallback();
+ void testReduceCacheSizeCallbackWhileActive();
+ void testEvictBody();
+ void testEvictHeader();
+ void testKeepBodyWhenLessThanOneFourth();
+ void testComplexEviction();
+ void testEraseEmptyOnReturn();
+ void testDeleteDoesNotReAddMemoryUsage();
+ void testEraseDoesNotReAddMemoryUsage();
+ void testGetWithNoCreation();
+
+private:
+ framework::defaultimplementation::ComponentRegisterImpl::UP _register;
+ framework::Component::UP _component;
+ FakeClock::UP _clock;
+ framework::defaultimplementation::MemoryManager::UP _memoryManager;
+ std::vector<framework::MemoryToken::LP> _stolenMemory;
+ std::unique_ptr<MemFilePersistenceMetrics> _metrics;
+
+ std::unique_ptr<MemFileCache> _cache;
+
+ void setSize(const document::BucketId& id,
+ uint64_t metaSize,
+ uint64_t headerSz = 0,
+ uint64_t bodySz = 0,
+ bool createIfNotInCache = true)
+ {
+ MemFilePtr file(_cache->get(id, env(), env().getDirectory(),
+ createIfNotInCache));
+ CPPUNIT_ASSERT(file.get());
+
+ file->_cacheSizeOverride.metaSize = metaSize;
+ file->_cacheSizeOverride.headerSize = headerSz;
+ file->_cacheSizeOverride.bodySize = bodySz;
+ }
+
+ std::string
+ getBucketStatus(uint32_t buckets)
+ {
+ std::ostringstream ost;
+ for (uint32_t i = 1; i < buckets + 1; i++) {
+ document::BucketId id(16, i);
+ ost << id << " ";
+ if (!_cache->contains(id)) {
+ ost << "<nil>\n";
+ } else {
+ MemFilePtr file(_cache->get(id, env(), env().getDirectory()));
+ if (file->_cacheSizeOverride.bodySize > 0) {
+ ost << "body,";
+ }
+ if (file->_cacheSizeOverride.headerSize > 0) {
+ ost << "header\n";
+ } else {
+ ost << "meta only\n";
+ }
+ }
+ }
+
+ return ost.str();
+ }
+
+ uint64_t cacheSize() {
+ return _cache->size();
+ }
+
+ document::BucketId getLRU() {
+ return _cache->getLeastRecentlyUsedBucket()->_bid;
+ }
+
+ void setCacheSize(uint64_t sz) {
+ MemFileCache::MemoryUsage usage;
+ usage.metaSize = sz / 3;
+ usage.headerSize = sz / 3;
+ usage.bodySize = sz - usage.metaSize - usage.headerSize;
+
+ _cache->setCacheSize(usage);
+ }
+
+ void stealMemory(uint64_t memToSteal) {
+ setCacheSize(_cache->getCacheSize() - memToSteal);
+ }
+
+ void setup(uint64_t maxMemory) {
+ tearDown();
+ _register.reset(
+ new framework::defaultimplementation::ComponentRegisterImpl);
+ _clock.reset(new FakeClock);
+ _register->setClock(*_clock);
+ _memoryManager.reset(
+ new framework::defaultimplementation::MemoryManager(
+ framework::defaultimplementation::AllocationLogic::UP(
+ new framework::defaultimplementation::SimpleMemoryLogic(
+ *_clock, maxMemory * 2))));
+ _register->setMemoryManager(*_memoryManager);
+ _component.reset(new framework::Component(*_register, "testcomponent"));
+ _metrics.reset(new MemFilePersistenceMetrics(*_component));
+ _cache.reset(new MemFileCache(*_register, _metrics->_cache));
+ setCacheSize(maxMemory);
+ _memoryManager->registerAllocationType(framework::MemoryAllocationType(
+ "steal", framework::MemoryAllocationType::FORCE_ALLOCATE));
+ }
+
+public:
+ void tearDown() {
+ _stolenMemory.clear();
+ _cache.reset(0);
+ _metrics.reset(0);
+ _component.reset(0);
+ _register.reset(0);
+ _memoryManager.reset(0);
+ _clock.reset(0);
+ }
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(MemCacheTest);
+
+namespace {
+ FakeClock clock;
+}
+
+void
+MemCacheTest::testSimpleLRU()
+{
+ setup(2000);
+
+ for (uint32_t i = 1; i < 4; i++) {
+ setSize(document::BucketId(16, i), 100);
+ }
+
+ CPPUNIT_ASSERT_EQUAL(document::BucketId(16, 1), getLRU());
+
+ setSize(document::BucketId(16, 1), 100);
+
+ CPPUNIT_ASSERT_EQUAL(1UL, _cache->getMetrics().hits.getValue());
+ CPPUNIT_ASSERT_EQUAL(document::BucketId(16, 2), getLRU());
+}
+
+void
+MemCacheTest::testCacheSize()
+{
+ setup(400);
+
+ setSize(document::BucketId(16, 2), 100);
+ setSize(document::BucketId(16, 1), 150);
+
+ CPPUNIT_ASSERT_EQUAL(0UL, _cache->getMetrics().hits.getValue());
+ CPPUNIT_ASSERT_EQUAL(2UL, _cache->getMetrics().misses.getValue());
+
+ CPPUNIT_ASSERT_EQUAL(250ul, cacheSize());
+
+ setSize(document::BucketId(16, 1), 200);
+
+ CPPUNIT_ASSERT_EQUAL(1UL, _cache->getMetrics().hits.getValue());
+ CPPUNIT_ASSERT_EQUAL(2UL, _cache->getMetrics().misses.getValue());
+
+ CPPUNIT_ASSERT_EQUAL(300ul, cacheSize());
+
+ CPPUNIT_ASSERT(_cache->contains(document::BucketId(16, 2)));
+ CPPUNIT_ASSERT(_cache->contains(document::BucketId(16, 1)));
+
+ setSize(document::BucketId(16, 1), 301);
+
+ CPPUNIT_ASSERT_EQUAL(2UL, _cache->getMetrics().hits.getValue());
+ CPPUNIT_ASSERT_EQUAL(2UL, _cache->getMetrics().misses.getValue());
+
+ CPPUNIT_ASSERT(!_cache->contains(document::BucketId(16, 2)));
+ CPPUNIT_ASSERT(_cache->contains(document::BucketId(16, 1)));
+
+ _cache->clear();
+ CPPUNIT_ASSERT_EQUAL(0ul, cacheSize());
+}
+
+void
+MemCacheTest::testEvictBody()
+{
+ setup(1400);
+
+ CPPUNIT_ASSERT_EQUAL(0UL, _cache->getMetrics().body_evictions.getValue());
+
+ setSize(BucketId(16, 1), 150, 100, 0);
+ setSize(BucketId(16, 2), 100, 100, 900);
+
+ CPPUNIT_ASSERT_EQUAL(1350ul, cacheSize());
+
+ stealMemory(150);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x4000000000000001) header\n"
+ "BucketId(0x4000000000000002) header\n"),
+ getBucketStatus(2));
+ CPPUNIT_ASSERT_EQUAL(1UL, _cache->getMetrics().body_evictions.getValue());
+}
+
+void
+MemCacheTest::testKeepBodyWhenLessThanOneFourth()
+{
+ setup(450);
+
+ setSize(BucketId(16, 1), 150, 0, 0);
+ setSize(BucketId(16, 2), 100, 50, 50);
+
+ stealMemory(150);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x4000000000000001) <nil>\n"
+ "BucketId(0x4000000000000002) body,header\n"),
+ getBucketStatus(2));
+}
+
+void
+MemCacheTest::testEvictHeader()
+{
+ setup(550);
+
+ CPPUNIT_ASSERT_EQUAL(0UL, _cache->getMetrics().header_evictions.getValue());
+
+ setSize(BucketId(16, 1), 150, 0, 0);
+ setSize(BucketId(16, 2), 100, 200, 100);
+
+ stealMemory(150);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x4000000000000001) meta only\n"
+ "BucketId(0x4000000000000002) meta only\n"),
+ getBucketStatus(2));
+ CPPUNIT_ASSERT_EQUAL(1UL, _cache->getMetrics().header_evictions.getValue());
+}
+
+#define ASSERT_CACHE_EVICTIONS(meta, header, body) \
+ CPPUNIT_ASSERT_EQUAL(size_t(meta), _cache->getMetrics().body_evictions.getValue()); \
+ CPPUNIT_ASSERT_EQUAL(size_t(header), _cache->getMetrics().header_evictions.getValue()); \
+ CPPUNIT_ASSERT_EQUAL(size_t(body), _cache->getMetrics().meta_evictions.getValue());
+
+void
+MemCacheTest::testComplexEviction()
+{
+ setup(4200);
+
+ setSize(BucketId(16, 1), 150, 0, 0);
+ setSize(BucketId(16, 2), 100, 200, 200);
+ setSize(BucketId(16, 3), 100, 200, 0);
+ setSize(BucketId(16, 4), 100, 400, 0);
+ setSize(BucketId(16, 5), 100, 200, 400);
+ setSize(BucketId(16, 6), 100, 200, 300);
+ setSize(BucketId(16, 7), 100, 0, 0);
+ setSize(BucketId(16, 8), 100, 200, 400);
+ setSize(BucketId(16, 9), 100, 200, 250);
+
+ CPPUNIT_ASSERT_EQUAL(4100ul, cacheSize());
+
+ ASSERT_CACHE_EVICTIONS(0, 0, 0);
+
+ stealMemory(600);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x4000000000000001) meta only\n"
+ "BucketId(0x4000000000000002) header\n"
+ "BucketId(0x4000000000000003) header\n"
+ "BucketId(0x4000000000000004) header\n"
+ "BucketId(0x4000000000000005) header\n"
+ "BucketId(0x4000000000000006) body,header\n"
+ "BucketId(0x4000000000000007) meta only\n"
+ "BucketId(0x4000000000000008) body,header\n"
+ "BucketId(0x4000000000000009) body,header\n"),
+ getBucketStatus(9));
+
+ CPPUNIT_ASSERT_EQUAL(3500ul, cacheSize());
+
+ ASSERT_CACHE_EVICTIONS(2, 0, 0);
+
+ stealMemory(500);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x4000000000000001) meta only\n"
+ "BucketId(0x4000000000000002) meta only\n"
+ "BucketId(0x4000000000000003) meta only\n"
+ "BucketId(0x4000000000000004) header\n"
+ "BucketId(0x4000000000000005) header\n"
+ "BucketId(0x4000000000000006) body,header\n"
+ "BucketId(0x4000000000000007) meta only\n"
+ "BucketId(0x4000000000000008) body,header\n"
+ "BucketId(0x4000000000000009) body,header\n"),
+ getBucketStatus(9));
+
+ CPPUNIT_ASSERT_EQUAL(3100ul, cacheSize());
+
+ ASSERT_CACHE_EVICTIONS(2, 2, 0);
+
+ stealMemory(1000);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x4000000000000001) <nil>\n"
+ "BucketId(0x4000000000000002) meta only\n"
+ "BucketId(0x4000000000000003) meta only\n"
+ "BucketId(0x4000000000000004) meta only\n"
+ "BucketId(0x4000000000000005) meta only\n"
+ "BucketId(0x4000000000000006) header\n"
+ "BucketId(0x4000000000000007) meta only\n"
+ "BucketId(0x4000000000000008) body,header\n"
+ "BucketId(0x4000000000000009) body,header\n"),
+ getBucketStatus(9));
+
+ CPPUNIT_ASSERT_EQUAL(2050ul, cacheSize());
+
+ ASSERT_CACHE_EVICTIONS(3, 4, 1);
+
+ stealMemory(1100);
+
+ CPPUNIT_ASSERT_EQUAL(
+ std::string(
+ "BucketId(0x4000000000000001) <nil>\n"
+ "BucketId(0x4000000000000002) <nil>\n"
+ "BucketId(0x4000000000000003) <nil>\n"
+ "BucketId(0x4000000000000004) <nil>\n"
+ "BucketId(0x4000000000000005) <nil>\n"
+ "BucketId(0x4000000000000006) <nil>\n"
+ "BucketId(0x4000000000000007) meta only\n"
+ "BucketId(0x4000000000000008) header\n"
+ "BucketId(0x4000000000000009) body,header\n"),
+ getBucketStatus(9));
+
+ CPPUNIT_ASSERT_EQUAL(950ul, cacheSize());
+}
+
+#undef ASSERT_CACHE_EVICTIONS
+
+void
+MemCacheTest::testEraseEmptyOnReturn()
+{
+ setup(4200);
+ setSize(BucketId(16, 1), 0, 0, 0);
+ CPPUNIT_ASSERT(!_cache->contains(document::BucketId(16, 1)));
+}
+
+void
+MemCacheTest::testDeleteDoesNotReAddMemoryUsage()
+{
+ BucketId id(16, 1);
+ setup(1000);
+ setSize(id, 100, 200, 300);
+ CPPUNIT_ASSERT_EQUAL(600ul, cacheSize());
+ {
+ MemFilePtr file(_cache->get(id, env(), env().getDirectory()));
+ file.deleteFile();
+ }
+ CPPUNIT_ASSERT_EQUAL(0ul, cacheSize());
+
+}
+
+void
+MemCacheTest::testGetWithNoCreation()
+{
+ BucketId id(16, 1);
+ setup(1000);
+ setSize(id, 100, 200, 300, false);
+ CPPUNIT_ASSERT_EQUAL(0ul, cacheSize());
+}
+
+
+void
+MemCacheTest::testEraseDoesNotReAddMemoryUsage()
+{
+ BucketId id(16, 1);
+ setup(1000);
+ setSize(id, 100, 200, 300);
+ CPPUNIT_ASSERT_EQUAL(600ul, cacheSize());
+ {
+ MemFilePtr file(_cache->get(id, env(), env().getDirectory()));
+ file.eraseFromCache();
+ }
+ CPPUNIT_ASSERT_EQUAL(0ul, cacheSize());
+
+}
+
+} // memfile
+} // storage
diff --git a/memfilepersistence/src/tests/spi/memfileautorepairtest.cpp b/memfilepersistence/src/tests/spi/memfileautorepairtest.cpp
new file mode 100644
index 00000000000..04d82741e67
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/memfileautorepairtest.cpp
@@ -0,0 +1,411 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/mapper/memfilemapper.h>
+#include <vespa/memfilepersistence/mapper/memfile_v1_serializer.h>
+#include <vespa/memfilepersistence/mapper/memfile_v1_verifier.h>
+#include <vespa/memfilepersistence/mapper/fileinfo.h>
+#include <vespa/memfilepersistence/mapper/simplememfileiobuffer.h>
+#include <tests/spi/memfiletestutils.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+
+namespace storage {
+namespace memfile {
+
+class MemFileAutoRepairTest : public SingleDiskMemFileTestUtils
+{
+public:
+ void setUp();
+ void tearDown();
+
+ void testFileMetadataCorruptionIsAutoRepaired();
+ void testDocumentContentCorruptionIsAutoRepaired();
+ void testCorruptionEvictsBucketFromCache();
+ void testRepairFailureInMaintainEvictsBucketFromCache();
+ void testZeroLengthFileIsDeleted();
+ void testTruncatedBodyLocationIsAutoRepaired();
+ void testTruncatedHeaderLocationIsAutoRepaired();
+ void testTruncatedHeaderBlockIsAutoRepaired();
+
+ void corruptBodyBlock();
+
+ CPPUNIT_TEST_SUITE(MemFileAutoRepairTest);
+ CPPUNIT_TEST(testFileMetadataCorruptionIsAutoRepaired);
+ CPPUNIT_TEST(testDocumentContentCorruptionIsAutoRepaired);
+ CPPUNIT_TEST(testCorruptionEvictsBucketFromCache);
+ CPPUNIT_TEST(testRepairFailureInMaintainEvictsBucketFromCache);
+ CPPUNIT_TEST(testZeroLengthFileIsDeleted);
+ CPPUNIT_TEST(testTruncatedBodyLocationIsAutoRepaired);
+ CPPUNIT_TEST(testTruncatedHeaderLocationIsAutoRepaired);
+ CPPUNIT_TEST(testTruncatedHeaderBlockIsAutoRepaired);
+ CPPUNIT_TEST_SUITE_END();
+
+private:
+ void assertDocumentIsSilentlyRemoved(
+ const document::BucketId& bucket,
+ const document::DocumentId& docId);
+
+ void reconfigureMinimumHeaderBlockSize(uint32_t newMinSize);
+
+ document::BucketId _bucket;
+ std::unique_ptr<FileSpecification> _file;
+ std::vector<document::DocumentId> _slotIds;
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(MemFileAutoRepairTest);
+
+namespace {
+ // A totall uncached memfile with content to use for verify testing
+ std::unique_ptr<MemFile> _memFile;
+
+ // Clear old content. Create new file. Make sure nothing is cached.
+ void prepareBucket(SingleDiskMemFileTestUtils& util,
+ const FileSpecification& file) {
+ _memFile.reset();
+ util.env()._cache.clear();
+ vespalib::unlink(file.getPath());
+ util.createTestBucket(file.getBucketId(), 0);
+ util.env()._cache.clear();
+ _memFile.reset(new MemFile(file, util.env()));
+ _memFile->getMemFileIO().close();
+
+ }
+
+ MetaSlot getSlot(uint32_t index) {
+ assert(_memFile.get());
+ vespalib::LazyFile file(_memFile->getFile().getPath(), 0);
+ MetaSlot result;
+ file.read(&result, sizeof(MetaSlot),
+ sizeof(Header) + sizeof(MetaSlot) * index);
+ return result;
+ }
+
+ void setSlot(uint32_t index, MetaSlot slot,
+ bool updateFileChecksum = true)
+ {
+ (void)updateFileChecksum;
+ assert(_memFile.get());
+ //if (updateFileChecksum) slot.updateFileChecksum();
+ vespalib::LazyFile file(_memFile->getFile().getPath(), 0);
+ file.write(&slot, sizeof(MetaSlot),
+ sizeof(Header) + sizeof(MetaSlot) * index);
+ }
+}
+
+void
+MemFileAutoRepairTest::setUp()
+{
+ SingleDiskMemFileTestUtils::setUp();
+ _bucket = BucketId(16, 0xa);
+ createTestBucket(_bucket, 0);
+
+ {
+ MemFilePtr memFilePtr(env()._cache.get(_bucket, env(), env().getDirectory()));
+ _file.reset(new FileSpecification(memFilePtr->getFile()));
+ CPPUNIT_ASSERT(memFilePtr->getSlotCount() >= 2);
+ for (size_t i = 0; i < memFilePtr->getSlotCount(); ++i) {
+ _slotIds.push_back(memFilePtr->getDocumentId((*memFilePtr)[i]));
+ }
+ }
+ env()._cache.clear();
+}
+
+void
+MemFileAutoRepairTest::tearDown()
+{
+ _file.reset(0);
+ _memFile.reset(0);
+ SingleDiskMemFileTestUtils::tearDown();
+};
+
+void
+MemFileAutoRepairTest::testFileMetadataCorruptionIsAutoRepaired()
+{
+ // Test corruption detected in initial metadata load
+ prepareBucket(*this, *_file);
+ document::DocumentId id(_slotIds[1]);
+ MetaSlot slot(getSlot(1));
+ CPPUNIT_ASSERT_EQUAL(slot._gid,
+ id.getGlobalId()); // Sanity checking...
+ {
+ MetaSlot s(slot);
+ s.setTimestamp(Timestamp(40));
+ setSlot(1, s);
+ }
+
+ CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
+
+ // File not in cache; should be detected in initial load
+ spi::GetResult res(doGet(_bucket, id, document::AllFields()));
+ // FIXME: currently loadFile is silently fixing corruptions!
+ //CPPUNIT_ASSERT_EQUAL(spi::Result::TRANSIENT_ERROR, res.getErrorCode());
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res.getErrorCode());
+ CPPUNIT_ASSERT(!res.hasDocument());
+
+ CPPUNIT_ASSERT_EQUAL(std::string("400000000000000a"), getModifiedBuckets());
+ CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
+
+ // File should now have been repaired, so a subsequent get for
+ // the same document should just return an empty (but OK) result.
+ spi::GetResult res2(doGet(_bucket, id, document::AllFields()));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res2.getErrorCode());
+ CPPUNIT_ASSERT(!res2.hasDocument());
+
+ CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
+}
+
+void
+MemFileAutoRepairTest::corruptBodyBlock()
+{
+ CPPUNIT_ASSERT(!env()._cache.contains(_bucket));
+ // Corrupt body block of slot 1
+ MetaSlot slot(getSlot(1));
+ {
+ MetaSlot s(slot);
+ s.setBodyPos(52);
+ s.setBodySize(18);
+ s.updateChecksum();
+ setSlot(1, s);
+ }
+}
+
+void
+MemFileAutoRepairTest::testDocumentContentCorruptionIsAutoRepaired()
+{
+ // Corrupt body block
+ prepareBucket(*this, *_file);
+ document::DocumentId id(_slotIds[1]);
+ corruptBodyBlock();
+
+ CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
+
+ spi::GetResult res(doGet(_bucket, id, document::AllFields()));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::TRANSIENT_ERROR, res.getErrorCode());
+ CPPUNIT_ASSERT(!res.hasDocument());
+
+ CPPUNIT_ASSERT(!env()._cache.contains(_bucket));
+
+ CPPUNIT_ASSERT_EQUAL(std::string("400000000000000a"), getModifiedBuckets());
+ CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
+
+ // File should now have been repaired, so a subsequent get for
+ // the same document should just return an empty (but OK) result.
+ spi::GetResult res2(doGet(_bucket, id, document::AllFields()));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res2.getErrorCode());
+ CPPUNIT_ASSERT(!res2.hasDocument());
+
+ // File should now be in cache OK
+ CPPUNIT_ASSERT(env()._cache.contains(_bucket));
+ CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
+}
+
+// Ideally we'd test this for each spi operation that accesses MemFiles, but
+// they all use the same eviction+auto-repair logic...
+void
+MemFileAutoRepairTest::testCorruptionEvictsBucketFromCache()
+{
+ prepareBucket(*this, *_file);
+ corruptBodyBlock();
+
+ // Read slot 0 and shove file into cache
+ spi::GetResult res(doGet(_bucket, _slotIds[0], document::AllFields()));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res.getErrorCode());
+ CPPUNIT_ASSERT(res.hasDocument());
+ CPPUNIT_ASSERT(env()._cache.contains(_bucket));
+
+ spi::GetResult res2(doGet(_bucket, _slotIds[1], document::AllFields()));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::TRANSIENT_ERROR, res2.getErrorCode());
+ CPPUNIT_ASSERT(!res2.hasDocument());
+
+ // Out of the cache! Begone! Shoo!
+ CPPUNIT_ASSERT(!env()._cache.contains(_bucket));
+
+}
+
+void
+MemFileAutoRepairTest::testRepairFailureInMaintainEvictsBucketFromCache()
+{
+ prepareBucket(*this, *_file);
+ corruptBodyBlock();
+ spi::Result result(getPersistenceProvider().maintain(
+ spi::Bucket(_bucket, spi::PartitionId(0)), spi::HIGH));
+ // File being successfully repaired does not constitute a failure of
+ // the maintain() call.
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
+ // It should, however, shove it out of the cache.
+ CPPUNIT_ASSERT(!env()._cache.contains(_bucket));
+}
+
+void
+MemFileAutoRepairTest::testZeroLengthFileIsDeleted()
+{
+ // Completely truncate auto-created file
+ vespalib::LazyFile file(_file->getPath(), 0);
+ file.resize(0);
+
+ // No way to deal with zero-length files aside from deleting them.
+ spi::Result result(getPersistenceProvider().maintain(
+ spi::Bucket(_bucket, spi::PartitionId(0)), spi::HIGH));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
+ CPPUNIT_ASSERT(!env()._cache.contains(_bucket));
+ CPPUNIT_ASSERT(!vespalib::fileExists(_file->getPath()));
+}
+
+namespace {
+
+uint32_t
+alignDown(uint32_t value)
+{
+ uint32_t blocks = value / 512;
+ return blocks * 512;
+};
+
+FileInfo
+fileInfoFromMemFile(const MemFilePtr& mf)
+{
+ auto& ioBuf(dynamic_cast<const SimpleMemFileIOBuffer&>(
+ mf->getMemFileIO()));
+ return ioBuf.getFileInfo();
+}
+
+}
+
+void
+MemFileAutoRepairTest::assertDocumentIsSilentlyRemoved(
+ const document::BucketId& bucket,
+ const document::DocumentId& docId)
+{
+ // Corrupted (truncated) slot should be transparently removed during
+ // loadFile and it should be as if it was never there!
+ spi::Bucket spiBucket(bucket, spi::PartitionId(0));
+ spi::GetResult res(doGet(spiBucket, docId, document::AllFields()));
+ CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res.getErrorCode());
+ CPPUNIT_ASSERT(!res.hasDocument());
+}
+
+void
+MemFileAutoRepairTest::testTruncatedBodyLocationIsAutoRepaired()
+{
+ document::BucketId bucket(16, 4);
+ document::Document::SP doc(
+ createRandomDocumentAtLocation(4, 1234, 1024, 1024));
+
+ doPut(doc, bucket, framework::MicroSecTime(1000));
+ flush(bucket);
+ FileInfo fileInfo;
+ {
+ MemFilePtr mf(getMemFile(bucket));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), mf->getSlotCount());
+ fileInfo = fileInfoFromMemFile(mf);
+
+ const uint32_t bodyBlockStart(
+ sizeof(Header)
+ + fileInfo._metaDataListSize * sizeof(MetaSlot)
+ + fileInfo._headerBlockSize);
+
+ vespalib::LazyFile file(mf->getFile().getPath(), 0);
+ uint32_t slotBodySize = (*mf)[0].getLocation(BODY)._size;
+ CPPUNIT_ASSERT(slotBodySize > 0);
+ // Align down to nearest sector alignment to avoid unrelated DirectIO
+ // checks to kick in. Since the body block is always aligned on a
+ // sector boundary, we know this cannot truncate into the header block.
+ file.resize(alignDown(bodyBlockStart + slotBodySize - 1));
+ }
+ env()._cache.clear();
+ assertDocumentIsSilentlyRemoved(bucket, doc->getId());
+}
+
+void
+MemFileAutoRepairTest::testTruncatedHeaderLocationIsAutoRepaired()
+{
+ document::BucketId bucket(16, 4);
+ document::Document::SP doc(
+ createRandomDocumentAtLocation(4, 1234, 1024, 1024));
+ // Ensure header has a bunch of data (see alignment comments below).
+ doc->setValue(doc->getField("hstringval"),
+ document::StringFieldValue(std::string(1024, 'A')));
+
+ doPut(doc, bucket, framework::MicroSecTime(1000));
+ flush(bucket);
+ FileInfo fileInfo;
+ {
+ MemFilePtr mf(getMemFile(bucket));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), mf->getSlotCount());
+ fileInfo = fileInfoFromMemFile(mf);
+
+ const uint32_t headerBlockStart(
+ sizeof(Header)
+ + fileInfo._metaDataListSize * sizeof(MetaSlot));
+
+ vespalib::LazyFile file(mf->getFile().getPath(), 0);
+ uint32_t slotHeaderSize = (*mf)[0].getLocation(HEADER)._size;
+ CPPUNIT_ASSERT(slotHeaderSize > 0);
+ // Align down to nearest sector alignment to avoid unrelated DirectIO
+ // checks to kick in. The header block is not guaranteed to start on
+ // sector boundary, but we assume there is enough slack in the header
+ // section for the metadata slots themselves to be untouched since we
+ // have a minimum header size of 1024 for the doc in question.
+ file.resize(alignDown(headerBlockStart + slotHeaderSize - 1));
+ }
+ env()._cache.clear();
+ assertDocumentIsSilentlyRemoved(bucket, doc->getId());
+}
+
+void
+MemFileAutoRepairTest::reconfigureMinimumHeaderBlockSize(uint32_t newMinSize)
+{
+ using MemFileConfig = vespa::config::storage::StorMemfilepersistenceConfig;
+ using MemFileConfigBuilder
+ = vespa::config::storage::StorMemfilepersistenceConfigBuilder;
+ MemFileConfigBuilder builder(
+ *env().acquireConfigReadLock().memFilePersistenceConfig());
+ builder.minimumFileMetaSlots = 2;
+ builder.minimumFileHeaderBlockSize = newMinSize;
+ auto newConfig = std::unique_ptr<MemFileConfig>(new MemFileConfig(builder));
+ env().acquireConfigWriteLock().setMemFilePersistenceConfig(
+ std::move(newConfig));
+}
+
+void
+MemFileAutoRepairTest::testTruncatedHeaderBlockIsAutoRepaired()
+{
+ document::BucketId bucket(16, 4);
+ document::Document::SP doc(
+ createRandomDocumentAtLocation(4, 1234, 1, 1));
+ // Ensure header block is large enough that free space is added to the end.
+ reconfigureMinimumHeaderBlockSize(8192);
+ // Add header field and remove randomly generated body field, ensuring
+ // we have no data to add to body field. This will prevent slot body
+ // location checking from detecting a header truncation.
+ doc->setValue(doc->getField("hstringval"),
+ document::StringFieldValue("foo"));
+ doc->remove(doc->getField("content"));
+
+ doPut(doc, bucket, framework::MicroSecTime(1000));
+ flush(bucket);
+ FileInfo fileInfo;
+ {
+ MemFilePtr mf(getMemFile(bucket));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), mf->getSlotCount());
+ fileInfo = fileInfoFromMemFile(mf);
+
+ const uint32_t headerBlockEnd(
+ sizeof(Header)
+ + fileInfo._metaDataListSize * sizeof(MetaSlot)
+ + fileInfo._headerBlockSize);
+
+ vespalib::LazyFile file(mf->getFile().getPath(), 0);
+ CPPUNIT_ASSERT_EQUAL(uint32_t(0),
+ (*mf)[0].getLocation(BODY)._size); // No body.
+ const auto headerLoc((*mf)[0].getLocation(HEADER));
+ const uint32_t extent(headerLoc._pos + headerLoc._size);
+ // Make sure we don't intersect an existing slot range.
+ CPPUNIT_ASSERT(extent < alignDown(headerBlockEnd - 1));
+ file.resize(alignDown(headerBlockEnd - 1));
+ }
+ env()._cache.clear();
+ assertDocumentIsSilentlyRemoved(bucket, doc->getId());
+}
+
+}
+}
diff --git a/memfilepersistence/src/tests/spi/memfiletest.cpp b/memfilepersistence/src/tests/spi/memfiletest.cpp
new file mode 100644
index 00000000000..70b03271da9
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/memfiletest.cpp
@@ -0,0 +1,987 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/memfile/memfile.h>
+#include <tests/spi/memfiletestutils.h>
+#include <tests/spi/logginglazyfile.h>
+#include <tests/spi/options_builder.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/memfilepersistence/memfile/memfilecompactor.h>
+#include <vespa/memfilepersistence/mapper/simplememfileiobuffer.h>
+#include <limits>
+
+namespace storage {
+namespace memfile {
+
+struct MemFileTest : public SingleDiskMemFileTestUtils
+{
+ typedef MemFileCompactor::SlotList SlotList;
+
+ /**
+ * Feed a document whose ID is deterministically generated from `seed` to
+ * bucket (16, 4) at time `timestamp`.
+ */
+ document::DocumentId feedDocument(
+ uint64_t seed,
+ uint64_t timestamp,
+ uint32_t headerSize = 0,
+ uint32_t minBodySize = 10,
+ uint32_t maxBodySize = 100);
+
+ /**
+ * Feed n instances of documents with the same ID to bucket (16, 4) using
+ * a timestamp range of [1000, 1000+n).
+ */
+ void feedSameDocNTimes(uint32_t n);
+
+ void setMaxDocumentVersionsOption(uint32_t n);
+
+ std::vector<Types::Timestamp> compactWithVersionLimit(uint32_t maxVersions);
+
+ void testCompactRemoveDoublePut();
+ void testCompactPutRemove();
+ void testCompactGidCollision();
+ void testCompactGidCollisionAndNot();
+ void testCompactWithMemFile();
+ void testCompactCombined();
+ void testCompactDifferentPuts();
+ void testNoCompactionWhenDocumentVersionsWithinLimit();
+ void testCompactWhenDocumentVersionsExceedLimit();
+ void testCompactLimit1KeepsNewestVersionOnly();
+ void testCompactionOptionsArePropagatedFromConfig();
+ void testZeroDocumentVersionConfigIsCorrected();
+ void testResizeToFreeSpace();
+ void testNoFileWriteOnNoOpCompaction();
+ void testCacheSize();
+ void testClearCache();
+ void testGetSlotsByTimestamp();
+ void testCacheInconsistentSlot();
+ void testEnsureCached();
+ void testAddSlotWhenDiskFull();
+ void testGetSerializedSize();
+ void testGetBucketInfo();
+ void testCopySlotsPreservesLocationSharing();
+ void testFlushingToNonExistingFileAlwaysRunsCompaction();
+ void testOrderDocSchemeDocumentsCanBeAddedToFile();
+
+ CPPUNIT_TEST_SUITE(MemFileTest);
+ CPPUNIT_TEST(testCompactRemoveDoublePut);
+ CPPUNIT_TEST(testCompactPutRemove);
+ CPPUNIT_TEST(testCompactGidCollision);
+ CPPUNIT_TEST(testCompactGidCollisionAndNot);
+ CPPUNIT_TEST(testCompactWithMemFile);
+ CPPUNIT_TEST(testCompactCombined);
+ CPPUNIT_TEST(testCompactDifferentPuts);
+ CPPUNIT_TEST(testNoCompactionWhenDocumentVersionsWithinLimit);
+ CPPUNIT_TEST(testCompactWhenDocumentVersionsExceedLimit);
+ CPPUNIT_TEST(testCompactLimit1KeepsNewestVersionOnly);
+ CPPUNIT_TEST(testCompactionOptionsArePropagatedFromConfig);
+ CPPUNIT_TEST(testZeroDocumentVersionConfigIsCorrected);
+ CPPUNIT_TEST(testNoFileWriteOnNoOpCompaction);
+ CPPUNIT_TEST(testCacheSize);
+ CPPUNIT_TEST(testClearCache);
+ CPPUNIT_TEST(testGetSlotsByTimestamp);
+ CPPUNIT_TEST(testEnsureCached);
+ CPPUNIT_TEST(testResizeToFreeSpace);
+ CPPUNIT_TEST(testAddSlotWhenDiskFull);
+ CPPUNIT_TEST(testGetSerializedSize);
+ CPPUNIT_TEST(testGetBucketInfo);
+ CPPUNIT_TEST(testCopySlotsPreservesLocationSharing);
+ CPPUNIT_TEST(testFlushingToNonExistingFileAlwaysRunsCompaction);
+ CPPUNIT_TEST(testOrderDocSchemeDocumentsCanBeAddedToFile);
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(MemFileTest);
+
+/**
+ * Slots should actually be the same pointer. Use this assert to do correct
+ * check, and still print content of slots on failure.
+ */
+#define ASSERT_SLOT_EQUAL(slotptra, slotptrb) \
+{ \
+ CPPUNIT_ASSERT(slotptra != 0); \
+ CPPUNIT_ASSERT(slotptrb != 0); \
+ std::ostringstream slotdiff; \
+ slotdiff << "Expected: " << *slotptra << ", but got " << *slotptrb; \
+ CPPUNIT_ASSERT_EQUAL_MSG(slotdiff.str(), slotptra, slotptrb); \
+}
+
+namespace {
+
+framework::MicroSecTime sec(uint64_t n) {
+ return framework::MicroSecTime(n * 1000000ULL);
+}
+
+/**
+ * Utility functions for tests to call to do compacting, such that the
+ * tests themselves are not bound to the current interface.
+ *
+ * Also, this function translates second time to microsecond time.
+ */
+MemFileTest::SlotList getSlotsToRemove(
+ const MemFile& file, uint64_t currentTime,
+ uint64_t revertTime, uint64_t keepRemoveTime)
+{
+ MemFileCompactor compactor(
+ sec(currentTime),
+ CompactionOptions()
+ .maxDocumentVersions(
+ std::numeric_limits<uint32_t>::max())
+ .revertTimePeriod(sec(revertTime))
+ .keepRemoveTimePeriod(sec(keepRemoveTime)));
+ return compactor.getSlotsToRemove(file);
+}
+
+class AutoFlush
+{
+public:
+ AutoFlush(MemFilePtr& ptr) : _ptr(ptr) {}
+ ~AutoFlush() { _ptr->flushToDisk(); }
+private:
+ MemFilePtr& _ptr;
+};
+
+}
+
+document::DocumentId
+MemFileTest::feedDocument(
+ uint64_t seed,
+ uint64_t timestamp,
+ uint32_t headerSize,
+ uint32_t minDocSize,
+ uint32_t maxDocSize) {
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 4, seed, minDocSize, maxDocSize));
+
+ if (headerSize > 0) {
+ std::string val(headerSize, 'A');
+ doc->setValue(doc->getField("hstringval"),
+ document::StringFieldValue(val));
+ }
+
+ doPut(doc,
+ document::BucketId(16, 4),
+ Timestamp(timestamp * 1000000));
+
+ return doc->getId();
+}
+
+void
+MemFileTest::feedSameDocNTimes(uint32_t n)
+{
+ for (uint32_t i = 0; i < n; ++i) {
+ feedDocument(1234, 1000 + i);
+ }
+}
+
+void
+MemFileTest::setMaxDocumentVersionsOption(uint32_t n)
+{
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options)
+ .maxDocumentVersions(n)
+ .build());
+}
+
+void
+MemFileTest::testCacheSize()
+{
+ // Feed some puts
+ for (uint32_t i = 0; i < 4; i++) {
+ feedDocument(1234 * (i % 2), 1000 + 200 * i);
+ }
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+
+ CPPUNIT_ASSERT(file->getCacheSize().sum() > 0);
+}
+
+void
+MemFileTest::testClearCache()
+{
+ // Feed some puts
+ for (uint32_t i = 0; i < 4; i++) {
+ feedDocument(1234 * (i % 2), 1000 + 200 * i);
+ }
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ file->flushToDisk();
+
+ CPPUNIT_ASSERT(file->getCacheSize().bodySize > 0);
+ CPPUNIT_ASSERT(file->getCacheSize().headerSize > 0);
+
+ file->clearCache(HEADER);
+
+ CPPUNIT_ASSERT(file->getCacheSize().bodySize > 0);
+ CPPUNIT_ASSERT(file->getMemFileIO().getCachedSize(BODY) > 0);
+ CPPUNIT_ASSERT_EQUAL(0, (int)file->getCacheSize().headerSize);
+ CPPUNIT_ASSERT_EQUAL(uint64_t(0), file->getMemFileIO().getCachedSize(HEADER));
+
+ file->clearCache(BODY);
+
+ CPPUNIT_ASSERT_EQUAL(0, (int)file->getCacheSize().bodySize);
+ CPPUNIT_ASSERT_EQUAL(uint64_t(0), file->getMemFileIO().getCachedSize(BODY));
+}
+
+
+void
+MemFileTest::testCompactGidCollision()
+{
+ // Feed two puts
+ for (uint32_t i = 0; i < 2; i++) {
+ feedDocument(1234 * i, 1000 + 200 * i);
+ }
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ AutoFlush af(file);
+ const_cast<MemSlot&>((*file)[1]).setGlobalId((*file)[0].getGlobalId());
+
+ CPPUNIT_ASSERT_EQUAL(2, (int)file->getSlotCount());
+
+ {
+ SlotList toRemove(getSlotsToRemove(*file, 1600, 300, 86400));
+ CPPUNIT_ASSERT_EQUAL(0, (int)toRemove.size());
+ file->removeSlots(toRemove);
+ }
+}
+
+void
+MemFileTest::testCompactGidCollisionAndNot()
+{
+ // Feed some puts
+ for (uint32_t i = 0; i < 4; i++) {
+ feedDocument(1234 * (i % 2), 1000 + 200 * i);
+ }
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ AutoFlush af(file);
+ const_cast<MemSlot&>((*file)[2]).setGlobalId((*file)[0].getGlobalId());
+ const_cast<MemSlot&>((*file)[3]).setGlobalId((*file)[1].getGlobalId());
+
+ CPPUNIT_ASSERT_EQUAL(4, (int)file->getSlotCount());
+
+ {
+ SlotList toRemove(getSlotsToRemove(*file, 2000, 300, 86400));
+
+ CPPUNIT_ASSERT_EQUAL(2, (int)toRemove.size());
+ ASSERT_SLOT_EQUAL(&(*file)[0], toRemove[0]);
+ ASSERT_SLOT_EQUAL(&(*file)[1], toRemove[1]);
+ file->removeSlots(toRemove);
+ }
+}
+
+
+void
+MemFileTest::testCompactRemoveDoublePut()
+{
+ // Feed two puts at time 1000 and 1200
+ for (uint32_t i = 0; i < 2; i++) {
+ feedDocument(1234, 1000 + 200 * i);
+ }
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ AutoFlush af(file);
+ CPPUNIT_ASSERT_EQUAL(2, (int)file->getSlotCount());
+
+ {
+ // Not time to collect yet, newest is still revertable
+ SlotList toRemove(getSlotsToRemove(*file, 1300, 300, 86400));
+ CPPUNIT_ASSERT_EQUAL(0, (int)toRemove.size());
+ }
+
+ {
+ SlotList toRemove(getSlotsToRemove(*file, 1600, 300, 86400));
+
+ CPPUNIT_ASSERT_EQUAL(1, (int)toRemove.size());
+ ASSERT_SLOT_EQUAL(&(*file)[0], toRemove[0]);
+ file->removeSlots(toRemove);
+ }
+}
+
+void
+MemFileTest::testCompactPutRemove()
+{
+ document::DocumentId docId = feedDocument(1234, 1000);
+
+ doRemove(docId, Timestamp(1200*1000000), 0);
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ AutoFlush af(file);
+
+ {
+ // Since remove can still be reverted, we can't revert anything.
+ SlotList toRemove(getSlotsToRemove(*file, 1300, 300, 600));
+
+ CPPUNIT_ASSERT_EQUAL(0, (int)toRemove.size());
+ }
+
+ {
+ SlotList toRemove(getSlotsToRemove(*file, 1600, 300, 600));
+
+ CPPUNIT_ASSERT_EQUAL(1, (int)toRemove.size());
+ ASSERT_SLOT_EQUAL(&(*file)[0], toRemove[0]);
+ file->removeSlots(toRemove);
+ }
+
+ {
+ SlotList toRemove(getSlotsToRemove(*file, 1900, 300, 600));
+
+ CPPUNIT_ASSERT_EQUAL(1, (int)toRemove.size());
+ ASSERT_SLOT_EQUAL(&(*file)[0], toRemove[0]);
+ file->removeSlots(toRemove);
+ }
+}
+
+void
+MemFileTest::testCompactCombined()
+{
+ document::DocumentId docId;
+
+ // Feed some puts at time 1000, 1200, 1400, 1600 and 1800 for same doc.
+ for (uint32_t i = 0; i < 5; i++) {
+ docId = feedDocument(1234, 1000 + i * 200);
+ }
+ flush(document::BucketId(16, 4));
+
+ // Now add remove at time 2000.
+ doRemove(docId, Timestamp(2000 * 1000000), 0);
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ AutoFlush af(file);
+ CPPUNIT_ASSERT_EQUAL(6, (int)file->getSlotCount());
+
+ {
+ // Compact all redundant slots that are older than revert period of 300.
+ // This includes 1000, 1200, 1400 and 1600.
+ SlotList toRemove(getSlotsToRemove(*file, 2001, 300, 86400));
+ CPPUNIT_ASSERT_EQUAL(4, (int)toRemove.size());
+ for (int i = 0; i < 4; ++i) {
+ ASSERT_SLOT_EQUAL(&(*file)[i], toRemove[i]);
+ }
+ file->removeSlots(toRemove);
+ }
+}
+
+void
+MemFileTest::testCompactDifferentPuts()
+{
+ document::DocumentId docId;
+
+ // Feed some puts
+ for (uint32_t i = 0; i < 2; i++) {
+ for (uint32_t j = 0; j < 3; j++) {
+ feedDocument(1234 * j, 1000 + (i * 3 + j) * 200);
+ }
+ }
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ AutoFlush af(file);
+ CPPUNIT_ASSERT_EQUAL(6, (int)file->getSlotCount());
+
+ {
+ SlotList toRemove(getSlotsToRemove(*file, 3000, 300, 86400));
+ CPPUNIT_ASSERT_EQUAL(3, (int)toRemove.size());
+
+ for (uint32_t i = 0; i < 3; i++) {
+ bool found = false;
+ for (uint32_t j = 0; j < 3; j++) {
+ if ((*file)[j] == *toRemove[i]) {
+ found = true;
+ }
+ }
+
+ CPPUNIT_ASSERT(found);
+ }
+ file->removeSlots(toRemove);
+ }
+}
+
+void
+MemFileTest::testCompactWithMemFile()
+{
+ // Feed two puts
+ for (uint32_t i = 0; i < 2; i++) {
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 4, 1234, 10, 100));
+
+ doPut(doc, document::BucketId(16, 4), Timestamp((1000 + i * 200)*1000000), 0);
+ }
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ AutoFlush af(file);
+ CPPUNIT_ASSERT_EQUAL(2, (int)file->getSlotCount());
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options)
+ .revertTimePeriod(framework::MicroSecTime(1000))
+ .build());
+
+ getFakeClock()._absoluteTime = framework::MicroSecTime(2000ULL * 1000000);
+
+ CPPUNIT_ASSERT(file->compact());
+ CPPUNIT_ASSERT(!file->compact());
+
+ CPPUNIT_ASSERT_EQUAL(1, (int)file->getSlotCount());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1200 * 1000000), (*file)[0].getTimestamp());
+}
+
+/**
+ * Feed 5 versions of a single document at absolute times 0 through 4 seconds
+ * and run compaction using the provided max document version option.
+ * Revert time/keep remove time options are effectively disabled for this test.
+ * Returns timestamps of all slots that are marked as compactable.
+ */
+std::vector<Types::Timestamp>
+MemFileTest::compactWithVersionLimit(uint32_t maxVersions)
+{
+ document::BucketId bucket(16, 4);
+ std::shared_ptr<Document> doc(
+ createRandomDocumentAtLocation(4, 1234, 10, 100));
+ uint32_t versionLimit = 5;
+ for (uint32_t i = 0; i < versionLimit; ++i) {
+ Timestamp ts(sec(i).getTime());
+ doPut(doc, bucket, ts, 0);
+ }
+ flush(bucket);
+
+ MemFilePtr file(getMemFile(bucket));
+ CPPUNIT_ASSERT_EQUAL(versionLimit, file->getSlotCount());
+
+ framework::MicroSecTime currentTime(sec(versionLimit));
+ MemFileCompactor compactor(
+ currentTime,
+ CompactionOptions()
+ .revertTimePeriod(sec(versionLimit))
+ .keepRemoveTimePeriod(sec(versionLimit))
+ .maxDocumentVersions(maxVersions));
+ auto slots = compactor.getSlotsToRemove(*file);
+ // Convert to timestamps since caller won't have access to actual MemFile.
+ std::vector<Timestamp> timestamps;
+ for (const MemSlot* slot : slots) {
+ timestamps.push_back(slot->getTimestamp());
+ }
+ return timestamps;
+}
+
+void
+MemFileTest::testNoCompactionWhenDocumentVersionsWithinLimit()
+{
+ auto timestamps = compactWithVersionLimit(5);
+ CPPUNIT_ASSERT(timestamps.empty());
+}
+
+void
+MemFileTest::testCompactWhenDocumentVersionsExceedLimit()
+{
+ auto timestamps = compactWithVersionLimit(2);
+ CPPUNIT_ASSERT_EQUAL(size_t(3), timestamps.size());
+ std::vector<Timestamp> expected = {
+ sec(0), sec(1), sec(2)
+ };
+ CPPUNIT_ASSERT_EQUAL(expected, timestamps);
+}
+
+void
+MemFileTest::testCompactLimit1KeepsNewestVersionOnly()
+{
+ auto timestamps = compactWithVersionLimit(1);
+ CPPUNIT_ASSERT_EQUAL(size_t(4), timestamps.size());
+ std::vector<Timestamp> expected = {
+ sec(0), sec(1), sec(2), sec(3)
+ };
+ CPPUNIT_ASSERT_EQUAL(expected, timestamps);
+}
+
+void
+MemFileTest::testCompactionOptionsArePropagatedFromConfig()
+{
+ vespa::config::storage::StorMemfilepersistenceConfigBuilder mfcBuilder;
+ vespa::config::content::PersistenceConfigBuilder pcBuilder;
+
+ pcBuilder.maximumVersionsOfSingleDocumentStored = 12345;
+ pcBuilder.revertTimePeriod = 555;
+ pcBuilder.keepRemoveTimePeriod = 777;
+
+ vespa::config::storage::StorMemfilepersistenceConfig mfc(mfcBuilder);
+ vespa::config::content::PersistenceConfig pc(pcBuilder);
+ Options opts(mfc, pc);
+
+ CPPUNIT_ASSERT_EQUAL(framework::MicroSecTime(555 * 1000000),
+ opts._revertTimePeriod);
+ CPPUNIT_ASSERT_EQUAL(framework::MicroSecTime(777 * 1000000),
+ opts._keepRemoveTimePeriod);
+ CPPUNIT_ASSERT_EQUAL(uint32_t(12345), opts._maxDocumentVersions);
+}
+
+void
+MemFileTest::testZeroDocumentVersionConfigIsCorrected()
+{
+ vespa::config::storage::StorMemfilepersistenceConfigBuilder mfcBuilder;
+ vespa::config::content::PersistenceConfigBuilder pcBuilder;
+
+ pcBuilder.maximumVersionsOfSingleDocumentStored = 0;
+
+ vespa::config::storage::StorMemfilepersistenceConfig mfc(mfcBuilder);
+ vespa::config::content::PersistenceConfig pc(pcBuilder);
+ Options opts(mfc, pc);
+
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), opts._maxDocumentVersions);
+}
+
+void
+MemFileTest::testGetSlotsByTimestamp()
+{
+ for (uint32_t i = 0; i < 10; i++) {
+ feedDocument(i, 1000 + i);
+ }
+ flush(document::BucketId(16, 4));
+
+ std::vector<Timestamp> timestamps;
+ timestamps.push_back(Timestamp(999 * 1000000));
+ timestamps.push_back(Timestamp(1001 * 1000000));
+ timestamps.push_back(Timestamp(1002 * 1000000));
+ timestamps.push_back(Timestamp(1007 * 1000000));
+ timestamps.push_back(Timestamp(1100 * 1000000));
+ std::vector<const MemSlot*> slots;
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ file->getSlotsByTimestamp(timestamps, slots);
+ CPPUNIT_ASSERT_EQUAL(std::size_t(3), slots.size());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1001 * 1000000), slots[0]->getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1002 * 1000000), slots[1]->getTimestamp());
+ CPPUNIT_ASSERT_EQUAL(Timestamp(1007 * 1000000), slots[2]->getTimestamp());
+}
+
+void
+MemFileTest::testEnsureCached()
+{
+ // Feed some puts
+ for (uint32_t i = 0; i < 5; i++) {
+ feedDocument(i, 1000 + i * 200, 600, 600, 600);
+ }
+ flush(document::BucketId(16, 4));
+
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options).maximumReadThroughGap(512).build());
+ env()._cache.clear();
+
+ {
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ CPPUNIT_ASSERT(file.get());
+ CPPUNIT_ASSERT_EQUAL(5, (int)file->getSlotCount());
+
+ file->ensureDocumentIdCached((*file)[1]);
+
+ for (std::size_t i = 0; i < file->getSlotCount(); ++i) {
+ if (i == 1) {
+ CPPUNIT_ASSERT(file->documentIdAvailable((*file)[i]));
+ } else {
+ CPPUNIT_ASSERT(!file->documentIdAvailable((*file)[i]));
+ }
+ CPPUNIT_ASSERT(!file->partAvailable((*file)[i], BODY));
+ }
+ }
+
+ env()._cache.clear();
+
+ {
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ file->ensureDocumentCached((*file)[2], true);
+
+ for (std::size_t i = 0; i < file->getSlotCount(); ++i) {
+ if (i == 2) {
+ CPPUNIT_ASSERT(file->documentIdAvailable((*file)[i]));
+ CPPUNIT_ASSERT(file->partAvailable((*file)[i], HEADER));
+ } else {
+ CPPUNIT_ASSERT(!file->documentIdAvailable((*file)[i]));
+ CPPUNIT_ASSERT(!file->partAvailable((*file)[i], HEADER));
+ }
+ CPPUNIT_ASSERT(!file->partAvailable((*file)[i], BODY));
+ }
+ }
+
+ env()._cache.clear();
+
+ {
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+
+ file->ensureDocumentCached((*file)[3], false);
+
+ for (std::size_t i = 0; i < file->getSlotCount(); ++i) {
+ if (i == 3) {
+ CPPUNIT_ASSERT(file->documentIdAvailable((*file)[i]));
+ CPPUNIT_ASSERT(file->partAvailable((*file)[i], HEADER));
+ CPPUNIT_ASSERT(file->partAvailable((*file)[i], BODY));
+ } else {
+ CPPUNIT_ASSERT(!file->documentIdAvailable((*file)[i]));
+ CPPUNIT_ASSERT(!file->partAvailable((*file)[i], HEADER));
+ CPPUNIT_ASSERT(!file->partAvailable((*file)[i], BODY));
+ }
+ }
+ }
+
+ env()._cache.clear();
+
+ {
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+
+ std::vector<Timestamp> ts;
+ for (int i = 2; i < 5; ++i) {
+ ts.push_back((*file)[i].getTimestamp());
+ }
+
+ file->ensureDocumentCached(ts, false);
+
+ for (std::size_t i = 0; i < file->getSlotCount(); ++i) {
+ if (i > 1 && i < 5) {
+ CPPUNIT_ASSERT(file->documentIdAvailable((*file)[i]));
+ CPPUNIT_ASSERT(file->partAvailable((*file)[i], HEADER));
+ CPPUNIT_ASSERT(file->partAvailable((*file)[i], BODY));
+ } else {
+ CPPUNIT_ASSERT(!file->documentIdAvailable((*file)[i]));
+ CPPUNIT_ASSERT(!file->partAvailable((*file)[i], HEADER));
+ CPPUNIT_ASSERT(!file->partAvailable((*file)[i], BODY));
+ }
+ }
+ }
+
+ env()._cache.clear();
+
+ {
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+
+ file->ensureHeaderBlockCached();
+
+ for (std::size_t i = 0; i < file->getSlotCount(); ++i) {
+ CPPUNIT_ASSERT(file->documentIdAvailable((*file)[i]));
+ CPPUNIT_ASSERT(file->partAvailable((*file)[i], HEADER));
+ CPPUNIT_ASSERT(!file->partAvailable((*file)[i], BODY));
+ }
+ }
+
+ env()._cache.clear();
+
+ {
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+
+ file->ensureBodyBlockCached();
+
+ for (std::size_t i = 0; i < file->getSlotCount(); ++i) {
+ CPPUNIT_ASSERT(file->documentIdAvailable((*file)[i]));
+ CPPUNIT_ASSERT(file->partAvailable((*file)[i], HEADER));
+ CPPUNIT_ASSERT(file->partAvailable((*file)[i], BODY));
+ }
+ }
+}
+
+void
+MemFileTest::testResizeToFreeSpace()
+{
+ /**
+ * This test tests that files are resized to a smaller size when they need
+ * to be. This should happen during a call to flushToDisk() in MemFile,
+ * which is either dirty or if passed flag to check even if clean. (Which
+ * the integrity checker cycle uses). A clean file is used for testing to
+ * ensure that no part of the code only works for dirty files. This test
+ * only test for the case where body block is too large. The real
+ * implementation here will be in the flushUpdatesToFile() function for the
+ * given file formats. (VersionSerializer's) If more cases wants to be
+ * tested add those as unit tests for the versionserializers themselves.
+ */
+
+ // Create a test bucket to test with.
+ BucketId bucket(16, 0xa);
+ createTestBucket(bucket, 0);
+
+ off_t file_size =
+ ((SimpleMemFileIOBuffer&)getMemFile(bucket)->getMemFileIO()).
+ getFileHandle().getFileSize();
+
+ // Clear cache so we can manually modify backing file to increase the
+ // size of it.
+ FileSpecification file(getMemFile(bucket)->getFile());
+ env()._cache.clear();
+ {
+ // Extend file to 1 MB, which should create an excessively large
+ // body block such that file should be resized to be smaller
+ vespalib::LazyFile fileHandle(file.getPath(), 0);
+ fileHandle.write("foobar", 6, 2 * 1024 * 1024 - 6);
+ }
+ MemFilePtr memFile(getMemFile(bucket));
+ memFile->flushToDisk(CHECK_NON_DIRTY_FILE_FOR_SPACE);
+ CPPUNIT_ASSERT_EQUAL(file_size,
+ ((SimpleMemFileIOBuffer&)memFile->getMemFileIO()).
+ getFileHandle().getFileSize());
+}
+
+namespace {
+
+const vespalib::LazyFile&
+getFileHandle(const MemFile& mf1)
+{
+ return dynamic_cast<const SimpleMemFileIOBuffer&>(
+ mf1.getMemFileIO()).getFileHandle();
+}
+
+const LoggingLazyFile&
+getLoggerFile(const MemFile& file)
+{
+ return dynamic_cast<const LoggingLazyFile&>(getFileHandle(file));
+}
+
+}
+
+void
+MemFileTest::testNoFileWriteOnNoOpCompaction()
+{
+ BucketId bucket(16, 4);
+ env()._lazyFileFactory = std::unique_ptr<Environment::LazyFileFactory>(
+ new LoggingLazyFile::Factory());
+
+ // Feed some unique puts, none of which can be compacted away.
+ for (uint32_t i = 0; i < 2; i++) {
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 4, i, 10, 100));
+
+ doPut(doc, bucket, Timestamp((1000 + i * 200)*1000000), 0);
+ }
+ flush(bucket);
+
+ MemFilePtr file(getMemFile(bucket));
+
+ size_t opsBeforeFlush = getLoggerFile(*file).getOperationCount();
+ file->flushToDisk(CHECK_NON_DIRTY_FILE_FOR_SPACE);
+ size_t opsAfterFlush = getLoggerFile(*file).getOperationCount();
+
+ // Disk should not have been touched, since no slots have been
+ // compacted away.
+ if (opsBeforeFlush != opsAfterFlush) {
+ std::cerr << "\n" << getLoggerFile(*file).toString() << "\n";
+ }
+ CPPUNIT_ASSERT_EQUAL(opsBeforeFlush, opsAfterFlush);
+}
+
+void
+MemFileTest::testAddSlotWhenDiskFull()
+{
+ {
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ AutoFlush af(file);
+ {
+ // Add a dummy-slot that can later be removed
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ file->addPutSlot(*doc, Timestamp(1001));
+ }
+ }
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ AutoFlush af(file);
+ PartitionMonitor* mon = env().getDirectory().getPartition().getMonitor();
+ // Set disk to 99% full
+ mon->setStatOncePolicy();
+ mon->setMaxFillness(.98f);
+ mon->overrideRealStat(512, 100000, 99000);
+ CPPUNIT_ASSERT(mon->isFull());
+
+ // Test that addSlot with a non-persisted Put fails
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ try {
+ file->addPutSlot(*doc, Timestamp(10003));
+ CPPUNIT_ASSERT(false);
+ } catch (vespalib::IoException& e) {
+ CPPUNIT_ASSERT_EQUAL(vespalib::IoException::NO_SPACE, e.getType());
+ }
+ }
+
+ // Slots with valid header and body locations should also
+ // not fail, as these are added when the file is loaded
+ {
+ // Just steal parts from existing slot to ensure they're persisted
+ const MemSlot* existing = file->getSlotAtTime(Timestamp(1001));
+
+ MemSlot slot(existing->getGlobalId(),
+ Timestamp(1005),
+ existing->getLocation(HEADER),
+ existing->getLocation(BODY),
+ IN_USE,
+ 0x1234);
+ file->addSlot(slot);
+ }
+
+ // Removes should not fail when disk is full
+ {
+ file->addRemoveSlot(*file->getSlotAtTime(Timestamp(1001)), Timestamp(1003));
+ }
+}
+
+void
+MemFileTest::testGetSerializedSize() {
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 4, 1234, 1024, 1024));
+
+ std::string val("Header");
+ doc->setValue(doc->getField("hstringval"),
+ document::StringFieldValue(val));
+
+ doPut(doc, document::BucketId(16, 4), framework::MicroSecTime(1000));
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+ file->ensureBodyBlockCached();
+ const MemSlot* slot = file->getSlotAtTime(framework::MicroSecTime(1000));
+ CPPUNIT_ASSERT(slot != 0);
+
+ vespalib::nbostream serializedHeader;
+ doc->serializeHeader(serializedHeader);
+
+ vespalib::nbostream serializedBody;
+ doc->serializeBody(serializedBody);
+
+ CPPUNIT_ASSERT_EQUAL(uint32_t(serializedHeader.size()),
+ file->getSerializedSize(*slot, HEADER));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(serializedBody.size()),
+ file->getSerializedSize(*slot, BODY));
+}
+
+void
+MemFileTest::testGetBucketInfo()
+{
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 4, 1234, 100, 100));
+ doc->setValue(doc->getField("content"),
+ document::StringFieldValue("foo"));
+ document::Document::SP doc2(createRandomDocumentAtLocation(
+ 4, 1235, 100, 100));
+ doc2->setValue(doc->getField("content"),
+ document::StringFieldValue("bar"));
+
+ doPut(doc, document::BucketId(16, 4), framework::MicroSecTime(1000));
+ flush(document::BucketId(16, 4));
+
+ doPut(doc2, document::BucketId(16, 4), framework::MicroSecTime(1001));
+ flush(document::BucketId(16, 4));
+
+ // Do remove which should only add a single meta entry
+ doRemove(doc->getId(), Timestamp(1002), 0);
+ flush(document::BucketId(16, 4));
+
+ MemFilePtr file(getMemFile(document::BucketId(16, 4)));
+
+ CPPUNIT_ASSERT_EQUAL(3u, file->getSlotCount());
+ uint32_t maxHeaderExtent = (*file)[1].getLocation(HEADER)._pos
+ + (*file)[1].getLocation(HEADER)._size;
+ uint32_t maxBodyExtent = (*file)[1].getLocation(BODY)._pos
+ + (*file)[1].getLocation(BODY)._size;
+
+ uint32_t wantedUsedSize = 64 + 40*3 + maxHeaderExtent + maxBodyExtent;
+ BucketInfo info = file->getBucketInfo();
+ CPPUNIT_ASSERT_EQUAL(1u, info.getDocumentCount());
+ CPPUNIT_ASSERT_EQUAL(3u, info.getEntryCount());
+ CPPUNIT_ASSERT_EQUAL(wantedUsedSize, info.getUsedSize());
+ uint32_t wantedUniqueSize = (*file)[1].getLocation(HEADER)._size
+ + (*file)[1].getLocation(BODY)._size;
+ CPPUNIT_ASSERT_EQUAL(wantedUniqueSize, info.getDocumentSize());
+}
+
+void
+MemFileTest::testCopySlotsPreservesLocationSharing()
+{
+ document::BucketId bucket(16, 4);
+ // Feed two puts to same document (identical seed). These should not
+ // share any blocks. Note: implicit sec -> microsec conversion.
+ feedDocument(1234, 1000); // slot 0
+ auto docId = feedDocument(1234, 1001); // slot 1
+ // Update only header of last version of document. This should share
+ // slot body block 2 with that slot 1.
+ auto update = createHeaderUpdate(docId, document::IntFieldValue(5678));
+ doUpdate(bucket, update, Timestamp(1002 * 1000000), 0);
+ // Feed a remove for doc in slot 2. This should share the header block of
+ // slot 3 with the newest document in slot 2.
+ doRemove(docId, Timestamp(1003 * 1000000), 0);
+ flush(bucket);
+
+ {
+ MemFilePtr src(getMemFile(document::BucketId(16, 4)));
+ MemFilePtr dest(getMemFile(document::BucketId(17, 4)));
+ std::vector<Timestamp> timestamps {
+ Timestamp(1000 * 1000000),
+ Timestamp(1001 * 1000000),
+ Timestamp(1002 * 1000000),
+ Timestamp(1003 * 1000000)
+ };
+ std::vector<const MemSlot*> slots {
+ src->getSlotAtTime(Timestamp(1000 * 1000000)),
+ src->getSlotAtTime(Timestamp(1001 * 1000000)),
+ src->getSlotAtTime(Timestamp(1002 * 1000000)),
+ src->getSlotAtTime(Timestamp(1003 * 1000000))
+ };
+ dest->copySlotsFrom(*src, slots);
+ dest->flushToDisk();
+ CPPUNIT_ASSERT_EQUAL(uint32_t(4), dest->getSlotCount());
+
+ DataLocation header[4];
+ DataLocation body[4];
+ for (int i = 0; i < 4; ++i) {
+ const MemSlot* slot = dest->getSlotAtTime(timestamps[i]);
+ header[i] = slot->getLocation(HEADER);
+ body[i] = slot->getLocation(BODY);
+ }
+ CPPUNIT_ASSERT(!(header[0] == header[1]));
+
+ CPPUNIT_ASSERT_EQUAL(body[2], body[1]);
+ CPPUNIT_ASSERT_EQUAL(header[3], header[2]);
+ }
+}
+
+void
+MemFileTest::testFlushingToNonExistingFileAlwaysRunsCompaction()
+{
+ document::BucketId bucket(16, 4);
+
+ setMaxDocumentVersionsOption(1);
+ feedSameDocNTimes(10);
+ flush(bucket);
+
+ // Max version limit is 1, flushing should have compacted it down.
+ MemFilePtr file(getMemFile(bucket));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), file->getSlotCount());
+}
+
+void
+MemFileTest::testOrderDocSchemeDocumentsCanBeAddedToFile()
+{
+ // Quick explanation of the esoteric and particular values chosen below:
+ // orderdoc mangles the MSB of the bucket ID based on the document ID's
+ // ordering parameters and thus its bucket cannot be directly deduced from
+ // the generated GID. The values given here specify a document whose GID
+ // bits differ from those generated by the document and where a GID-only
+ // bucket ownership check would fail (nuking the node with an assertion).
+ // We have to make sure cases do not trigger false positives.
+ document::BucketId bucket(0x84000000ee723751);
+ auto doc = createDocument("the quick red fox trips over a hedge",
+ "orderdoc(3,1):storage_test:group1:9:9");
+ doPut(std::shared_ptr<Document>(std::move(doc)),
+ bucket,
+ Timestamp(1000000 * 1234));
+ flush(bucket);
+
+ MemFilePtr file(getMemFile(bucket));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), file->getSlotCount());
+ // Ideally we'd test the failure case as well, but that'd require framework
+ // support for death tests.
+}
+
+} // memfile
+} // storage
diff --git a/memfilepersistence/src/tests/spi/memfiletestutils.cpp b/memfilepersistence/src/tests/spi/memfiletestutils.cpp
new file mode 100644
index 00000000000..1e882ccbe6b
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/memfiletestutils.cpp
@@ -0,0 +1,455 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+
+#include <vespa/document/datatype/documenttype.h>
+#include <vespa/memfilepersistence/spi/memfilepersistenceprovider.h>
+#include <tests/spi/memfiletestutils.h>
+#include <tests/spi/simulatedfailurefile.h>
+#include <vespa/memfilepersistence/memfile/memfilecache.h>
+#include <vespa/storageframework/defaultimplementation/memory/simplememorylogic.h>
+#include <sys/time.h>
+
+using document::DocumentType;
+
+namespace storage {
+namespace memfile {
+
+namespace {
+ spi::LoadType defaultLoadType(0, "default");
+}
+
+namespace {
+ vdstestlib::DirConfig initialize(uint32_t numDisks) {
+ system(vespalib::make_string("rm -rf vdsroot").c_str());
+ for (uint32_t i = 0; i < numDisks; i++) {
+ system(vespalib::make_string("mkdir -p vdsroot/disks/d%d", i).c_str());
+ }
+ vdstestlib::DirConfig config(getStandardConfig(true));
+ return config;
+ }
+
+ template<typename T>
+ struct ConfigReader : public T::Subscriber
+ {
+ T config;
+
+ ConfigReader(const std::string& configId) {
+ T::subscribe(configId, *this);
+ }
+ void configure(const T& c) { config = c; }
+ };
+}
+
+MemFileTestEnvironment::MemFileTestEnvironment(
+ uint32_t numDisks,
+ framework::ComponentRegister& reg,
+ const document::DocumentTypeRepo& repo)
+ : _config(initialize(numDisks)),
+ _provider(reg, _config.getConfigId())
+{
+ _provider.setDocumentRepo(repo);
+ _provider.getPartitionStates();
+}
+
+MemFileTestUtils::MemFileTestUtils()
+{
+}
+
+MemFileTestUtils::~MemFileTestUtils()
+{
+}
+
+void
+MemFileTestUtils::setupDisks(uint32_t numDisks) {
+ tearDown();
+ _componentRegister.reset(
+ new framework::defaultimplementation::ComponentRegisterImpl);
+ _clock.reset(new FakeClock);
+ _componentRegister->setClock(*_clock);
+ _memoryManager.reset(
+ new framework::defaultimplementation::MemoryManager(
+ framework::defaultimplementation::AllocationLogic::UP(
+ new framework::defaultimplementation::SimpleMemoryLogic(
+ *_clock, 1024 * 1024 * 1024))));
+ _componentRegister->setMemoryManager(*_memoryManager);
+ _env.reset(new MemFileTestEnvironment(numDisks,
+ *_componentRegister,
+ *getTypeRepo()));
+}
+
+Environment&
+MemFileTestUtils::env()
+{
+ return static_cast<MemFilePersistenceProvider&>(
+ getPersistenceProvider()).getEnvironment();
+}
+
+MemFilePersistenceProvider&
+MemFileTestUtils::getPersistenceProvider()
+{
+ return _env->_provider;
+}
+
+MemFilePersistenceThreadMetrics&
+MemFileTestUtils::getMetrics()
+{
+ return getPersistenceProvider().getMetrics();
+}
+
+std::string
+MemFileTestUtils::getMemFileStatus(const document::BucketId& id,
+ uint32_t disk)
+{
+ MemFilePtr file(getMemFile(id, disk));
+ std::ostringstream ost;
+ ost << id << ": " << file->getSlotCount() << "," << file->getDisk();
+ return ost.str();
+}
+
+std::string
+MemFileTestUtils::getModifiedBuckets()
+{
+ spi::BucketIdListResult result(
+ getPersistenceProvider().getModifiedBuckets());
+ const spi::BucketIdListResult::List& list(result.getList());
+ std::ostringstream ss;
+ for (size_t i = 0; i < list.size(); ++i) {
+ if (i != 0) {
+ ss << ",";
+ }
+ ss << std::hex << list[i].getId();
+ }
+ return ss.str();
+}
+
+MemFilePtr
+MemFileTestUtils::getMemFile(const document::BucketId& id, uint16_t disk)
+{
+ return env()._cache.get(id, env(), env().getDirectory(disk));
+}
+
+spi::Result
+MemFileTestUtils::flush(const document::BucketId& id, uint16_t disk)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ return getPersistenceProvider().flush(
+ spi::Bucket(id, spi::PartitionId(disk)), context);
+}
+
+document::Document::SP
+MemFileTestUtils::doPutOnDisk(
+ uint16_t disk,
+ uint32_t location,
+ Timestamp timestamp,
+ uint32_t minSize,
+ uint32_t maxSize)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ location, timestamp.getTime(), minSize, maxSize));
+ getPersistenceProvider().put(
+ spi::Bucket(document::BucketId(16, location), spi::PartitionId(disk)),
+ spi::Timestamp(timestamp.getTime()),
+ doc,
+ context);
+ return doc;
+}
+
+bool
+MemFileTestUtils::doRemoveOnDisk(
+ uint16_t disk,
+ const document::BucketId& bucketId,
+ const document::DocumentId& docId,
+ Timestamp timestamp,
+ OperationHandler::RemoveType persistRemove)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ if (persistRemove == OperationHandler::PERSIST_REMOVE_IF_FOUND) {
+ spi::RemoveResult result = getPersistenceProvider().removeIfFound(
+ spi::Bucket(bucketId, spi::PartitionId(disk)),
+ spi::Timestamp(timestamp.getTime()),
+ docId,
+ context);
+ return result.wasFound();
+ }
+ spi::RemoveResult result = getPersistenceProvider().remove(
+ spi::Bucket(bucketId, spi::PartitionId(disk)),
+ spi::Timestamp(timestamp.getTime()),
+ docId,
+ context);
+
+ return result.wasFound();
+}
+
+bool
+MemFileTestUtils::doUnrevertableRemoveOnDisk(
+ uint16_t disk,
+ const document::BucketId& bucketId,
+ const DocumentId& docId,
+ Timestamp timestamp)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ spi::RemoveResult result =
+ getPersistenceProvider().remove(
+ spi::Bucket(bucketId, spi::PartitionId(disk)),
+ spi::Timestamp(timestamp.getTime()),
+ docId, context);
+
+ return result.wasFound();
+}
+
+spi::GetResult
+MemFileTestUtils::doGetOnDisk(
+ uint16_t disk,
+ const document::BucketId& bucketId,
+ const document::DocumentId& docId,
+ const document::FieldSet& fields)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ return getPersistenceProvider().get(
+ spi::Bucket(bucketId, spi::PartitionId(disk)),
+ fields, docId, context);
+}
+
+document::DocumentUpdate::SP
+MemFileTestUtils::createBodyUpdate(
+ const document::DocumentId& docId,
+ const document::FieldValue& updateValue)
+{
+ const DocumentType*
+ docType(getTypeRepo()->getDocumentType("testdoctype1"));
+ document::DocumentUpdate::SP update(
+ new document::DocumentUpdate(*docType, docId));
+ std::shared_ptr<document::AssignValueUpdate> assignUpdate(
+ new document::AssignValueUpdate(updateValue));
+ document::FieldUpdate fieldUpdate(docType->getField("content"));
+ fieldUpdate.addUpdate(*assignUpdate);
+ update->addUpdate(fieldUpdate);
+ return update;
+}
+
+document::DocumentUpdate::SP
+MemFileTestUtils::createHeaderUpdate(
+ const document::DocumentId& docId,
+ const document::FieldValue& updateValue)
+{
+ const DocumentType*
+ docType(getTypeRepo()->getDocumentType("testdoctype1"));
+ document::DocumentUpdate::SP update(
+ new document::DocumentUpdate(*docType, docId));
+ std::shared_ptr<document::AssignValueUpdate> assignUpdate(
+ new document::AssignValueUpdate(updateValue));
+ document::FieldUpdate fieldUpdate(docType->getField("headerval"));
+ fieldUpdate.addUpdate(*assignUpdate);
+ update->addUpdate(fieldUpdate);
+ return update;
+}
+
+void
+MemFileTestUtils::doPut(const document::Document::SP& doc,
+ Timestamp time,
+ uint16_t disk,
+ uint16_t usedBits)
+{
+ document::BucketId bucket(
+ getBucketIdFactory().getBucketId(doc->getId()));
+ bucket.setUsedBits(usedBits);
+ doPut(doc, bucket, time, disk);
+}
+
+void
+MemFileTestUtils::doPut(const document::Document::SP& doc,
+ document::BucketId bid,
+ Timestamp time,
+ uint16_t disk)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ getPersistenceProvider().put(spi::Bucket(bid, spi::PartitionId(disk)),
+ spi::Timestamp(time.getTime()), doc, context);
+}
+
+spi::UpdateResult
+MemFileTestUtils::doUpdate(document::BucketId bid,
+ const document::DocumentUpdate::SP& update,
+ Timestamp time,
+ uint16_t disk)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ return getPersistenceProvider().update(
+ spi::Bucket(bid, spi::PartitionId(disk)),
+ spi::Timestamp(time.getTime()), update, context);
+}
+
+void
+MemFileTestUtils::doRemove(const document::DocumentId& id, Timestamp time,
+ uint16_t disk, bool unrevertableRemove,
+ uint16_t usedBits)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ document::BucketId bucket(getBucketIdFactory().getBucketId(id));
+ bucket.setUsedBits(usedBits);
+
+ if (unrevertableRemove) {
+ getPersistenceProvider().remove(
+ spi::Bucket(bucket, spi::PartitionId(disk)),
+ spi::Timestamp(time.getTime()),
+ id, context);
+ } else {
+ spi::RemoveResult result = getPersistenceProvider().removeIfFound(
+ spi::Bucket(bucket, spi::PartitionId(disk)),
+ spi::Timestamp(time.getTime()),
+ id, context);
+
+ if (!result.wasFound()) {
+ throw vespalib::IllegalStateException(
+ "Attempted to remove non-existing doc " + id.toString(),
+ VESPA_STRLOC);
+ }
+ }
+}
+
+void
+MemFileTestUtils::copyHeader(document::Document& dest,
+ const document::Document& src)
+{
+ // FIXME(vekterli): temporary solution while we don't have
+ // fieldset pruning functionality in Document.
+ //dest.setHeaderPtr(src.getHeaderPtr());
+ vespalib::nbostream originalBodyStream;
+ dest.serializeBody(originalBodyStream);
+
+ vespalib::nbostream headerStream;
+ src.serializeHeader(headerStream);
+ document::ByteBuffer hbuf(headerStream.peek(), headerStream.size());
+ dest.deserializeHeader(*getTypeRepo(), hbuf);
+ // deserializeHeader clears fields struct, so have to re-set body
+ document::ByteBuffer bbuf(originalBodyStream.peek(),
+ originalBodyStream.size());
+ dest.deserializeBody(*getTypeRepo(), bbuf);
+}
+
+void
+MemFileTestUtils::copyBody(document::Document& dest,
+ const document::Document& src)
+{
+ // FIXME(vekterli): temporary solution while we don't have
+ // fieldset pruning functionality in Document.
+ //dest.setBodyPtr(src.getBodyPtr());
+ vespalib::nbostream stream;
+ src.serializeBody(stream);
+ document::ByteBuffer buf(stream.peek(), stream.size());
+ dest.deserializeBody(*getTypeRepo(), buf);
+}
+
+void
+MemFileTestUtils::clearBody(document::Document& doc)
+{
+ // FIXME(vekterli): temporary solution while we don't have
+ // fieldset pruning functionality in Document.
+ //doc->getBody().clear();
+ vespalib::nbostream stream;
+ doc.serializeHeader(stream);
+ doc.deserialize(*getTypeRepo(), stream);
+}
+
+void
+MemFileTestUtils::createTestBucket(const document::BucketId& bucket,
+ uint16_t disk)
+{
+
+ uint32_t opsPerType = 2;
+ uint32_t numberOfLocations = 2;
+ uint32_t minDocSize = 0;
+ uint32_t maxDocSize = 128;
+
+ for (uint32_t useHeaderOnly = 0; useHeaderOnly < 2; ++useHeaderOnly) {
+ bool headerOnly = (useHeaderOnly == 1);
+ for (uint32_t optype=0; optype < 4; ++optype) {
+ for (uint32_t i=0; i<opsPerType; ++i) {
+ uint32_t seed = useHeaderOnly * 10000 + optype * 1000 + i + 1;
+ uint64_t location = (seed % numberOfLocations);
+ location <<= 32;
+ location += (bucket.getRawId() & 0xffffffff);
+ document::Document::SP doc(
+ createRandomDocumentAtLocation(
+ location, seed, minDocSize, maxDocSize));
+ if (headerOnly) {
+ clearBody(*doc);
+ }
+ doPut(doc, Timestamp(seed), disk, bucket.getUsedBits());
+ if (optype == 0) { // Regular put
+ } else if (optype == 1) { // Overwritten later in time
+ Document::SP doc2(new Document(*doc));
+ doc2->setValue(doc2->getField("content"),
+ document::StringFieldValue("overwritten"));
+ doPut(doc2, Timestamp(seed + 500),
+ disk, bucket.getUsedBits());
+ } else if (optype == 2) { // Removed
+ doRemove(doc->getId(), Timestamp(seed + 500), disk, false,
+ bucket.getUsedBits());
+ } else if (optype == 3) { // Unrevertable removed
+ doRemove(doc->getId(), Timestamp(seed), disk, true,
+ bucket.getUsedBits());
+ }
+ }
+ }
+ }
+ flush(bucket, disk);
+}
+
+void
+MemFileTestUtils::simulateIoErrorsForSubsequentlyOpenedFiles(
+ const IoErrors& errs)
+{
+ std::unique_ptr<SimulatedFailureLazyFile::Factory> factory(
+ new SimulatedFailureLazyFile::Factory);
+ factory->setWriteOpsBeforeFailure(errs._afterWrites);
+ factory->setReadOpsBeforeFailure(errs._afterReads);
+ env()._lazyFileFactory = std::move(factory);
+}
+
+void
+MemFileTestUtils::unSimulateIoErrorsForSubsequentlyOpenedFiles()
+{
+ env()._lazyFileFactory = std::unique_ptr<Environment::LazyFileFactory>(
+ new DefaultLazyFileFactory(0));
+}
+
+std::string
+MemFileTestUtils::stringifyFields(const document::Document& doc) const
+{
+ using namespace document;
+ std::vector<std::string> output;
+ const StructFieldValue& fields(doc.getFields());
+ for (StructFieldValue::const_iterator
+ it(fields.begin()), e(fields.end());
+ it != e; ++it)
+ {
+ std::ostringstream ss;
+ const Field& f(it.field());
+ ss << f.getName() << ": ";
+ FieldValue::UP val(fields.getValue(f));
+ if (val.get()) {
+ ss << val->toString();
+ } else {
+ ss << "(null)";
+ }
+ output.push_back(ss.str());
+ }
+ std::ostringstream ret;
+ std::sort(output.begin(), output.end());
+ std::copy(output.begin(), output.end(),
+ std::ostream_iterator<std::string>(ret, "\n"));
+ return ret.str();
+}
+
+} // memfile
+} // storage
diff --git a/memfilepersistence/src/tests/spi/memfiletestutils.h b/memfilepersistence/src/tests/spi/memfiletestutils.h
new file mode 100644
index 00000000000..a13b902a214
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/memfiletestutils.h
@@ -0,0 +1,294 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * \class storage::memfile::MemFileTestUtils
+ * \ingroup memfile
+ *
+ * \brief Utilities for unit tests of the MemFile layer.
+ *
+ * The memfile layer typically needs a MemFileEnvironment object that must be
+ * set up. This class creates such an object to be used by unit tests. Other
+ * utilities useful for only MemFile testing can be added here too.
+ */
+
+#pragma once
+
+#include <vespa/memfilepersistence/memfile/memfilecache.h>
+#include <tests/testhelper.h>
+#include <vespa/persistence/spi/persistenceprovider.h>
+#include <vespa/memfilepersistence/spi/memfilepersistenceprovider.h>
+#include <vespa/document/base/testdocman.h>
+#include <vespa/storageframework/defaultimplementation/clock/realclock.h>
+#include <vespa/storageframework/defaultimplementation/component/componentregisterimpl.h>
+#include <vespa/storageframework/defaultimplementation/memory/memorymanager.h>
+
+namespace storage {
+namespace memfile {
+
+struct FakeClock : public framework::Clock {
+public:
+ typedef std::unique_ptr<FakeClock> UP;
+
+ framework::MicroSecTime _absoluteTime;
+
+ FakeClock() {};
+
+ virtual void addSecondsToTime(uint32_t nr) {
+ _absoluteTime += framework::MicroSecTime(nr * uint64_t(1000000));
+ }
+
+ virtual framework::MicroSecTime getTimeInMicros() const {
+ return _absoluteTime;
+ }
+ virtual framework::MilliSecTime getTimeInMillis() const {
+ return getTimeInMicros().getMillis();
+ }
+ virtual framework::SecondTime getTimeInSeconds() const {
+ return getTimeInMicros().getSeconds();
+ }
+};
+
+struct MemFileTestEnvironment {
+ MemFileTestEnvironment(uint32_t numDisks,
+ framework::ComponentRegister& reg,
+ const document::DocumentTypeRepo& repo);
+
+ vdstestlib::DirConfig _config;
+ MemFilePersistenceProvider _provider;
+};
+
+class MemFileTestUtils : public Types, public document::TestDocMan, public CppUnit::TestFixture {
+private:
+ // This variables are kept in test class. Instances that needs to be
+ // unique per test needs to be setup in setupDisks and cleared in
+ // tearDown
+ document::BucketIdFactory _bucketIdFactory;
+ framework::defaultimplementation::ComponentRegisterImpl::UP _componentRegister;
+ FakeClock::UP _clock;
+ framework::defaultimplementation::MemoryManager::UP _memoryManager;
+ std::unique_ptr<MemFileTestEnvironment> _env;
+
+public:
+ MemFileTestUtils();
+ virtual ~MemFileTestUtils();
+
+ void setupDisks(uint32_t disks);
+
+ void tearDown() {
+ _env.reset();
+ _componentRegister.reset();
+ _memoryManager.reset();
+ _clock.reset();
+ }
+
+ std::string getMemFileStatus(const document::BucketId& id, uint32_t disk = 0);
+
+ std::string getModifiedBuckets();
+
+ /**
+ Flushes all cached data to disk and updates the bucket database accordingly.
+ */
+ void flush();
+
+ FakeClock& getFakeClock() { return *_clock; }
+
+ spi::Result flush(const document::BucketId& id, uint16_t disk = 0);
+
+ MemFilePersistenceProvider& getPersistenceProvider();
+
+ MemFilePtr getMemFile(const document::BucketId& id, uint16_t disk = 0);
+
+ Environment& env();
+
+ MemFilePersistenceThreadMetrics& getMetrics();
+
+ MemFileTestEnvironment& getEnv() { return *_env; }
+
+ /**
+ Performs a put to the given disk.
+ Returns the document that was inserted.
+ */
+ document::Document::SP doPutOnDisk(
+ uint16_t disk,
+ uint32_t location,
+ Timestamp timestamp,
+ uint32_t minSize = 0,
+ uint32_t maxSize = 128);
+
+ document::Document::SP doPut(
+ uint32_t location,
+ Timestamp timestamp,
+ uint32_t minSize = 0,
+ uint32_t maxSize = 128)
+ { return doPutOnDisk(0, location, timestamp, minSize, maxSize); }
+
+ /**
+ Performs a remove to the given disk.
+ Returns the new doccount if document was removed, or -1 if not found.
+ */
+ bool doRemoveOnDisk(
+ uint16_t disk,
+ const document::BucketId& bid,
+ const document::DocumentId& id,
+ Timestamp timestamp,
+ OperationHandler::RemoveType persistRemove);
+
+ bool doRemove(
+ const document::BucketId& bid,
+ const document::DocumentId& id,
+ Timestamp timestamp,
+ OperationHandler::RemoveType persistRemove) {
+ return doRemoveOnDisk(0, bid, id, timestamp, persistRemove);
+ }
+
+ bool doUnrevertableRemoveOnDisk(uint16_t disk,
+ const document::BucketId& bid,
+ const DocumentId& id,
+ Timestamp timestamp);
+
+ bool doUnrevertableRemove(const document::BucketId& bid,
+ const DocumentId& id,
+ Timestamp timestamp)
+ {
+ return doUnrevertableRemoveOnDisk(0, bid, id, timestamp);
+ }
+
+ virtual const document::BucketIdFactory& getBucketIdFactory() const
+ { return _bucketIdFactory; }
+
+ document::BucketIdFactory& getBucketIdFactory()
+ { return _bucketIdFactory; }
+
+ /**
+ * Do a remove toward storage set up in test environment.
+ *
+ * @id Document to remove.
+ * @disk If set, use this disk, otherwise lookup in bucket db.
+ * @unrevertableRemove If set, instead of adding put, turn put to remove.
+ * @usedBits Generate bucket to use from docid using this amount of bits.
+ */
+ void doRemove(const DocumentId& id, Timestamp, uint16_t disk,
+ bool unrevertableRemove = false, uint16_t usedBits = 16);
+
+ spi::GetResult doGetOnDisk(
+ uint16_t disk,
+ const document::BucketId& bucketId,
+ const document::DocumentId& docId,
+ const document::FieldSet& fields);
+
+ spi::GetResult doGet(
+ const document::BucketId& bucketId,
+ const document::DocumentId& docId,
+ const document::FieldSet& fields)
+ { return doGetOnDisk(0, bucketId, docId, fields); }
+
+ document::DocumentUpdate::SP createBodyUpdate(
+ const document::DocumentId& id,
+ const document::FieldValue& updateValue);
+
+ document::DocumentUpdate::SP createHeaderUpdate(
+ const document::DocumentId& id,
+ const document::FieldValue& updateValue);
+
+ virtual const document::DocumentTypeRepo::SP getTypeRepo() const
+ { return document::TestDocMan::getTypeRepoSP(); }
+
+ /**
+ * Do a put toward storage set up in test environment.
+ *
+ * @doc Document to put. Use TestDocMan to generate easily.
+ * @disk If set, use this disk, otherwise lookup in bucket db.
+ * @usedBits Generate bucket to use from docid using this amount of bits.
+ */
+ void doPut(const Document::SP& doc, Timestamp,
+ uint16_t disk, uint16_t usedBits = 16);
+
+ void doPut(const document::Document::SP& doc,
+ document::BucketId bid,
+ Timestamp time,
+ uint16_t disk = 0);
+
+ spi::UpdateResult doUpdate(document::BucketId bid,
+ const document::DocumentUpdate::SP& update,
+ Timestamp time,
+ uint16_t disk = 0);
+
+ /**
+ * Create a test bucket with various content representing most states a
+ * bucket can represent. (Such that tests have a nice test bucket to use
+ * that require operations to handle all the various bucket contents.
+ *
+ * @disk If set, use this disk, otherwise lookup in bucket db.
+ */
+ void createTestBucket(const BucketId&, uint16_t disk = 0xffff);
+
+ /**
+ * In-place modify doc so that it has no more body fields.
+ */
+ void clearBody(document::Document& doc);
+
+ /**
+ * Copy all header data from src into dest, replacing any
+ * header fields it may already have there. NOTE: this will
+ * also overwrite document ID, type etc!
+ */
+ void copyHeader(document::Document& dest,
+ const document::Document& src);
+
+ /**
+ * Copy all body data from src into dest, replacing any
+ * body fields it may already have there.
+ */
+ void copyBody(document::Document& dest,
+ const document::Document& src);
+
+ std::string stringifyFields(const Document& doc) const;
+
+ struct IoErrors {
+ int _afterReads;
+ int _afterWrites;
+
+ IoErrors()
+ : _afterReads(0),
+ _afterWrites(0)
+ {
+ }
+
+ IoErrors& afterReads(int n) {
+ _afterReads = n;
+ return *this;
+ }
+
+ IoErrors& afterWrites(int n) {
+ _afterWrites = n;
+ return *this;
+ }
+ };
+
+ /**
+ * Replaces internal LazyFile factory so that it produces LazyFile
+ * implementations that trigger I/O exceptions on read/write. Optionally,
+ * can supply a parameter setting explicit bounds on how many operations
+ * are allowed on a file before trigging exceptions from there on out. A
+ * bound of -1 in practice means "don't fail ever" while 0 means "fail the
+ * next op of that type".
+ */
+ void simulateIoErrorsForSubsequentlyOpenedFiles(
+ const IoErrors& errs = IoErrors());
+
+ /**
+ * Replace internal LazyFile factory with the default, non-failing impl.
+ */
+ void unSimulateIoErrorsForSubsequentlyOpenedFiles();
+};
+
+class SingleDiskMemFileTestUtils : public MemFileTestUtils
+{
+public:
+ void setUp() {
+ setupDisks(1);
+ }
+};
+
+} // memfile
+} // storage
+
diff --git a/memfilepersistence/src/tests/spi/memfilev1serializertest.cpp b/memfilepersistence/src/tests/spi/memfilev1serializertest.cpp
new file mode 100644
index 00000000000..a5d1c50d043
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/memfilev1serializertest.cpp
@@ -0,0 +1,1110 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/mapper/memfilemapper.h>
+#include <vespa/memfilepersistence/mapper/memfile_v1_serializer.h>
+#include <vespa/memfilepersistence/mapper/simplememfileiobuffer.h>
+#include <tests/spi/memfiletestutils.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/memfilepersistence/mapper/locationreadplanner.h>
+#include <tests/spi/simulatedfailurefile.h>
+#include <tests/spi/options_builder.h>
+
+namespace storage {
+namespace memfile {
+
+struct MemFileV1SerializerTest : public SingleDiskMemFileTestUtils
+{
+ void tearDown();
+ void setUpPartialWriteEnvironment();
+ void resetConfig(uint32_t minimumFileSize, uint32_t minimumFileHeaderBlockSize);
+ void doTestPartialWriteRemove(bool readAll);
+ void doTestPartialWriteUpdate(bool readAll);
+
+ void testWriteReadSingleDoc();
+ void testWriteReadPartial();
+ void testWriteReadPartialRemoved();
+ void testPartialWritePutHeaderOnly();
+ void testPartialWritePut();
+ void testPartialWriteRemoveCached();
+ void testPartialWriteRemoveNotCached();
+ void testPartialWriteUpdateCached();
+ void testPartialWriteUpdateNotCached();
+ void testPartialWriteTooMuchFreeSpace();
+ void testPartialWriteNotEnoughFreeSpace();
+ void testWriteReadSingleRemovedDoc();
+ void testLocationDiskIoPlannerSimple();
+ void testLocationDiskIoPlannerMergeReads();
+ void testLocationDiskIoPlannerAlignReads();
+ void testLocationDiskIoPlannerOneDocument();
+ void testSeparateReadsForHeaderAndBody();
+ void testLocationsRemappedConsistently();
+ void testHeaderBufferTooSmall();
+
+ /*std::unique_ptr<MemFile> createMemFile(FileSpecification& file,
+ bool callLoadFile)
+ {
+ return std::unique_ptr<MemFile>(new MemFile(file, env(), callLoadFile));
+ }*/
+
+ CPPUNIT_TEST_SUITE(MemFileV1SerializerTest);
+ CPPUNIT_TEST(testWriteReadSingleDoc);
+ CPPUNIT_TEST(testWriteReadPartial);
+ CPPUNIT_TEST(testWriteReadPartialRemoved);
+ CPPUNIT_TEST(testWriteReadSingleRemovedDoc);
+ CPPUNIT_TEST(testPartialWritePutHeaderOnly);
+ CPPUNIT_TEST(testPartialWritePut);
+ CPPUNIT_TEST(testPartialWriteRemoveCached);
+ CPPUNIT_TEST(testPartialWriteRemoveNotCached);
+ CPPUNIT_TEST(testPartialWriteUpdateCached);
+ CPPUNIT_TEST(testPartialWriteUpdateNotCached);
+ CPPUNIT_TEST(testLocationDiskIoPlannerSimple);
+ CPPUNIT_TEST(testLocationDiskIoPlannerMergeReads);
+ CPPUNIT_TEST(testLocationDiskIoPlannerAlignReads);
+ CPPUNIT_TEST(testLocationDiskIoPlannerOneDocument);
+ CPPUNIT_TEST(testSeparateReadsForHeaderAndBody);
+ CPPUNIT_TEST(testPartialWriteTooMuchFreeSpace);
+ CPPUNIT_TEST(testPartialWriteNotEnoughFreeSpace);
+ CPPUNIT_TEST(testLocationsRemappedConsistently);
+ CPPUNIT_TEST(testHeaderBufferTooSmall);
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(MemFileV1SerializerTest);
+
+namespace {
+
+const vespalib::LazyFile&
+getFileHandle(const MemFile& mf1)
+{
+ return static_cast<const SimpleMemFileIOBuffer&>(
+ mf1.getMemFileIO()).getFileHandle();
+}
+
+const LoggingLazyFile&
+getLoggerFile(const MemFile& file)
+{
+ return static_cast<const LoggingLazyFile&>(getFileHandle(file));
+}
+
+bool isContentEqual(MemFile& mf1, MemFile& mf2,
+ bool requireEqualContentCached, std::ostream& error)
+{
+ MemFile::const_iterator it1(
+ mf1.begin(Types::ITERATE_GID_UNIQUE | Types::ITERATE_REMOVED));
+ MemFile::const_iterator it2(
+ mf2.begin(Types::ITERATE_GID_UNIQUE | Types::ITERATE_REMOVED));
+ while (true) {
+ if (it1 == mf1.end() && it2 == mf2.end()) {
+ return true;
+ }
+ if (it1 == mf1.end() || it2 == mf2.end()) {
+ error << "Different amount of GID unique slots";
+ return false;
+ }
+ if (it1->getTimestamp() != it2->getTimestamp()) {
+ error << "Different timestamps";
+ return false;
+ }
+ if (it1->getGlobalId() != it2->getGlobalId()) {
+ error << "Different gids";
+ return false;
+ }
+ if (it1->getPersistedFlags() != it2->getPersistedFlags()) {
+ error << "Different persisted flags";
+ return false;
+ }
+ if (requireEqualContentCached) {
+ if (mf1.partAvailable(*it1, Types::BODY)
+ ^ mf2.partAvailable(*it2, Types::BODY)
+ || mf1.partAvailable(*it1, Types::HEADER)
+ ^ mf2.partAvailable(*it2, Types::HEADER))
+ {
+ error << "Difference in cached content: ";
+ return false;
+ }
+ }
+
+ if (mf1.partAvailable(*it1, Types::HEADER) &&
+ mf2.partAvailable(*it2, Types::HEADER))
+ {
+ document::Document::UP doc1 = mf1.getDocument(*it1, Types::ALL);
+ document::Document::UP doc2 = mf2.getDocument(*it2, Types::ALL);
+
+ CPPUNIT_ASSERT(doc1.get());
+ CPPUNIT_ASSERT(doc2.get());
+
+ if (*doc1 != *doc2) {
+ error << "Documents different: Expected:\n"
+ << doc1->toString(true) << "\nActual:\n"
+ << doc2->toString(true) << "\n";
+ return false;
+ }
+ }
+ ++it1;
+ ++it2;
+ }
+}
+
+bool
+validateMemFileStructure(const MemFile& mf, std::ostream& error)
+{
+ const SimpleMemFileIOBuffer& ioBuf(
+ dynamic_cast<const SimpleMemFileIOBuffer&>(mf.getMemFileIO()));
+ const FileInfo& fileInfo(ioBuf.getFileInfo());
+ if (fileInfo.getFileSize() % 512) {
+ error << "File size is not a multiple of 512 bytes";
+ return false;
+ }
+ if (fileInfo.getBlockIndex(Types::BODY) % 512) {
+ error << "Body start index is not a multiple of 512 bytes";
+ return false;
+ }
+ if (fileInfo.getBlockSize(Types::BODY) % 512) {
+ error << "Body size is not a multiple of 512 bytes";
+ return false;
+ }
+ return true;
+}
+
+}
+
+void
+MemFileV1SerializerTest::tearDown() {
+ //_memFile.reset();
+}
+
+/**
+ * Adjust minimum slotfile size values to avoid rewriting file
+ * when we want to get a partial write
+ */
+void
+MemFileV1SerializerTest::setUpPartialWriteEnvironment()
+{
+ resetConfig(4096, 2048);
+}
+
+void
+MemFileV1SerializerTest::resetConfig(uint32_t minimumFileSize,
+ uint32_t minimumFileHeaderBlockSize)
+{
+ using MemFileConfig = vespa::config::storage::StorMemfilepersistenceConfig;
+ using MemFileConfigBuilder
+ = vespa::config::storage::StorMemfilepersistenceConfigBuilder;
+
+ MemFileConfigBuilder persistenceConfig(
+ *env().acquireConfigReadLock().memFilePersistenceConfig());
+ persistenceConfig.minimumFileHeaderBlockSize = minimumFileHeaderBlockSize;
+ persistenceConfig.minimumFileSize = minimumFileSize;
+ auto newCfg = std::unique_ptr<MemFileConfig>(
+ new MemFileConfig(persistenceConfig));
+ env().acquireConfigWriteLock().setMemFilePersistenceConfig(
+ std::move(newCfg));
+}
+
+struct DummyMemFileIOInterface : MemFileIOInterface {
+ Document::UP getDocumentHeader(const document::DocumentTypeRepo&,
+ DataLocation) const
+ {
+ return Document::UP();
+ }
+
+ document::DocumentId getDocumentId(DataLocation) const {
+ return document::DocumentId("");
+ }
+
+ void readBody(const document::DocumentTypeRepo&,
+ DataLocation,
+ Document&) const
+ {
+ }
+ DataLocation addDocumentIdOnlyHeader(
+ const DocumentId&,
+ const document::DocumentTypeRepo&)
+ {
+ return DataLocation();
+ }
+ DataLocation addHeader(const Document&) { return DataLocation(); }
+ DataLocation addBody(const Document&) { return DataLocation(); }
+ void clear(DocumentPart) {}
+ bool verifyConsistent() const { return true; }
+ void move(const FileSpecification&) {}
+ DataLocation copyCache(const MemFileIOInterface&,
+ DocumentPart,
+ DataLocation)
+ {
+ return DataLocation();
+ }
+
+ void close() {};
+ bool isCached(DataLocation, DocumentPart) const { return false; }
+ bool isPersisted(DataLocation, DocumentPart) const { return false; }
+ uint32_t getSerializedSize(DocumentPart,
+ DataLocation) const { return 0; }
+
+ void ensureCached(Environment&,
+ DocumentPart,
+ const std::vector<DataLocation>&)
+ {}
+
+ size_t getCachedSize(DocumentPart) const { return 0; }
+};
+
+#define VESPA_MEMFILEV1_SETUP_SOURCE \
+ system("rm -f testfile.0"); \
+ document::Document::SP doc(createRandomDocumentAtLocation(4)); \
+ FileSpecification file(document::BucketId(16, 4), env().getDirectory(0), "testfile.0"); \
+ MemFile source(file, env());
+
+#define VESPA_MEMFILEV1_DIFF(source, target) \
+ "\nSource:\n" + source.toString(true) \
+ + "\nTarget:\n" + target.toString(true)
+
+#define VESPA_MEMFILEV1_VALIDATE_STRUCTURE(mfile) \
+{ \
+ std::ostringstream validateErr; \
+ if (!validateMemFileStructure(mfile, validateErr)) { \
+ CPPUNIT_FAIL(validateErr.str()); \
+ } \
+}
+
+#define VESPA_MEMFILEV1_ASSERT_SERIALIZATION(sourceMemFile) \
+env()._memFileMapper.flush(sourceMemFile, env()); \
+VESPA_MEMFILEV1_VALIDATE_STRUCTURE(sourceMemFile) \
+MemFile target(file, env()); \
+VESPA_MEMFILEV1_VALIDATE_STRUCTURE(target) \
+{ \
+ target.ensureBodyBlockCached(); \
+ target.getBucketInfo(); \
+ std::ostringstream diff; \
+ if (!isContentEqual(sourceMemFile, target, true, diff)) { \
+ std::string msg = "MemFiles not content equal: " + diff.str() \
+ + VESPA_MEMFILEV1_DIFF(sourceMemFile, target); \
+ CPPUNIT_FAIL(msg); \
+ } \
+}
+
+void
+MemFileV1SerializerTest::testWriteReadSingleDoc()
+{
+ VESPA_MEMFILEV1_SETUP_SOURCE;
+ source.addPutSlot(*doc, Timestamp(1001));
+ std::string foo(VESPA_MEMFILEV1_DIFF(source, source));
+ VESPA_MEMFILEV1_ASSERT_SERIALIZATION(source);
+}
+
+void
+MemFileV1SerializerTest::testWriteReadPartial()
+{
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ std::map<Timestamp, Document::SP> docs;
+ {
+ MemFile source(file, env());
+
+ for (int i = 0; i < 50; ++i) {
+ Document::SP doc(createRandomDocumentAtLocation(4, i, 1000, 2000));
+ source.addPutSlot(*doc, Timestamp(1001 + i));
+ docs[Timestamp(1001 + i)] = doc;
+ }
+
+ env()._memFileMapper.flush(source, env());
+ VESPA_MEMFILEV1_VALIDATE_STRUCTURE(source);
+ }
+
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options).maximumReadThroughGap(1024).build());
+ env()._lazyFileFactory = std::unique_ptr<Environment::LazyFileFactory>(
+ new LoggingLazyFile::Factory());
+
+ MemFile target(file, env());
+
+ std::vector<Timestamp> timestamps;
+
+ for (int i = 0; i < 50; i+=4) {
+ timestamps.push_back(Timestamp(1001 + i));
+ }
+ CPPUNIT_ASSERT_EQUAL(size_t(13), timestamps.size());
+
+ getLoggerFile(target).operations.clear();
+ target.ensureDocumentCached(timestamps, false);
+ // Headers are small enough that they get read in 1 op + 13 body reads
+ CPPUNIT_ASSERT_EQUAL(14, (int)getLoggerFile(target).operations.size());
+
+ for (std::size_t i = 0; i < timestamps.size(); ++i) {
+ const MemSlot* slot = target.getSlotAtTime(timestamps[i]);
+ CPPUNIT_ASSERT(slot);
+ CPPUNIT_ASSERT(target.partAvailable(*slot, HEADER));
+ CPPUNIT_ASSERT(target.partAvailable(*slot, BODY));
+ CPPUNIT_ASSERT_EQUAL(*docs[timestamps[i]], *target.getDocument(*slot, ALL));
+ }
+ VESPA_MEMFILEV1_VALIDATE_STRUCTURE(target);
+}
+
+void
+MemFileV1SerializerTest::testWriteReadPartialRemoved()
+{
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ MemFile source(file, env());
+
+ for (int i = 0; i < 50; ++i) {
+ Document::SP doc(createRandomDocumentAtLocation(4, i, 1000, 2000));
+ source.addPutSlot(*doc, Timestamp(1001 + i));
+ source.addRemoveSlot(*source.getSlotAtTime(Timestamp(1001 + i)),
+ Timestamp(2001 + i));
+ }
+
+ env()._memFileMapper.flush(source, env());
+ VESPA_MEMFILEV1_VALIDATE_STRUCTURE(source);
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options).maximumReadThroughGap(1024).build());
+ env()._lazyFileFactory = std::unique_ptr<Environment::LazyFileFactory>(
+ new LoggingLazyFile::Factory);
+
+ MemFile target(file, env());
+
+ std::vector<Timestamp> timestamps;
+
+ for (int i = 0; i < 50; i+=4) {
+ timestamps.push_back(Timestamp(2001 + i));
+ }
+
+ getLoggerFile(target).operations.clear();
+ target.ensureDocumentCached(timestamps, false);
+ // All removed; should only read header locations
+ CPPUNIT_ASSERT_EQUAL(1, (int)getLoggerFile(target).operations.size());
+
+ for (std::size_t i = 0; i < timestamps.size(); ++i) {
+ const MemSlot* slot = target.getSlotAtTime(timestamps[i]);
+ const MemSlot* removedPut(
+ target.getSlotAtTime(timestamps[i] - Timestamp(1000)));
+ CPPUNIT_ASSERT(slot);
+ CPPUNIT_ASSERT(removedPut);
+ CPPUNIT_ASSERT(target.partAvailable(*slot, HEADER));
+ CPPUNIT_ASSERT_EQUAL(removedPut->getLocation(HEADER),
+ slot->getLocation(HEADER));
+ CPPUNIT_ASSERT_EQUAL(DataLocation(0, 0), slot->getLocation(BODY));
+ }
+ VESPA_MEMFILEV1_VALIDATE_STRUCTURE(target);
+}
+
+void MemFileV1SerializerTest::testWriteReadSingleRemovedDoc()
+{
+ VESPA_MEMFILEV1_SETUP_SOURCE;
+ source.addPutSlot(*doc, Timestamp(1001));
+ source.addRemoveSlot(
+ *source.getSlotAtTime(Timestamp(1001)), Timestamp(2001));
+ VESPA_MEMFILEV1_ASSERT_SERIALIZATION(source);
+}
+
+/**
+ * Write a single put with no body to the memfile and ensure it is
+ * persisted properly without a body block
+ */
+void
+MemFileV1SerializerTest::testPartialWritePutHeaderOnly()
+{
+ setUpPartialWriteEnvironment();
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ document::Document::SP doc(createRandomDocumentAtLocation(4));
+ {
+ MemFile source(file, env());
+ source.addPutSlot(*doc, Timestamp(1001));
+ env()._memFileMapper.flush(source, env());
+ VESPA_MEMFILEV1_VALIDATE_STRUCTURE(source);
+ }
+ {
+ // Have to put a second time since the first one will always
+ // rewrite the entire file
+ MemFile target(file, env());
+ Document::SP doc2(createRandomDocumentAtLocation(4));
+ clearBody(*doc2);
+ target.addPutSlot(*doc2, Timestamp(1003));
+ env()._memFileMapper.flush(target, env());
+ VESPA_MEMFILEV1_VALIDATE_STRUCTURE(target);
+ }
+ {
+ MemFile target(file, env());
+ target.ensureBodyBlockCached();
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), target.getSlotCount());
+
+ const MemSlot& slot = *target.getSlotAtTime(Timestamp(1003));
+ CPPUNIT_ASSERT(slot.getLocation(HEADER)._pos > 0);
+ CPPUNIT_ASSERT(slot.getLocation(HEADER)._size > 0);
+ CPPUNIT_ASSERT_EQUAL(
+ DataLocation(0, 0), slot.getLocation(BODY));
+ VESPA_MEMFILEV1_VALIDATE_STRUCTURE(target);
+ }
+}
+
+
+
+
+void
+MemFileV1SerializerTest::testLocationDiskIoPlannerSimple()
+{
+ std::vector<MemSlot> slots;
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1001),
+ DataLocation(0, 1024),
+ DataLocation(4096, 512), 0, 0));
+ }
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1003),
+ DataLocation(1024, 1024),
+ DataLocation(8192, 512), 0, 0));
+ }
+
+ std::vector<DataLocation> headers;
+ std::vector<DataLocation> bodies;
+ headers.push_back(slots[0].getLocation(HEADER));
+ bodies.push_back(slots[0].getLocation(BODY));
+
+ DummyMemFileIOInterface dummyIo;
+ {
+ LocationDiskIoPlanner planner(dummyIo, HEADER, headers, 100, 0);
+
+ CPPUNIT_ASSERT_EQUAL(1, (int)planner.getIoOperations().size());
+ CPPUNIT_ASSERT_EQUAL(
+ DataLocation(0, 1024),
+ planner.getIoOperations()[0]);
+ }
+ {
+ LocationDiskIoPlanner planner(dummyIo, BODY, bodies, 100, 4096);
+
+ CPPUNIT_ASSERT_EQUAL(1, (int)planner.getIoOperations().size());
+ CPPUNIT_ASSERT_EQUAL(
+ DataLocation(8192, 512), // + block index
+ planner.getIoOperations()[0]);
+ }
+}
+
+void
+MemFileV1SerializerTest::testLocationDiskIoPlannerMergeReads()
+{
+ std::vector<MemSlot> slots;
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1001),
+ DataLocation(0, 1024),
+ DataLocation(5120, 512), 0, 0));
+ }
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1002),
+ DataLocation(2048, 1024),
+ DataLocation(7168, 512), 0, 0));
+ }
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1003),
+ DataLocation(1024, 1024),
+ DataLocation(9216, 512), 0, 0));
+ }
+
+ std::vector<DataLocation> headers;
+ std::vector<DataLocation> bodies;
+ for (int i = 0; i < 2; ++i) {
+ headers.push_back(slots[i].getLocation(HEADER));
+ bodies.push_back(slots[i].getLocation(BODY));
+ }
+
+ DummyMemFileIOInterface dummyIo;
+ {
+ LocationDiskIoPlanner planner(dummyIo, HEADER, headers, 1025, 0);
+
+ CPPUNIT_ASSERT_EQUAL(1, (int)planner.getIoOperations().size());
+ CPPUNIT_ASSERT_EQUAL(
+ DataLocation(0, 3072),
+ planner.getIoOperations()[0]);
+ }
+
+ {
+ LocationDiskIoPlanner planner(dummyIo, BODY, bodies, 1025, 0);
+
+ CPPUNIT_ASSERT_EQUAL(2, (int)planner.getIoOperations().size());
+ CPPUNIT_ASSERT_EQUAL(
+ DataLocation(5120, 512),
+ planner.getIoOperations()[0]);
+ CPPUNIT_ASSERT_EQUAL(
+ DataLocation(7168, 512),
+ planner.getIoOperations()[1]);
+ }
+}
+
+void
+MemFileV1SerializerTest::testLocationDiskIoPlannerOneDocument()
+{
+ std::vector<MemSlot> slots;
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1001),
+ DataLocation(0, 1024),
+ DataLocation(5120, 512), 0, 0));
+ }
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1002),
+ DataLocation(2048, 1024),
+ DataLocation(7168, 512), 0, 0));
+ }
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1003),
+ DataLocation(1024, 1024),
+ DataLocation(9216, 512), 0, 0));
+ }
+
+ std::vector<DataLocation> headers;
+ std::vector<DataLocation> bodies;
+ headers.push_back(slots[1].getLocation(HEADER));
+ bodies.push_back(slots[1].getLocation(BODY));
+
+ DummyMemFileIOInterface dummyIo;
+ {
+ LocationDiskIoPlanner planner(dummyIo, HEADER, headers, 1000, 0);
+ CPPUNIT_ASSERT_EQUAL(1, (int)planner.getIoOperations().size());
+ CPPUNIT_ASSERT_EQUAL(
+ DataLocation(2048, 1024),
+ planner.getIoOperations()[0]);
+ }
+
+ {
+ LocationDiskIoPlanner planner(dummyIo, BODY, bodies, 1000, 0);
+ CPPUNIT_ASSERT_EQUAL(1, (int)planner.getIoOperations().size());
+ CPPUNIT_ASSERT_EQUAL(
+ DataLocation(7168, 512),
+ planner.getIoOperations()[0]);
+ }
+}
+
+void
+MemFileV1SerializerTest::testLocationDiskIoPlannerAlignReads()
+{
+ std::vector<MemSlot> slots;
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1001),
+ DataLocation(7, 100),
+ DataLocation(5000, 500), 0, 0));
+ }
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1002),
+ DataLocation(2000, 100),
+ DataLocation(7000, 500), 0, 0));
+ }
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1003),
+ DataLocation(110, 200),
+ DataLocation(9000, 500), 0, 0));
+ }
+
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ slots.push_back(
+ MemSlot(
+ doc->getId().getGlobalId(),
+ Timestamp(1004),
+ DataLocation(3000, 100),
+ DataLocation(11000, 500), 0, 0));
+ }
+
+ std::vector<DataLocation> headers;
+ std::vector<DataLocation> bodies;
+ for (int i = 0; i < 2; ++i) {
+ headers.push_back(slots[i].getLocation(HEADER));
+ bodies.push_back(slots[i].getLocation(BODY));
+ }
+
+ DummyMemFileIOInterface dummyIo;
+ {
+ LocationDiskIoPlanner planner(dummyIo, HEADER, headers, 512, 0);
+ std::vector<DataLocation> expected;
+ expected.push_back(DataLocation(0, 512));
+ expected.push_back(DataLocation(1536, 1024));
+
+ CPPUNIT_ASSERT_EQUAL(expected, planner.getIoOperations());
+ }
+ {
+ LocationDiskIoPlanner planner(dummyIo, BODY, bodies, 512, 0);
+ std::vector<DataLocation> expected;
+ expected.push_back(DataLocation(4608, 1024));
+ expected.push_back(DataLocation(6656, 1024));
+
+ CPPUNIT_ASSERT_EQUAL(expected, planner.getIoOperations());
+ }
+}
+
+// TODO(vekterli): add read planner test with a location cached
+
+void
+MemFileV1SerializerTest::testSeparateReadsForHeaderAndBody()
+{
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ Document::SP doc(createRandomDocumentAtLocation(4, 0, 1000, 2000));
+ {
+ MemFile source(file, env());
+ source.addPutSlot(*doc, Timestamp(1001));
+
+ env()._memFileMapper.flush(source, env());
+ }
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options)
+ .maximumReadThroughGap(1024*1024*100)
+ .build());
+ env()._lazyFileFactory = std::unique_ptr<Environment::LazyFileFactory>(
+ new LoggingLazyFile::Factory());
+
+ MemFile target(file, env());
+
+ std::vector<Timestamp> timestamps;
+ timestamps.push_back(Timestamp(1001));
+
+ getLoggerFile(target).operations.clear();
+ target.ensureDocumentCached(timestamps, false);
+
+ CPPUNIT_ASSERT_EQUAL(2, (int)getLoggerFile(target).operations.size());
+ const MemSlot* slot = target.getSlotAtTime(Timestamp(1001));
+ CPPUNIT_ASSERT(slot);
+ CPPUNIT_ASSERT(target.partAvailable(*slot, HEADER));
+ CPPUNIT_ASSERT(target.partAvailable(*slot, BODY));
+ CPPUNIT_ASSERT_EQUAL(*doc, *target.getDocument(*slot, ALL));
+
+ CPPUNIT_ASSERT(getMetrics().serialization.headerReadSize.getLast() > 0);
+ CPPUNIT_ASSERT(getMetrics().serialization.bodyReadSize.getLast() > 0);
+}
+
+/**
+ * Write a single put with body to the memfile and ensure it is
+ * persisted properly with both header and body blocks
+ */
+void
+MemFileV1SerializerTest::testPartialWritePut()
+{
+ setUpPartialWriteEnvironment();
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ {
+ MemFile source(file, env());
+ source.addPutSlot(*doc, Timestamp(1001));
+
+ env()._memFileMapper.flush(source, env());
+ }
+
+ {
+ // Have to put a second time since the first one will always
+ // rewrite the entire file
+ MemFile target(file, env());
+ Document::SP doc2(createRandomDocumentAtLocation(4));
+ target.addPutSlot(*doc2, Timestamp(1003));
+ env()._memFileMapper.flush(target, env());
+ }
+ {
+ MemFile target(file, env());
+ target.ensureBodyBlockCached();
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), target.getSlotCount());
+
+ const MemSlot& slot = *target.getSlotAtTime(Timestamp(1003));
+ CPPUNIT_ASSERT(slot.getLocation(HEADER)._pos > 0);
+ CPPUNIT_ASSERT(slot.getLocation(HEADER)._size > 0);
+
+ CPPUNIT_ASSERT(slot.getLocation(BODY)._size > 0);
+ CPPUNIT_ASSERT(slot.getLocation(BODY)._pos > 0);
+ }
+}
+
+void
+MemFileV1SerializerTest::doTestPartialWriteRemove(bool readAll)
+{
+ setUpPartialWriteEnvironment();
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ {
+ MemFile source(file, env());
+ source.addPutSlot(*doc, Timestamp(1001));
+ env()._memFileMapper.flush(source, env());
+ }
+ {
+ MemFile target(file, env());
+ // Only populate cache before removing if explicitly told so
+ if (readAll) {
+ target.ensureBodyBlockCached();
+ }
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), target.getSlotCount());
+ target.addRemoveSlot(target[0], Timestamp(1003));
+
+ env()._memFileMapper.flush(target, env());
+ }
+ {
+ MemFile target(file, env());
+ target.ensureBodyBlockCached();
+
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), target.getSlotCount());
+
+ const MemSlot& originalSlot = target[0];
+ const MemSlot& removeSlot = target[1];
+ CPPUNIT_ASSERT(originalSlot.getLocation(HEADER)._size > 0);
+ CPPUNIT_ASSERT(originalSlot.getLocation(BODY)._size > 0);
+ CPPUNIT_ASSERT_EQUAL(
+ originalSlot.getLocation(HEADER),
+ removeSlot.getLocation(HEADER));
+ CPPUNIT_ASSERT_EQUAL(
+ DataLocation(0, 0), removeSlot.getLocation(BODY));
+ }
+}
+
+/**
+ * Ensure that removes get the same header location as the Put
+ * they're removing, and that they get a zero body location
+ */
+void
+MemFileV1SerializerTest::testPartialWriteRemoveCached()
+{
+ doTestPartialWriteRemove(true);
+}
+
+void
+MemFileV1SerializerTest::testPartialWriteRemoveNotCached()
+{
+ doTestPartialWriteRemove(false);
+}
+
+void
+MemFileV1SerializerTest::doTestPartialWriteUpdate(bool readAll)
+{
+ setUpPartialWriteEnvironment();
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ {
+ MemFile source(file, env());
+ source.addPutSlot(*doc, Timestamp(1001));
+ env()._memFileMapper.flush(source, env());
+ }
+
+ Document::SP doc2;
+ {
+ MemFile target(file, env());
+ if (readAll) {
+ target.ensureBodyBlockCached();
+ }
+
+ doc2.reset(new Document(*doc->getDataType(), doc->getId()));
+ clearBody(*doc2);
+ doc2->setValue(doc->getField("hstringval"),
+ document::StringFieldValue("Some updated content"));
+
+ target.addUpdateSlot(*doc2, *target.getSlotAtTime(Timestamp(1001)),
+ Timestamp(1003));
+ env()._memFileMapper.flush(target, env());
+ }
+
+ {
+ MemFile target(file, env());
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2), target.getSlotCount());
+ const MemSlot& originalSlot = target[0];
+ const MemSlot& updateSlot = target[1];
+ CPPUNIT_ASSERT(originalSlot.getLocation(HEADER)._size > 0);
+ CPPUNIT_ASSERT(originalSlot.getLocation(BODY)._size > 0);
+ CPPUNIT_ASSERT_EQUAL(
+ originalSlot.getLocation(BODY),
+ updateSlot.getLocation(BODY));
+ CPPUNIT_ASSERT(
+ updateSlot.getLocation(HEADER)
+ != originalSlot.getLocation(HEADER));
+
+ CPPUNIT_ASSERT_EQUAL(*doc, *target.getDocument(target[0], ALL));
+ copyHeader(*doc, *doc2);
+ CPPUNIT_ASSERT_EQUAL(*doc, *target.getDocument(target[1], ALL));
+ }
+}
+
+/**
+ * Ensure that header updates keep the same body block
+ */
+void
+MemFileV1SerializerTest::testPartialWriteUpdateCached()
+{
+ doTestPartialWriteUpdate(true);
+}
+
+void
+MemFileV1SerializerTest::testPartialWriteUpdateNotCached()
+{
+ doTestPartialWriteUpdate(false);
+}
+
+void
+MemFileV1SerializerTest::testPartialWriteTooMuchFreeSpace()
+{
+ setUpPartialWriteEnvironment();
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ {
+ MemFile source(file, env());
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ source.addPutSlot(*doc, Timestamp(1001));
+ env()._memFileMapper.flush(source, env());
+ }
+ int64_t sizeBefore;
+ // Append filler to slotfile to make it too big for comfort,
+ // forcing a rewrite to shrink it down
+ {
+ vespalib::File slotfile(file.getPath());
+ slotfile.open(0);
+ CPPUNIT_ASSERT(slotfile.isOpen());
+ sizeBefore = slotfile.getFileSize();
+ slotfile.resize(sizeBefore * 20); // Well over min fill rate of 10%
+ }
+ // Write new slot to file; it should now be rewritten with the
+ // same file size as originally
+ {
+ MemFile source(file, env());
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ source.addPutSlot(*doc, Timestamp(1003));
+ env()._memFileMapper.flush(source, env());
+ }
+ {
+ vespalib::File slotfile(file.getPath());
+ slotfile.open(0);
+ CPPUNIT_ASSERT(slotfile.isOpen());
+ CPPUNIT_ASSERT_EQUAL(
+ sizeBefore,
+ slotfile.getFileSize());
+ }
+ CPPUNIT_ASSERT_EQUAL(uint64_t(1), getMetrics().serialization
+ .fullRewritesDueToDownsizingFile.getValue());
+ CPPUNIT_ASSERT_EQUAL(uint64_t(0), getMetrics().serialization
+ .fullRewritesDueToTooSmallFile.getValue());
+}
+
+void
+MemFileV1SerializerTest::testPartialWriteNotEnoughFreeSpace()
+{
+ setUpPartialWriteEnvironment();
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ // Write file initially
+ MemFile source(file, env());
+ {
+ Document::SP doc(createRandomDocumentAtLocation(4));
+ source.addPutSlot(*doc, Timestamp(1001));
+ env()._memFileMapper.flush(source, env());
+ }
+
+ uint32_t minFile = 1024 * 512;
+ auto memFileCfg = env().acquireConfigReadLock().memFilePersistenceConfig();
+ resetConfig(minFile, memFileCfg->minimumFileHeaderBlockSize);
+
+ // Create doc bigger than initial minimum filesize,
+ // prompting a full rewrite
+ Document::SP doc(
+ createRandomDocumentAtLocation(4, 0, 4096, 4096));
+ source.addPutSlot(*doc, Timestamp(1003));
+
+ env()._memFileMapper.flush(source, env());
+
+ CPPUNIT_ASSERT_EQUAL(
+ minFile,
+ uint32_t(getFileHandle(source).getFileSize()));
+
+ CPPUNIT_ASSERT_EQUAL(uint64_t(0), getMetrics().serialization
+ .fullRewritesDueToDownsizingFile.getValue());
+ CPPUNIT_ASSERT_EQUAL(uint64_t(1), getMetrics().serialization
+ .fullRewritesDueToTooSmallFile.getValue());
+
+ // Now, ensure we respect minimum file size and don't try to
+ // "helpfully" rewrite the file again (try to detect full
+ // file rewrite with help from the fact we don't currently
+ // check whether or not the file is < the minimum filesize.
+ // If that changes, so must this)
+ memFileCfg = env().acquireConfigReadLock().memFilePersistenceConfig();
+ resetConfig(2 * minFile, memFileCfg->minimumFileHeaderBlockSize);
+
+ source.addRemoveSlot(*source.getSlotAtTime(Timestamp(1003)),
+ Timestamp(1005));
+ env()._memFileMapper.flush(source, env());
+
+ CPPUNIT_ASSERT_EQUAL(
+ minFile,
+ uint32_t(getFileHandle(source).getFileSize()));
+
+ CPPUNIT_ASSERT_EQUAL(uint64_t(1), getMetrics().serialization
+ .fullRewritesDueToTooSmallFile.getValue());
+}
+
+// Test that we don't mess up when remapping locations that
+// have already been written during the same operation. That is:
+// part A is remapped (P1, S1) -> (P2, S2)
+// part B is remapped (P2, S2) -> (P3, S3)
+// Obviously, part B should not overwrite the location of part A,
+// but this will happen if we don't do the updating in one batch.
+void
+MemFileV1SerializerTest::testLocationsRemappedConsistently()
+{
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+
+ std::map<Timestamp, Document::SP> docs;
+ {
+ MemFile mf(file, env());
+ Document::SP tmpDoc(
+ createRandomDocumentAtLocation(4, 0, 100, 100));
+
+ // Create docs identical in size but differing only in doc ids
+ // By keeping same size but inserting with _lower_ timestamps
+ // for docs that get higher location positions, we ensure that
+ // when the file is rewritten, the lower timestamp slots will
+ // get remapped to locations that match existing locations for
+ // higher timestamp slots.
+ for (int i = 0; i < 2; ++i) {
+ std::ostringstream ss;
+ ss << "doc" << i;
+ DocumentId id(document::UserDocIdString("userdoc:foo:4:" + ss.str()));
+ Document::SP doc(new Document(*tmpDoc->getDataType(), id));
+ doc->getFields() = tmpDoc->getFields();
+ mf.addPutSlot(*doc, Timestamp(1000 - i));
+ docs[Timestamp(1000 - i)] = doc;
+ }
+
+ env()._memFileMapper.flush(mf, env());
+ // Dirty the cache for rewrite
+ {
+ DocumentId id2(document::UserDocIdString("userdoc:foo:4:doc9"));
+ Document::UP doc2(new Document(*tmpDoc->getDataType(), id2));
+ doc2->getFields() = tmpDoc->getFields();
+ mf.addPutSlot(*doc2, Timestamp(2000));
+ docs[Timestamp(2000)] = std::move(doc2);
+ }
+
+ // Force rewrite
+ auto memFileCfg = env().acquireConfigReadLock()
+ .memFilePersistenceConfig();
+ resetConfig(1024*512, memFileCfg ->minimumFileHeaderBlockSize);
+ env()._memFileMapper.flush(mf, env());
+ }
+
+ MemFile target(file, env());
+ target.ensureBodyBlockCached();
+
+ std::ostringstream err;
+ if (!env()._memFileMapper.verify(target, env(), err)) {
+ std::cerr << err.str() << "\n";
+ CPPUNIT_FAIL("MemFile verification failed");
+ }
+
+ typedef std::map<Timestamp, Document::SP>::iterator Iter;
+ for (Iter it(docs.begin()); it != docs.end(); ++it) {
+ const MemSlot* slot = target.getSlotAtTime(it->first);
+ CPPUNIT_ASSERT(slot);
+ CPPUNIT_ASSERT(target.partAvailable(*slot, HEADER));
+ CPPUNIT_ASSERT(target.partAvailable(*slot, BODY));
+ CPPUNIT_ASSERT_EQUAL(*it->second, *target.getDocument(*slot, ALL));
+ }
+}
+
+/**
+ * Test that we read in the correct header information when we have to read
+ * in two passes to get it in its entirety.
+ */
+void
+MemFileV1SerializerTest::testHeaderBufferTooSmall()
+{
+ system("rm -f testfile.0");
+ FileSpecification file(BucketId(16, 4), env().getDirectory(0), "testfile.0");
+ FileInfo wantedInfo;
+ {
+ MemFile f(file, env());
+ // 50*40 bytes of meta list data should be more than sufficient
+ for (size_t i = 0; i < 50; ++i) {
+ Document::SP doc(createRandomDocumentAtLocation(4, i));
+ f.addPutSlot(*doc, Timestamp(1001 + i));
+ env()._memFileMapper.flush(f, env());
+ }
+ SimpleMemFileIOBuffer& io(
+ dynamic_cast<SimpleMemFileIOBuffer&>(f.getMemFileIO()));
+ wantedInfo = io.getFileInfo();
+ }
+
+ // Force initial index read to be too small to contain all metadata,
+ // triggering buffer resize and secondary read.
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options).initialIndexRead(512).build());
+ {
+ MemFile f(file, env());
+ CPPUNIT_ASSERT_EQUAL(uint32_t(50), f.getSlotCount());
+ // Ensure we've read correct file info
+ SimpleMemFileIOBuffer& io(
+ dynamic_cast<SimpleMemFileIOBuffer&>(f.getMemFileIO()));
+ const FileInfo& info(io.getFileInfo());
+ CPPUNIT_ASSERT_EQUAL(wantedInfo.getFileSize(), info.getFileSize());
+ CPPUNIT_ASSERT_EQUAL(wantedInfo.getHeaderBlockStartIndex(),
+ info.getHeaderBlockStartIndex());
+ CPPUNIT_ASSERT_EQUAL(wantedInfo.getBodyBlockStartIndex(),
+ info.getBodyBlockStartIndex());
+ CPPUNIT_ASSERT_EQUAL(wantedInfo.getBlockSize(HEADER),
+ info.getBlockSize(HEADER));
+ CPPUNIT_ASSERT_EQUAL(wantedInfo.getBlockSize(BODY),
+ info.getBlockSize(BODY));
+ }
+}
+
+} // memfile
+} // storage
diff --git a/memfilepersistence/src/tests/spi/memfilev1verifiertest.cpp b/memfilepersistence/src/tests/spi/memfilev1verifiertest.cpp
new file mode 100644
index 00000000000..0cf04eadaa2
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/memfilev1verifiertest.cpp
@@ -0,0 +1,501 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/mapper/memfilemapper.h>
+#include <vespa/memfilepersistence/mapper/memfile_v1_serializer.h>
+#include <vespa/memfilepersistence/mapper/memfile_v1_verifier.h>
+#include <vespa/memfilepersistence/mapper/fileinfo.h>
+#include <vespa/memfilepersistence/mapper/simplememfileiobuffer.h>
+#include <tests/spi/memfiletestutils.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <tests/spi/simulatedfailurefile.h>
+
+namespace storage {
+namespace memfile {
+
+struct MemFileV1VerifierTest : public SingleDiskMemFileTestUtils
+{
+ void testVerify();
+
+ void tearDown();
+
+ std::unique_ptr<MemFile> createMemFile(FileSpecification& file,
+ bool callLoadFile)
+ {
+ return std::unique_ptr<MemFile>(new MemFile(file, env(), callLoadFile));
+ }
+
+ CPPUNIT_TEST_SUITE(MemFileV1VerifierTest);
+ CPPUNIT_TEST_IGNORED(testVerify);
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(MemFileV1VerifierTest);
+
+namespace {
+ // A totall uncached memfile with content to use for verify testing
+ std::unique_ptr<MemFile> _memFile;
+
+ // Clear old content. Create new file. Make sure nothing is cached.
+ void prepareBucket(SingleDiskMemFileTestUtils& util,
+ const FileSpecification& file) {
+ _memFile.reset();
+ util.env()._cache.clear();
+ vespalib::unlink(file.getPath());
+ util.createTestBucket(file.getBucketId(), 0);
+ util.env()._cache.clear();
+ _memFile.reset(new MemFile(file, util.env()));
+ _memFile->getMemFileIO().close();
+
+ }
+
+ // Get copy of header of memfile created
+ Header getHeader() {
+ assert(_memFile.get());
+ vespalib::LazyFile file(_memFile->getFile().getPath(), 0);
+ Header result;
+ file.read(&result, sizeof(Header), 0);
+ return result;
+ }
+
+ MetaSlot getSlot(uint32_t index) {
+ assert(_memFile.get());
+ vespalib::LazyFile file(_memFile->getFile().getPath(), 0);
+ MetaSlot result;
+ file.read(&result, sizeof(MetaSlot),
+ sizeof(Header) + sizeof(MetaSlot) * index);
+ return result;
+ }
+
+ void setSlot(uint32_t index, MetaSlot slot,
+ bool updateFileChecksum = true)
+ {
+ (void)updateFileChecksum;
+ assert(_memFile.get());
+ //if (updateFileChecksum) slot.updateFileChecksum();
+ vespalib::LazyFile file(_memFile->getFile().getPath(), 0);
+ file.write(&slot, sizeof(MetaSlot),
+ sizeof(Header) + sizeof(MetaSlot) * index);
+ }
+
+ void setHeader(const Header& header) {
+ assert(_memFile.get());
+ vespalib::LazyFile file(_memFile->getFile().getPath(), 0);
+ file.write(&header, sizeof(Header), 0);
+ }
+
+ void verifySlotFile(MemFileV1VerifierTest& util,
+ const std::string& expectedError,
+ const std::string& message,
+ int32_t remainingEntries,
+ bool includeContent = true,
+ bool includeHeader = true)
+ {
+ assert(_memFile.get());
+ FileSpecification file(_memFile->getFile());
+ _memFile.reset();
+ _memFile = util.createMemFile(file, false);
+ std::ostringstream before;
+ try{
+ util.env()._memFileMapper.loadFile(*_memFile, util.env(), false);
+ _memFile->print(before, true, "");
+ } catch (vespalib::Exception& e) {
+ before << "Unknown. Exception during loadFile\n";
+ }
+ std::ostringstream errors;
+ uint32_t flags = (includeContent ? 0 : Types::DONT_VERIFY_BODY)
+ | (includeHeader ? 0 : Types::DONT_VERIFY_HEADER);
+ if (util.env()._memFileMapper.verify(
+ *_memFile, util.env(), errors, flags))
+ {
+ _memFile->print(std::cerr, true, "");
+ std::cerr << errors.str() << "\n";
+ CPPUNIT_FAIL("verify() failed to detect: " + message);
+ }
+ CPPUNIT_ASSERT_CONTAIN_MESSAGE(message + "\nBefore: " + before.str(),
+ expectedError, errors.str());
+ errors.str("");
+ if (util.env()._memFileMapper.repair(
+ *_memFile, util.env(), errors, flags))
+ {
+ CPPUNIT_FAIL("repair() failed to detect: " + message
+ + ": " + errors.str());
+ }
+ CPPUNIT_ASSERT_CONTAIN_MESSAGE(message + "\nBefore: " + before.str(),
+ expectedError, errors.str());
+ std::ostringstream remainingErrors;
+ if (!util.env()._memFileMapper.verify(
+ *_memFile, util.env(), remainingErrors, flags))
+ {
+ CPPUNIT_FAIL("verify() returns issue after repair of: "
+ + message + ": " + remainingErrors.str());
+ }
+ CPPUNIT_ASSERT_MESSAGE(remainingErrors.str(),
+ remainingErrors.str().size() == 0);
+ if (remainingEntries < 0) {
+ if (_memFile->fileExists()) {
+ CPPUNIT_FAIL(message + ": Expected file to not exist anymore");
+ }
+ } else if (dynamic_cast<SimpleMemFileIOBuffer&>(_memFile->getMemFileIO())
+ .getFileHandle().getFileSize() == 0)
+ {
+ std::ostringstream ost;
+ ost << "Expected " << remainingEntries << " to remain in file, "
+ << "but file does not exist\n";
+ CPPUNIT_FAIL(message + ": " + ost.str());
+ } else {
+ if (int64_t(_memFile->getSlotCount()) != remainingEntries) {
+ std::ostringstream ost;
+ ost << "Expected " << remainingEntries << " to remain in file, "
+ << "but found " << _memFile->getSlotCount() << "\n";
+ ost << errors.str() << "\n";
+ ost << "Before: " << before.str() << "\nAfter: ";
+ _memFile->print(ost, true, "");
+ CPPUNIT_FAIL(message + ": " + ost.str());
+ }
+ }
+ }
+}
+
+void
+MemFileV1VerifierTest::tearDown()
+{
+ _memFile.reset(0);
+ SingleDiskMemFileTestUtils::tearDown();
+};
+
+void
+MemFileV1VerifierTest::testVerify()
+{
+ BucketId bucket(16, 0xa);
+ std::unique_ptr<FileSpecification> file;
+ createTestBucket(bucket, 0);
+
+ {
+ MemFilePtr memFilePtr(env()._cache.get(bucket, env(), env().getDirectory()));
+ file.reset(new FileSpecification(memFilePtr->getFile()));
+ env()._cache.clear();
+ }
+ { // Ensure buildTestFile builds a valid file
+ // Initial file should be fine.
+ MemFile memFile(*file, env());
+ std::ostringstream errors;
+ if (!env()._memFileMapper.verify(memFile, env(), errors)) {
+ memFile.print(std::cerr, false, "");
+ CPPUNIT_FAIL("Slotfile failed verification: " + errors.str());
+ }
+ }
+ // Header tests
+ prepareBucket(*this, *file);
+ Header orgheader(getHeader());
+ { // Test wrong version
+ Header header(orgheader);
+ header.setVersion(0xc0edbabe);
+ header.updateChecksum();
+ setHeader(header);
+ verifySlotFile(*this,
+ "400000000000000a.0 is of wrong version",
+ "Faulty version",
+ -1);
+ }
+ { // Test meta data list size bigger than file
+ prepareBucket(*this, *file);
+ Header header(orgheader);
+ header.setMetaDataListSize(0xFFFF);
+ header.updateChecksum();
+ setHeader(header);
+ verifySlotFile(*this,
+ "indicates file is bigger than it physically is",
+ "Too big meta data list size",
+ -1);
+ }
+ { // Test header block size bigger than file
+ prepareBucket(*this, *file);
+ Header header(orgheader);
+ header.setHeaderBlockSize(0xFFFF);
+ header.updateChecksum();
+ setHeader(header);
+ verifySlotFile(*this,
+ "Header indicates file is bigger than it physically is",
+ "Too big header block size",
+ -1);
+ }
+ { // Test wrong header crc
+ prepareBucket(*this, *file);
+ Header header(orgheader);
+ header.setMetaDataListSize(4);
+ setHeader(header);
+ verifySlotFile(*this,
+ "Header checksum mismatch",
+ "Wrong header checksum",
+ -1);
+ }
+ // Meta data tests
+ prepareBucket(*this, *file);
+ MetaSlot slot6(getSlot(6));
+ { // Test extra removes - currently allowed
+ MetaSlot slot7(getSlot(7));
+ MetaSlot s(slot7);
+ s.setTimestamp(Timestamp(s._timestamp.getTime() - 1));
+ s.updateChecksum();
+ setSlot(6, s);
+ s.setTimestamp(Timestamp(s._timestamp.getTime() + 1));
+ s.updateChecksum();
+ setSlot(7, s);
+ std::ostringstream errors;
+ if (!env()._memFileMapper.verify(*_memFile, env(), errors)) {
+ _memFile->print(std::cerr, false, "");
+ std::cerr << errors.str() << "\n";
+ CPPUNIT_FAIL("Supposed to be legal with multiple remove values");
+ }
+ setSlot(7, slot7);
+ }
+ {
+ // Test metadata crc mismatch with "used" flag being accidentally
+ // flipped. Should not inhibit adding of subsequent slots.
+ prepareBucket(*this, *file);
+ MetaSlot s(slot6);
+ s.setUseFlag(false);
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "Slot 6 at timestamp 2001 failed checksum verification",
+ "Crc failure with use flag", 23, false);
+ }
+ { // Test overlapping documents
+ MetaSlot s(slot6);
+ // Direct overlapping header
+ prepareBucket(*this, *file);
+ s.setHeaderPos(0);
+ s.setHeaderSize(51);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "overlaps with slot",
+ "Direct overlapping header", 6, false, false);
+ // Contained header
+ // (contained bit not valid header so fails on other error now)
+ prepareBucket(*this, *file);
+ s.setHeaderPos(176);
+ s.setHeaderSize(80);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "not big enough to contain a document id",
+ "Contained header", 7, false);
+ // Partly overlapping header
+ // (contained bit not valid header so fails on other error now)
+ prepareBucket(*this, *file);
+ s.setHeaderPos(191);
+ s.setHeaderSize(35);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "not big enough to contain a document id",
+ "Partly overlapping header", 7, false);
+ prepareBucket(*this, *file);
+ s.setHeaderPos(185);
+ s.setHeaderSize(33);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "not big enough to contain a document id",
+ "Partly overlapping header (2)", 7, false);
+ // Direct overlapping body
+ prepareBucket(*this, *file);
+ s = slot6;
+ s.setBodyPos(0);
+ s.setBodySize(136);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "Multiple slots with different gids use same body position",
+ "Directly overlapping body", 6, false);
+ // Contained body
+ prepareBucket(*this, *file);
+ s.setBodyPos(10);
+ s.setBodySize(50);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "overlaps with slot",
+ "Contained body", 6, false);
+ CPPUNIT_ASSERT(_memFile->getSlotAtTime(Timestamp(1)) == 0);
+ // Overlapping body
+ prepareBucket(*this, *file);
+ s.setBodyPos(160);
+ s.setBodySize(40);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "overlaps with slot",
+ "Overlapping body", 5, false);
+ CPPUNIT_ASSERT(_memFile->getSlotAtTime(Timestamp(2)) == 0);
+ CPPUNIT_ASSERT(_memFile->getSlotAtTime(Timestamp(1501)) == 0);
+ // Overlapping body, verifying bodies
+ // (Bad body bit should be removed first, so only one slot needs
+ // removing)
+ prepareBucket(*this, *file);
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "Body checksum mismatch",
+ "Overlapping body(2)", 7, true);
+ }
+ { // Test out of bounds
+ MetaSlot s(slot6);
+
+ // Header out of bounds
+ prepareBucket(*this, *file);
+ s.setHeaderPos(500);
+ s.setHeaderSize(5000);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "goes out of bounds",
+ "Header out of bounds", 7, false, false);
+ // Body out of bounds
+ prepareBucket(*this, *file);
+ s = slot6;
+ s.setBodyPos(2400);
+ s.setBodySize(6000);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "goes out of bounds",
+ "Body out of bounds", 7, false);
+ }
+ { // Test timestamp collision
+ prepareBucket(*this, *file);
+ MetaSlot s(slot6);
+ s.setTimestamp(Timestamp(10002));
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "has same timestamp as slot 5",
+ "Timestamp collision", 6, false);
+ }
+ { // Test timestamp out of order
+ prepareBucket(*this, *file);
+ MetaSlot s(slot6);
+ s.setTimestamp(Timestamp(38));
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "Slot 6 is out of timestamp order",
+ "Timestamp out of order", 8, false);
+ }
+ { // Test metadata crc mismatch
+ prepareBucket(*this, *file);
+ MetaSlot s(slot6);
+ s.setTimestamp(Timestamp(40));
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "Slot 6 at timestamp 40 failed checksum verification",
+ "Crc failure", 7, false);
+ }
+ { // Test used after unused
+ // This might actually lose documents after the unused entries.
+ // The memfile will not know about the documents after unused entry.
+ // If the memfile contains changes and writes metadata back due to this,
+ // the following entries will be missing.
+ // (To prevent this repair would have to add metadata entries, but that
+ // may be problems if repair happens at a time where all header or body
+ // data in the file needs to be cached.)
+ prepareBucket(*this, *file);
+ MetaSlot s(slot6);
+ s.setUseFlag(false);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "Slot 7 found after unused entries",
+ "Used after unused", 6, false);
+ }
+ { // Test header blob corrupt
+ prepareBucket(*this, *file);
+ MetaSlot s(slot6);
+ s.setHeaderPos(519);
+ s.setHeaderSize(86);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "Header checksum mismatch",
+ "Corrupt header blob.", 7);
+ }
+ { // Test body blob corrupt
+ prepareBucket(*this, *file);
+ MetaSlot s(slot6);
+ s.setBodyPos(52);
+ s.setBodySize(18);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "Body checksum mismatch",
+ "Corrupt body blob.", 7);
+ }
+ { // Test too long name for header chunk
+ prepareBucket(*this, *file);
+ MetaSlot s(slot6);
+ s.setHeaderPos(160);
+ s.setHeaderSize(33);
+ s.updateChecksum();
+ setSlot(6, s);
+ verifySlotFile(*this,
+ "header is not big enough to contain a document",
+ "Too long name in header.", 7);
+ }
+ { // Test wrong file checksum
+// Currently disabled. Currently only possible to calculate file checksum from
+// memfile now, and memfile object wont be valid.
+/*
+ // First test if we actually have less entries at all..
+ prepareBucket(*this, *file);
+ MetaSlot s(getSlot(7));
+ s.setUseFlag(false);
+ s.updateChecksum();
+ setSlot(7, s, false);
+ s = getSlot(8);
+ s.setUseFlag(false);
+ s.updateChecksum();
+ setSlot(8, s, false);
+ verifySlotFile(*this,
+ "File checksum should have been",
+ "Wrong file checksum in file.", 7, false);
+std::cerr << "U\n";
+ // Then test with different timestamp in remaining document
+ prepareBucket(*this, *file);
+ s = getSlot(6);
+ s.setTimestamp(s._timestamp + 1);
+ s.updateChecksum();
+ setSlot(6, s, false);
+ verifySlotFile(*this,
+ "File checksum should have been",
+ "Wrong file checksum in file.", 9, false);
+std::cerr << "V\n";
+ // Then check with different gid
+ prepareBucket(*this, *file);
+ s = getSlot(6);
+ s._gid = GlobalId("sdfsdfsedsdfsdfsd");
+ s.updateChecksum();
+ setSlot(6, s, false);
+ verifySlotFile(*this,
+ "File checksum should have been",
+ "Wrong file checksum in file.", 9, false, false);
+*/
+ }
+ { // Test that documents not belonging in a bucket is removed
+// Currently disabled. Hard to test. Needs total rewrite
+/*
+ prepareBucket(*this, *file);
+ Blob b(createBlob(43u, "userdoc::0:315", "header", "body"));
+ _memFile->write(b, 80);
+ CPPUNIT_ASSERT_EQUAL(4u, _memFile->getBlobCount());
+ CPPUNIT_ASSERT(_memFile->read(b));
+ verifySlotFile(*this,
+ "belongs in bucket",
+ "Document not belonging there", 9);
+ CPPUNIT_ASSERT_EQUAL(3u, _memFile->getBlobCount());
+*/
+ }
+}
+
+}
+}
diff --git a/memfilepersistence/src/tests/spi/options_builder.h b/memfilepersistence/src/tests/spi/options_builder.h
new file mode 100644
index 00000000000..044e7f1d351
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/options_builder.h
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include <vespa/memfilepersistence/common/environment.h>
+#include <vespa/vespalib/stllike/string.h>
+#include <memory>
+
+namespace storage {
+namespace memfile {
+
+class OptionsBuilder
+{
+ Options _newOptions;
+public:
+ OptionsBuilder(const Options& opts)
+ : _newOptions(opts)
+ {
+ }
+
+ OptionsBuilder& maximumReadThroughGap(uint32_t readThroughGap) {
+ _newOptions._maximumGapToReadThrough = readThroughGap;
+ return *this;
+ }
+
+ OptionsBuilder& initialIndexRead(uint32_t bytesToRead) {
+ _newOptions._initialIndexRead = bytesToRead;
+ return *this;
+ }
+
+ OptionsBuilder& revertTimePeriod(framework::MicroSecTime revertTime) {
+ _newOptions._revertTimePeriod = revertTime;
+ return *this;
+ }
+
+ OptionsBuilder& defaultRemoveDocType(vespalib::stringref typeName) {
+ _newOptions._defaultRemoveDocType = typeName;
+ return *this;
+ }
+
+ OptionsBuilder& maxDocumentVersions(uint32_t maxVersions) {
+ _newOptions._maxDocumentVersions = maxVersions;
+ return *this;
+ }
+
+ std::unique_ptr<Options> build() const {
+ return std::unique_ptr<Options>(new Options(_newOptions));
+ }
+};
+
+} // memfile
+} // storage
+
diff --git a/memfilepersistence/src/tests/spi/providerconformancetest.cpp b/memfilepersistence/src/tests/spi/providerconformancetest.cpp
new file mode 100644
index 00000000000..526f61a812c
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/providerconformancetest.cpp
@@ -0,0 +1,74 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/log/log.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/persistence/conformancetest/conformancetest.h>
+#include <vespa/storageframework/defaultimplementation/component/componentregisterimpl.h>
+#include <vespa/storageframework/defaultimplementation/clock/realclock.h>
+#include <vespa/storageframework/defaultimplementation/memory/memorymanager.h>
+#include <vespa/storageframework/defaultimplementation/memory/simplememorylogic.h>
+#include <vespa/storageframework/generic/memory/memorymanagerinterface.h>
+#include <vespa/memfilepersistence/memfile/memfilecache.h>
+#include <vespa/memfilepersistence/spi/memfilepersistenceprovider.h>
+#include <tests/spi/memfiletestutils.h>
+
+LOG_SETUP(".test.dummyimpl");
+
+namespace storage {
+namespace memfile {
+
+struct ProviderConformanceTest : public spi::ConformanceTest {
+ struct Factory : public PersistenceFactory {
+ framework::defaultimplementation::ComponentRegisterImpl _compRegister;
+ framework::defaultimplementation::RealClock _clock;
+ framework::defaultimplementation::MemoryManager _memoryManager;
+ std::unique_ptr<MemFileCache> cache;
+
+ Factory()
+ : _compRegister(),
+ _clock(),
+ _memoryManager(
+ framework::defaultimplementation::AllocationLogic::UP(
+ new framework::defaultimplementation::SimpleMemoryLogic(
+ _clock, 1024 * 1024 * 1024)))
+ {
+ _compRegister.setClock(_clock);
+ _compRegister.setMemoryManager(_memoryManager);
+ }
+
+ spi::PersistenceProvider::UP
+ getPersistenceImplementation(const document::DocumentTypeRepo::SP& repo,
+ const document::DocumenttypesConfig&)
+ {
+ system("rm -rf vdsroot");
+ system("mkdir -p vdsroot/disks/d0");
+ vdstestlib::DirConfig config(getStandardConfig(true));
+
+ MemFilePersistenceProvider::UP result(
+ new MemFilePersistenceProvider(
+ _compRegister,
+ config.getConfigId()));
+ result->setDocumentRepo(*repo);
+ return spi::PersistenceProvider::UP(result.release());
+ }
+
+ bool
+ supportsRevert() const
+ {
+ return true;
+ }
+ };
+
+ ProviderConformanceTest()
+ : spi::ConformanceTest(PersistenceFactory::UP(new Factory)) {}
+
+ CPPUNIT_TEST_SUITE(ProviderConformanceTest);
+ DEFINE_CONFORMANCE_TESTS();
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(ProviderConformanceTest);
+
+} // memfile
+} // storage
diff --git a/memfilepersistence/src/tests/spi/shared_data_location_tracker_test.cpp b/memfilepersistence/src/tests/spi/shared_data_location_tracker_test.cpp
new file mode 100644
index 00000000000..fbf7badf5e4
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/shared_data_location_tracker_test.cpp
@@ -0,0 +1,111 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/memfilepersistence/memfile/shared_data_location_tracker.h>
+
+namespace storage {
+namespace memfile {
+
+class SharedDataLocationTrackerTest : public CppUnit::TestFixture
+{
+public:
+ void headerIsPassedDownToCacheAccessor();
+ void bodyIsPassedDownToCacheAccessor();
+ void firstInvocationReturnsNewLocation();
+ void multipleInvocationsForSharedSlotReturnSameLocation();
+
+ CPPUNIT_TEST_SUITE(SharedDataLocationTrackerTest);
+ CPPUNIT_TEST(headerIsPassedDownToCacheAccessor);
+ CPPUNIT_TEST(bodyIsPassedDownToCacheAccessor);
+ CPPUNIT_TEST(firstInvocationReturnsNewLocation);
+ CPPUNIT_TEST(multipleInvocationsForSharedSlotReturnSameLocation);
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(SharedDataLocationTrackerTest);
+
+namespace {
+
+using Params = std::pair<Types::DocumentPart, DataLocation>;
+constexpr auto HEADER = Types::HEADER;
+constexpr auto BODY = Types::BODY;
+
+/**
+ * A simple mock of a buffer cache which records all invocations
+ * and returns a location increasing by 100 for each invocation.
+ */
+struct MockBufferCacheCopier : BufferCacheCopier
+{
+ // This is practically _screaming_ for GoogleMock.
+ std::vector<Params> invocations;
+
+ DataLocation doCopyFromSourceToLocal(
+ Types::DocumentPart part,
+ DataLocation sourceLocation) override
+ {
+ Params params(part, sourceLocation);
+ const size_t invocationsBefore = invocations.size();
+ invocations.push_back(params);
+ return DataLocation(invocationsBefore * 100,
+ invocationsBefore * 100 + 100);
+ }
+};
+
+}
+
+void
+SharedDataLocationTrackerTest::headerIsPassedDownToCacheAccessor()
+{
+ MockBufferCacheCopier cache;
+ SharedDataLocationTracker tracker(cache, HEADER);
+ tracker.getOrCreateSharedLocation({0, 100});
+ CPPUNIT_ASSERT_EQUAL(size_t(1), cache.invocations.size());
+ CPPUNIT_ASSERT_EQUAL(Params(HEADER, {0, 100}), cache.invocations[0]);
+}
+
+void
+SharedDataLocationTrackerTest::bodyIsPassedDownToCacheAccessor()
+{
+ MockBufferCacheCopier cache;
+ SharedDataLocationTracker tracker(cache, BODY);
+ tracker.getOrCreateSharedLocation({0, 100});
+ CPPUNIT_ASSERT_EQUAL(size_t(1), cache.invocations.size());
+ CPPUNIT_ASSERT_EQUAL(Params(BODY, {0, 100}), cache.invocations[0]);
+}
+
+void
+SharedDataLocationTrackerTest::firstInvocationReturnsNewLocation()
+{
+ MockBufferCacheCopier cache;
+ SharedDataLocationTracker tracker(cache, HEADER);
+ // Auto-incrementing per cache copy invocation.
+ CPPUNIT_ASSERT_EQUAL(DataLocation(0, 100),
+ tracker.getOrCreateSharedLocation({500, 600}));
+ CPPUNIT_ASSERT_EQUAL(DataLocation(100, 200),
+ tracker.getOrCreateSharedLocation({700, 800}));
+
+ CPPUNIT_ASSERT_EQUAL(size_t(2), cache.invocations.size());
+ CPPUNIT_ASSERT_EQUAL(Params(HEADER, {500, 600}), cache.invocations[0]);
+ CPPUNIT_ASSERT_EQUAL(Params(HEADER, {700, 800}), cache.invocations[1]);
+}
+
+void
+SharedDataLocationTrackerTest
+ ::multipleInvocationsForSharedSlotReturnSameLocation()
+{
+ MockBufferCacheCopier cache;
+ SharedDataLocationTracker tracker(cache, HEADER);
+ CPPUNIT_ASSERT_EQUAL(DataLocation(0, 100),
+ tracker.getOrCreateSharedLocation({500, 600}));
+ // Same source location, thus we can reuse the same destination location
+ // as well.
+ CPPUNIT_ASSERT_EQUAL(DataLocation(0, 100),
+ tracker.getOrCreateSharedLocation({500, 600}));
+
+ CPPUNIT_ASSERT_EQUAL(size_t(1), cache.invocations.size());
+ CPPUNIT_ASSERT_EQUAL(Params(HEADER, {500, 600}), cache.invocations[0]);
+}
+
+} // memfile
+} // storage
+
diff --git a/memfilepersistence/src/tests/spi/simplememfileiobuffertest.cpp b/memfilepersistence/src/tests/spi/simplememfileiobuffertest.cpp
new file mode 100644
index 00000000000..af0466fafe7
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/simplememfileiobuffertest.cpp
@@ -0,0 +1,663 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/memfilepersistence/mapper/simplememfileiobuffer.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <tests/spi/memfiletestutils.h>
+#include <tests/spi/options_builder.h>
+
+namespace storage {
+namespace memfile {
+
+class SimpleMemFileIOBufferTest : public SingleDiskMemFileTestUtils
+{
+ CPPUNIT_TEST_SUITE(SimpleMemFileIOBufferTest);
+ CPPUNIT_TEST(testAddAndReadDocument);
+ CPPUNIT_TEST(testNonExistingLocation);
+ CPPUNIT_TEST(testCopy);
+ CPPUNIT_TEST(testCacheLocation);
+ CPPUNIT_TEST(testPersist);
+ CPPUNIT_TEST(testGetSerializedSize);
+ CPPUNIT_TEST(testRemapLocations);
+ CPPUNIT_TEST(testAlignmentUtilFunctions);
+ CPPUNIT_TEST(testCalculatedCacheSize);
+ CPPUNIT_TEST(testSharedBuffer);
+ CPPUNIT_TEST(testSharedBufferUsage);
+ CPPUNIT_TEST(testHeaderChunkEncoderComputesSizesCorrectly);
+ CPPUNIT_TEST(testHeaderChunkEncoderSerializesIdCorrectly);
+ CPPUNIT_TEST(testHeaderChunkEncoderSerializesHeaderCorrectly);
+ CPPUNIT_TEST(testRemovesCanBeWrittenWithBlankDefaultDocument);
+ CPPUNIT_TEST(testRemovesCanBeWrittenWithIdInferredDoctype);
+ CPPUNIT_TEST(testRemovesWithInvalidDocTypeThrowsException);
+ CPPUNIT_TEST_SUITE_END();
+
+ using BufferType = SimpleMemFileIOBuffer::BufferType;
+ using BufferLP = BufferType::LP;
+ using BufferAllocation = SimpleMemFileIOBuffer::BufferAllocation;
+ using HeaderChunkEncoder = SimpleMemFileIOBuffer::HeaderChunkEncoder;
+ using SimpleMemFileIOBufferUP = std::unique_ptr<SimpleMemFileIOBuffer>;
+
+ BufferAllocation allocateBuffer(size_t sz) {
+ return BufferAllocation(BufferLP(new BufferType(sz)), 0, sz);
+ }
+
+ /**
+ * Create an I/O buffer instance with for a dummy bucket. If removeDocType
+ * is non-empty, remove entries will be written in backwards compatible
+ * mode.
+ */
+ SimpleMemFileIOBufferUP createIoBufferWithDummySpec(
+ vespalib::stringref removeDocType = "");
+
+public:
+ class DummyFileReader : public VersionSerializer {
+ public:
+ virtual FileVersion getFileVersion() { return FileVersion(); }
+ virtual void loadFile(MemFile&, Environment&,
+ Buffer&, uint64_t ) {}
+ virtual FlushResult flushUpdatesToFile(MemFile&, Environment&) {
+ return FlushResult::TooSmall;
+ }
+ virtual void rewriteFile(MemFile&, Environment&) {}
+ virtual bool verify(MemFile&, Environment&,
+ std::ostream&, bool,
+ uint16_t) { return false; };
+ virtual void cacheLocations(MemFileIOInterface&,
+ Environment&,
+ const Options&,
+ DocumentPart,
+ const std::vector<DataLocation>&) {}
+ };
+
+ DummyFileReader dfr;
+
+ void testAddAndReadDocument();
+ void testNonExistingLocation();
+ void testCopy();
+ void testCacheLocation();
+ void testPersist();
+ void testGetSerializedSize();
+ void testRemapLocations();
+ void testAlignmentUtilFunctions();
+ void testCalculatedCacheSize();
+ void testSharedBuffer();
+ void testSharedBufferUsage();
+ void testHeaderChunkEncoderComputesSizesCorrectly();
+ void testHeaderChunkEncoderSerializesIdCorrectly();
+ void testHeaderChunkEncoderSerializesHeaderCorrectly();
+ void testRemovesCanBeWrittenWithBlankDefaultDocument();
+ void testRemovesCanBeWrittenWithIdInferredDoctype();
+ void testRemovesWithInvalidDocTypeThrowsException();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(SimpleMemFileIOBufferTest);
+
+
+void
+SimpleMemFileIOBufferTest::testAddAndReadDocument()
+{
+ FileSpecification fileSpec(BucketId(16, 123), env().getDirectory(), "testfile.0");
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 123,
+ 456,
+ 789,
+ 1234));
+
+ SimpleMemFileIOBuffer buffer(dfr,
+ vespalib::LazyFile::UP(),
+ std::unique_ptr<FileInfo>(new FileInfo),
+ fileSpec,
+ env());
+
+ DataLocation h = buffer.addHeader(*doc);
+ DataLocation b = buffer.addBody(*doc);
+
+ Document::UP newDoc = buffer.getDocumentHeader(*getTypeRepo(), h);
+ buffer.readBody(*getTypeRepo(), b, *newDoc);
+
+ CPPUNIT_ASSERT_EQUAL(*doc, *newDoc);
+ CPPUNIT_ASSERT_EQUAL(true, buffer.isCached(h, HEADER));
+ CPPUNIT_ASSERT_EQUAL(true, buffer.isCached(b, BODY));
+ CPPUNIT_ASSERT_EQUAL(false, buffer.isCached(h, BODY));
+ CPPUNIT_ASSERT_EQUAL(false, buffer.isCached(b, HEADER));
+ CPPUNIT_ASSERT_EQUAL(doc->getId(), buffer.getDocumentId(h));
+}
+
+void
+SimpleMemFileIOBufferTest::testPersist()
+{
+ FileSpecification fileSpec(BucketId(16, 123), env().getDirectory(), "testfile.0");
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 123,
+ 456,
+ 789,
+ 1234));
+
+ SimpleMemFileIOBuffer buffer(dfr,
+ vespalib::LazyFile::UP(),
+ std::unique_ptr<FileInfo>(new FileInfo),
+ fileSpec,
+ env());
+
+ DataLocation h = buffer.addHeader(*doc);
+ DataLocation b = buffer.addBody(*doc);
+
+ CPPUNIT_ASSERT(!buffer.isPersisted(h, HEADER));
+ CPPUNIT_ASSERT(!buffer.isPersisted(b, BODY));
+
+ buffer.persist(HEADER, h, DataLocation(1000, h.size()));
+ buffer.persist(BODY, b, DataLocation(5000, b.size()));
+
+ Document::UP newDoc = buffer.getDocumentHeader(*getTypeRepo(), DataLocation(1000, h.size()));
+ buffer.readBody(*getTypeRepo(), DataLocation(5000, b.size()), *newDoc);
+
+ CPPUNIT_ASSERT(buffer.isPersisted(DataLocation(1000, h.size()), HEADER));
+ CPPUNIT_ASSERT(buffer.isPersisted(DataLocation(5000, b.size()), BODY));
+
+ CPPUNIT_ASSERT_EQUAL(*doc, *newDoc);
+}
+
+void
+SimpleMemFileIOBufferTest::testCopy()
+{
+ FileSpecification fileSpec(BucketId(16, 123), env().getDirectory(), "testfile.0");
+ SimpleMemFileIOBuffer buffer(dfr,
+ vespalib::LazyFile::UP(),
+ std::unique_ptr<FileInfo>(new FileInfo),
+ fileSpec,
+ env());
+
+ for (uint32_t i = 0; i < 10; ++i) {
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 123,
+ 456,
+ 789,
+ 1234));
+
+ DataLocation h = buffer.addHeader(*doc);
+ DataLocation b = buffer.addBody(*doc);
+
+ SimpleMemFileIOBuffer buffer2(dfr,
+ vespalib::LazyFile::UP(),
+ std::unique_ptr<FileInfo>(new FileInfo),
+ fileSpec,
+ env());
+
+ DataLocation h2 = buffer2.copyCache(buffer, HEADER, h);
+ DataLocation b2 = buffer2.copyCache(buffer, BODY, b);
+
+ Document::UP newDoc = buffer2.getDocumentHeader(*getTypeRepo(), h2);
+ buffer2.readBody(*getTypeRepo(), b2, *newDoc);
+
+ CPPUNIT_ASSERT_EQUAL(*doc, *newDoc);
+ }
+}
+
+void
+SimpleMemFileIOBufferTest::testNonExistingLocation()
+{
+ FileSpecification fileSpec(BucketId(16, 123), env().getDirectory(), "testfile.0");
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 123,
+ 456,
+ 789,
+ 1234));
+
+ SimpleMemFileIOBuffer buffer(dfr,
+ vespalib::LazyFile::UP(),
+ std::unique_ptr<FileInfo>(new FileInfo),
+ fileSpec,
+ env());
+
+ DataLocation h = buffer.addHeader(*doc);
+ DataLocation b = buffer.addBody(*doc);
+
+ buffer.clear(HEADER);
+
+ try {
+ Document::UP newDoc = buffer.getDocumentHeader(*getTypeRepo(), h);
+ CPPUNIT_ASSERT(false);
+ } catch (SimpleMemFileIOBuffer::PartNotCachedException& e) {
+ }
+
+ buffer.clear(BODY);
+
+ try {
+ document::Document newDoc;
+ buffer.readBody(*getTypeRepo(), b, newDoc);
+ CPPUNIT_ASSERT(false);
+ } catch (SimpleMemFileIOBuffer::PartNotCachedException& e) {
+ }
+}
+
+void
+SimpleMemFileIOBufferTest::testCacheLocation()
+{
+ FileSpecification fileSpec(BucketId(16, 123), env().getDirectory(), "testfile.0");
+
+ SimpleMemFileIOBuffer buffer(dfr,
+ vespalib::LazyFile::UP(),
+ FileInfo::UP(new FileInfo(100, 10000, 50000)),
+ fileSpec,
+ env());
+
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 123,
+ 456,
+ 789,
+ 1234));
+
+ BufferAllocation headerBuf = buffer.serializeHeader(*doc);
+ BufferAllocation bodyBuf = buffer.serializeBody(*doc);
+
+ DataLocation hloc(1234, headerBuf.getSize());
+ DataLocation bloc(5678, bodyBuf.getSize());
+
+ buffer.cacheLocation(HEADER, hloc, headerBuf.getSharedBuffer(), 0);
+ buffer.cacheLocation(BODY, bloc, bodyBuf.getSharedBuffer(), 0);
+
+ Document::UP newDoc = buffer.getDocumentHeader(*getTypeRepo(), hloc);
+ buffer.readBody(*getTypeRepo(), bloc, *newDoc);
+
+ CPPUNIT_ASSERT_EQUAL(*doc, *newDoc);
+}
+
+void
+SimpleMemFileIOBufferTest::testGetSerializedSize()
+{
+ FileSpecification fileSpec(BucketId(16, 123), env().getDirectory(), "testfile.0");
+
+ SimpleMemFileIOBuffer buffer(dfr,
+ vespalib::LazyFile::UP(),
+ FileInfo::UP(new FileInfo(100, 10000, 50000)),
+ fileSpec,
+ env());
+
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 123,
+ 456,
+ 789,
+ 1234));
+
+ BufferAllocation headerBuf = buffer.serializeHeader(*doc);
+ BufferAllocation bodyBuf = buffer.serializeBody(*doc);
+
+ DataLocation hloc(1234, headerBuf.getSize());
+ DataLocation bloc(5678, bodyBuf.getSize());
+
+ buffer.cacheLocation(HEADER, hloc, headerBuf.getSharedBuffer(), 0);
+ buffer.cacheLocation(BODY, bloc, bodyBuf.getSharedBuffer(), 0);
+
+ vespalib::nbostream serializedHeader;
+ doc->serializeHeader(serializedHeader);
+
+ vespalib::nbostream serializedBody;
+ doc->serializeBody(serializedBody);
+
+ CPPUNIT_ASSERT_EQUAL(uint32_t(serializedHeader.size()),
+ buffer.getSerializedSize(HEADER, hloc));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(serializedBody.size()),
+ buffer.getSerializedSize(BODY, bloc));
+}
+
+// Test that remapping does not overwrite datalocations that it has
+// already updated
+void
+SimpleMemFileIOBufferTest::testRemapLocations()
+{
+ FileSpecification fileSpec(BucketId(16, 123), env().getDirectory(), "testfile.0");
+
+ SimpleMemFileIOBuffer buffer(dfr,
+ vespalib::LazyFile::UP(),
+ FileInfo::UP(new FileInfo(100, 10000, 50000)),
+ fileSpec,
+ env());
+
+ document::Document::SP doc(createRandomDocumentAtLocation(
+ 123,
+ 100,
+ 100));
+ BufferAllocation headerBuf = buffer.serializeHeader(*doc);
+ BufferAllocation bodyBuf = buffer.serializeBody(*doc);
+
+ document::Document::SP doc2(createRandomDocumentAtLocation(
+ 123,
+ 100,
+ 100));
+
+ BufferAllocation headerBuf2 = buffer.serializeHeader(*doc2);
+ BufferAllocation bodyBuf2 = buffer.serializeBody(*doc2);
+
+ DataLocation hloc(30000, headerBuf.getSize());
+ DataLocation hloc2(0, headerBuf2.getSize());
+ DataLocation hloc3(10000, hloc2._size);
+
+ buffer.cacheLocation(HEADER, hloc, headerBuf.getSharedBuffer(), 0);
+ buffer.cacheLocation(HEADER, hloc2, headerBuf2.getSharedBuffer(), 0);
+
+ std::map<DataLocation, DataLocation> remapping;
+ remapping[hloc2] = hloc;
+ remapping[hloc] = hloc3;
+
+ buffer.remapAndPersistAllLocations(HEADER, remapping);
+
+ Document::UP newDoc = buffer.getDocumentHeader(*getTypeRepo(), hloc3);
+ document::ByteBuffer bbuf(bodyBuf.getBuffer(), bodyBuf.getSize());
+ newDoc->deserializeBody(*getTypeRepo(), bbuf);
+
+ CPPUNIT_ASSERT_EQUAL(*doc, *newDoc);
+
+ Document::UP newDoc2 = buffer.getDocumentHeader(*getTypeRepo(), hloc);
+ document::ByteBuffer bbuf2(bodyBuf.getBuffer(), bodyBuf.getSize());
+ newDoc2->deserializeBody(*getTypeRepo(), bbuf2);
+ CPPUNIT_ASSERT_EQUAL(*doc2, *newDoc2);
+}
+
+/**
+ * Not technically a part of SimpleMemFileIOBuffer, but used by it and
+ * currently contained within its header file. Move test somewhere else
+ * if the code itself is moved.
+ */
+void
+SimpleMemFileIOBufferTest::testAlignmentUtilFunctions()
+{
+ using namespace util;
+ CPPUNIT_ASSERT_EQUAL(size_t(0), alignUpPow2<4096>(0));
+ CPPUNIT_ASSERT_EQUAL(size_t(4096), alignUpPow2<4096>(1));
+ CPPUNIT_ASSERT_EQUAL(size_t(4096), alignUpPow2<4096>(512));
+ CPPUNIT_ASSERT_EQUAL(size_t(4096), alignUpPow2<4096>(4096));
+ CPPUNIT_ASSERT_EQUAL(size_t(8192), alignUpPow2<4096>(4097));
+ CPPUNIT_ASSERT_EQUAL(size_t(32), alignUpPow2<16>(20));
+ CPPUNIT_ASSERT_EQUAL(size_t(32), alignUpPow2<32>(20));
+ CPPUNIT_ASSERT_EQUAL(size_t(64), alignUpPow2<64>(20));
+ CPPUNIT_ASSERT_EQUAL(size_t(128), alignUpPow2<128>(20));
+
+ CPPUNIT_ASSERT_EQUAL(uint32_t(0), nextPow2(0));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), nextPow2(1));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(4), nextPow2(3));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(16), nextPow2(15));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(64), nextPow2(40));
+ CPPUNIT_ASSERT_EQUAL(uint32_t(64), nextPow2(64));
+}
+
+/**
+ * Test that allocated buffers are correctly reported with their sizes
+ * rounded up to account for mmap overhead.
+ */
+void
+SimpleMemFileIOBufferTest::testCalculatedCacheSize()
+{
+ FileSpecification fileSpec(BucketId(16, 123),
+ env().getDirectory(), "testfile.0");
+ SimpleMemFileIOBuffer buffer(dfr,
+ vespalib::LazyFile::UP(),
+ std::unique_ptr<FileInfo>(new FileInfo),
+ fileSpec,
+ env());
+
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buffer.getCachedSize(HEADER));
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buffer.getCachedSize(BODY));
+
+ // All buffers are on a 4k page granularity.
+ BufferAllocation sharedHeaderBuffer(allocateBuffer(1500)); // -> 4096
+ buffer.cacheLocation(HEADER, DataLocation(0, 85),
+ sharedHeaderBuffer.getSharedBuffer(), 0);
+ CPPUNIT_ASSERT_EQUAL(size_t(4096), buffer.getCachedSize(HEADER));
+
+ buffer.cacheLocation(HEADER, DataLocation(200, 100),
+ sharedHeaderBuffer.getSharedBuffer(), 85);
+ CPPUNIT_ASSERT_EQUAL(size_t(4096), buffer.getCachedSize(HEADER));
+
+ BufferAllocation singleHeaderBuffer(allocateBuffer(200)); // -> 4096
+ buffer.cacheLocation(HEADER, DataLocation(0, 100),
+ singleHeaderBuffer.getSharedBuffer(), 0);
+ CPPUNIT_ASSERT_EQUAL(size_t(8192), buffer.getCachedSize(HEADER));
+
+ BufferAllocation singleBodyBuffer(allocateBuffer(300)); // -> 4096
+ buffer.cacheLocation(BODY, DataLocation(0, 100),
+ singleBodyBuffer.getSharedBuffer(), 0);
+ CPPUNIT_ASSERT_EQUAL(size_t(4096), buffer.getCachedSize(BODY));
+
+ buffer.clear(HEADER);
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buffer.getCachedSize(HEADER));
+
+ buffer.clear(BODY);
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buffer.getCachedSize(BODY));
+}
+
+void
+SimpleMemFileIOBufferTest::testSharedBuffer()
+{
+ typedef SimpleMemFileIOBuffer::SharedBuffer SharedBuffer;
+
+ {
+ SharedBuffer buf(1024);
+ CPPUNIT_ASSERT_EQUAL(size_t(1024), buf.getSize());
+ CPPUNIT_ASSERT_EQUAL(size_t(1024), buf.getFreeSize());
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buf.getUsedSize());
+ CPPUNIT_ASSERT(buf.hasRoomFor(1024));
+ CPPUNIT_ASSERT(!buf.hasRoomFor(1025));
+
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buf.allocate(13));
+ // Allocation should be rounded up to nearest alignment.
+ // TODO: is this even necessary?
+ CPPUNIT_ASSERT_EQUAL(size_t(16), buf.getUsedSize());
+ CPPUNIT_ASSERT_EQUAL(size_t(1008), buf.getFreeSize());
+ CPPUNIT_ASSERT(buf.hasRoomFor(1008));
+ CPPUNIT_ASSERT(!buf.hasRoomFor(1009));
+ CPPUNIT_ASSERT_EQUAL(size_t(16), buf.allocate(1));
+ CPPUNIT_ASSERT_EQUAL(size_t(24), buf.getUsedSize());
+
+ CPPUNIT_ASSERT_EQUAL(size_t(24), buf.allocate(999));
+ CPPUNIT_ASSERT(!buf.hasRoomFor(1));
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buf.getFreeSize());
+ CPPUNIT_ASSERT_EQUAL(size_t(1024), buf.getUsedSize());
+ }
+ // Test exact fit.
+ {
+ SharedBuffer buf(1024);
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buf.allocate(1024));
+ CPPUNIT_ASSERT(!buf.hasRoomFor(1));
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buf.getFreeSize());
+ CPPUNIT_ASSERT_EQUAL(size_t(1024), buf.getUsedSize());
+ }
+ // Test 512-byte alignment.
+ {
+ SharedBuffer buf(1024);
+ CPPUNIT_ASSERT(buf.hasRoomFor(1000, SharedBuffer::ALIGN_512_BYTES));
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buf.allocate(10));
+ CPPUNIT_ASSERT(!buf.hasRoomFor(1000, SharedBuffer::ALIGN_512_BYTES));
+ CPPUNIT_ASSERT(!buf.hasRoomFor(513, SharedBuffer::ALIGN_512_BYTES));
+ CPPUNIT_ASSERT(buf.hasRoomFor(512, SharedBuffer::ALIGN_512_BYTES));
+ CPPUNIT_ASSERT_EQUAL(size_t(512), buf.allocate(512, SharedBuffer::ALIGN_512_BYTES));
+ CPPUNIT_ASSERT_EQUAL(size_t(0), buf.getFreeSize());
+ CPPUNIT_ASSERT_EQUAL(size_t(1024), buf.getUsedSize());
+ }
+}
+
+void
+SimpleMemFileIOBufferTest::testSharedBufferUsage()
+{
+ FileSpecification fileSpec(BucketId(16, 123),
+ env().getDirectory(), "testfile.0");
+ SimpleMemFileIOBuffer ioBuf(dfr,
+ vespalib::LazyFile::UP(),
+ std::unique_ptr<FileInfo>(new FileInfo),
+ fileSpec,
+ env());
+
+ const size_t threshold = SimpleMemFileIOBuffer::WORKING_BUFFER_SIZE;
+
+ // Brand new allocation
+ BufferAllocation ba(ioBuf.allocateBuffer(HEADER, 1));
+ CPPUNIT_ASSERT(ba.buf.get());
+ CPPUNIT_ASSERT_EQUAL(uint32_t(0), ba.pos);
+ CPPUNIT_ASSERT_EQUAL(uint32_t(1), ba.size);
+ // Should reuse buffer, but get other offset
+ BufferAllocation ba2(ioBuf.allocateBuffer(HEADER, 500));
+ CPPUNIT_ASSERT_EQUAL(ba.buf.get(), ba2.buf.get());
+ CPPUNIT_ASSERT_EQUAL(uint32_t(8), ba2.pos);
+ CPPUNIT_ASSERT_EQUAL(uint32_t(500), ba2.size);
+ CPPUNIT_ASSERT_EQUAL(size_t(512), ba2.buf->getUsedSize());
+
+ // Allocate a buffer so big that it should get its own buffer instance
+ BufferAllocation ba3(ioBuf.allocateBuffer(HEADER, threshold));
+ CPPUNIT_ASSERT(ba3.buf.get() != ba2.buf.get());
+ CPPUNIT_ASSERT_EQUAL(uint32_t(0), ba3.pos);
+ CPPUNIT_ASSERT_EQUAL(uint32_t(threshold), ba3.size);
+
+ // But smaller allocs should still be done from working buffer
+ BufferAllocation ba4(ioBuf.allocateBuffer(HEADER, 512));
+ CPPUNIT_ASSERT_EQUAL(ba.buf.get(), ba4.buf.get());
+ CPPUNIT_ASSERT_EQUAL(uint32_t(512), ba4.pos);
+ CPPUNIT_ASSERT_EQUAL(uint32_t(512), ba4.size);
+ CPPUNIT_ASSERT_EQUAL(size_t(1024), ba4.buf->getUsedSize());
+
+ // Allocate lots of smaller buffers from the same buffer until we run out.
+ while (true) {
+ BufferAllocation tmp(ioBuf.allocateBuffer(HEADER, 1024));
+ CPPUNIT_ASSERT_EQUAL(ba.buf.get(), tmp.buf.get());
+ if (!tmp.buf->hasRoomFor(2048)) {
+ break;
+ }
+ }
+ BufferAllocation ba5(ioBuf.allocateBuffer(HEADER, 2048));
+ CPPUNIT_ASSERT(ba5.buf.get() != ba.buf.get());
+ CPPUNIT_ASSERT_EQUAL(uint32_t(0), ba5.pos);
+ CPPUNIT_ASSERT_EQUAL(uint32_t(2048), ba5.size);
+
+ // Allocating for different part should get different buffer.
+ BufferAllocation ba6(ioBuf.allocateBuffer(BODY, 128));
+ CPPUNIT_ASSERT(ba6.buf.get() != ba5.buf.get());
+ CPPUNIT_ASSERT_EQUAL(uint32_t(0), ba6.pos);
+ CPPUNIT_ASSERT_EQUAL(uint32_t(128), ba6.size);
+}
+
+void
+SimpleMemFileIOBufferTest::testHeaderChunkEncoderComputesSizesCorrectly()
+{
+ document::Document::SP doc(createRandomDocumentAtLocation(123, 100, 100));
+
+ std::string idString = doc->getId().toString();
+ HeaderChunkEncoder encoder(doc->getId());
+ // Without document, payload is: 3x u32 + doc id string (no zero term).
+ CPPUNIT_ASSERT_EQUAL(sizeof(uint32_t)*3 + idString.size(),
+ static_cast<size_t>(encoder.encodedSize()));
+
+ encoder.bufferDocument(*doc);
+ vespalib::nbostream stream;
+ doc->serializeHeader(stream);
+ // With document, add size of serialized document to the mix.
+ CPPUNIT_ASSERT_EQUAL(sizeof(uint32_t)*3 + idString.size() + stream.size(),
+ static_cast<size_t>(encoder.encodedSize()));
+}
+
+SimpleMemFileIOBufferTest::SimpleMemFileIOBufferUP
+SimpleMemFileIOBufferTest::createIoBufferWithDummySpec(
+ vespalib::stringref removeDocType)
+{
+ FileSpecification fileSpec(BucketId(16, 123),
+ env().getDirectory(), "testfile.0");
+ // Override config.
+ auto options = env().acquireConfigReadLock().options();
+ env().acquireConfigWriteLock().setOptions(
+ OptionsBuilder(*options)
+ .defaultRemoveDocType(removeDocType)
+ .build());
+
+ SimpleMemFileIOBufferUP ioBuf(
+ new SimpleMemFileIOBuffer(
+ dfr,
+ vespalib::LazyFile::UP(),
+ std::unique_ptr<FileInfo>(new FileInfo),
+ fileSpec,
+ env()));
+ return ioBuf;
+}
+
+void
+SimpleMemFileIOBufferTest::testHeaderChunkEncoderSerializesIdCorrectly()
+{
+ document::Document::SP doc(createRandomDocumentAtLocation(123, 100, 100));
+ HeaderChunkEncoder encoder(doc->getId());
+
+ SimpleMemFileIOBufferUP ioBuf(createIoBufferWithDummySpec());
+
+ BufferAllocation buf(ioBuf->allocateBuffer(HEADER, encoder.encodedSize()));
+ encoder.writeTo(buf);
+ DataLocation newLoc = ioBuf->addLocation(HEADER, buf);
+ document::DocumentId checkId = ioBuf->getDocumentId(newLoc);
+
+ CPPUNIT_ASSERT_EQUAL(doc->getId(), checkId);
+}
+
+void
+SimpleMemFileIOBufferTest::testHeaderChunkEncoderSerializesHeaderCorrectly()
+{
+ document::Document::SP doc(createRandomDocumentAtLocation(123, 100, 100));
+ HeaderChunkEncoder encoder(doc->getId());
+ encoder.bufferDocument(*doc);
+
+ SimpleMemFileIOBufferUP ioBuf(createIoBufferWithDummySpec());
+ BufferAllocation buf(ioBuf->allocateBuffer(HEADER, encoder.encodedSize()));
+ encoder.writeTo(buf);
+ DataLocation newLoc = ioBuf->addLocation(HEADER, buf);
+ Document::UP checkDoc = ioBuf->getDocumentHeader(*getTypeRepo(), newLoc);
+
+ CPPUNIT_ASSERT_EQUAL(doc->getId(), checkDoc->getId());
+ CPPUNIT_ASSERT_EQUAL(doc->getType(), checkDoc->getType());
+}
+
+void
+SimpleMemFileIOBufferTest::testRemovesCanBeWrittenWithBlankDefaultDocument()
+{
+ SimpleMemFileIOBufferUP ioBuf(createIoBufferWithDummySpec("testdoctype1"));
+
+ document::DocumentId id("userdoc:yarn:12345:fluff");
+ DataLocation loc(ioBuf->addDocumentIdOnlyHeader(id, *getTypeRepo()));
+ // Despite adding with document id only, we should now actually have a
+ // valid document header. Will fail with a DeserializeException if no
+ // header has been written.
+ Document::UP removeWithHeader(
+ ioBuf->getDocumentHeader(*getTypeRepo(), loc));
+ CPPUNIT_ASSERT_EQUAL(removeWithHeader->getId(), id);
+ CPPUNIT_ASSERT_EQUAL(removeWithHeader->getType(),
+ *getTypeRepo()->getDocumentType("testdoctype1"));
+}
+
+void
+SimpleMemFileIOBufferTest::testRemovesCanBeWrittenWithIdInferredDoctype()
+{
+ SimpleMemFileIOBufferUP ioBuf(createIoBufferWithDummySpec("testdoctype1"));
+
+ document::DocumentId id("id:yarn:testdoctype2:n=12345:fluff");
+ DataLocation loc(ioBuf->addDocumentIdOnlyHeader(id, *getTypeRepo()));
+ // Since document id contains an explicit document type, the blank remove
+ // document header should be written with that type instead of the one
+ // provided as default via config.
+ Document::UP removeWithHeader(
+ ioBuf->getDocumentHeader(*getTypeRepo(), loc));
+ CPPUNIT_ASSERT_EQUAL(removeWithHeader->getId(), id);
+ CPPUNIT_ASSERT_EQUAL(removeWithHeader->getType(),
+ *getTypeRepo()->getDocumentType("testdoctype2"));
+}
+
+void
+SimpleMemFileIOBufferTest::testRemovesWithInvalidDocTypeThrowsException()
+{
+ SimpleMemFileIOBufferUP ioBuf(createIoBufferWithDummySpec("testdoctype1"));
+
+ document::DocumentId id("id:yarn:nosuchtype:n=12345:fluff");
+ try {
+ DataLocation loc(ioBuf->addDocumentIdOnlyHeader(id, *getTypeRepo()));
+ CPPUNIT_FAIL("No exception thrown on bad doctype");
+ } catch (const vespalib::Exception& e) {
+ CPPUNIT_ASSERT(e.getMessage().find("Could not serialize document "
+ "for remove with unknown doctype "
+ "'nosuchtype'")
+ != std::string::npos);
+ }
+}
+
+} // memfile
+} // storage
diff --git a/memfilepersistence/src/tests/spi/simulatedfailurefile.h b/memfilepersistence/src/tests/spi/simulatedfailurefile.h
new file mode 100644
index 00000000000..1ded927a3d1
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/simulatedfailurefile.h
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include <tests/spi/memfiletestutils.h>
+#include <tests/spi/logginglazyfile.h>
+
+namespace storage {
+namespace memfile {
+
+class SimulatedFailureLazyFile : public vespalib::LazyFile
+{
+ mutable int _readOpsBeforeFailure;
+ mutable int _writeOpsBeforeFailure;
+public:
+ class Factory : public Environment::LazyFileFactory {
+ public:
+ Factory()
+ : _readOpsBeforeFailure(-1),
+ _writeOpsBeforeFailure(0)
+ {
+ }
+ vespalib::LazyFile::UP createFile(const std::string& fileName) const {
+ return vespalib::LazyFile::UP(
+ new SimulatedFailureLazyFile(fileName,
+ vespalib::File::DIRECTIO,
+ _readOpsBeforeFailure,
+ _writeOpsBeforeFailure));
+ }
+
+ void setReadOpsBeforeFailure(int ops) {
+ _readOpsBeforeFailure = ops;
+ }
+
+ void setWriteOpsBeforeFailure(int ops) {
+ _writeOpsBeforeFailure = ops;
+ }
+ private:
+ int _readOpsBeforeFailure;
+ int _writeOpsBeforeFailure;
+ };
+
+ SimulatedFailureLazyFile(
+ const std::string& filename,
+ int flags,
+ int readOpsBeforeFailure,
+ int writeOpsBeforeFailure)
+ : LazyFile(filename, flags),
+ _readOpsBeforeFailure(readOpsBeforeFailure),
+ _writeOpsBeforeFailure(writeOpsBeforeFailure)
+ {
+ }
+
+ off_t write(const void *buf, size_t bufsize, off_t offset)
+ {
+ if (_writeOpsBeforeFailure == 0) {
+ throw vespalib::IoException(
+ "A simulated I/O write exception was triggered",
+ vespalib::IoException::CORRUPT_DATA, VESPA_STRLOC);
+ }
+ --_writeOpsBeforeFailure;
+ return vespalib::LazyFile::write(buf, bufsize, offset);
+ }
+
+ size_t read(void *buf, size_t bufsize, off_t offset) const
+ {
+ if (_readOpsBeforeFailure == 0) {
+ throw vespalib::IoException(
+ "A simulated I/O read exception was triggered",
+ vespalib::IoException::CORRUPT_DATA, VESPA_STRLOC);
+ }
+ --_readOpsBeforeFailure;
+ return vespalib::LazyFile::read(buf, bufsize, offset);
+ }
+};
+
+} // ns memfile
+} // ns storage
+
diff --git a/memfilepersistence/src/tests/spi/splitoperationhandlertest.cpp b/memfilepersistence/src/tests/spi/splitoperationhandlertest.cpp
new file mode 100644
index 00000000000..75eab5c2972
--- /dev/null
+++ b/memfilepersistence/src/tests/spi/splitoperationhandlertest.cpp
@@ -0,0 +1,213 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+
+#include <vespa/document/datatype/documenttype.h>
+#include <tests/spi/memfiletestutils.h>
+#include <tests/spi/simulatedfailurefile.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+
+using document::DocumentType;
+
+namespace storage {
+namespace memfile {
+namespace {
+ spi::LoadType defaultLoadType(0, "default");
+}
+
+class SplitOperationHandlerTest : public SingleDiskMemFileTestUtils
+{
+
+ void doTestMultiDisk(uint16_t sourceDisk,
+ uint16_t targetDisk0,
+ uint16_t targetDisk1);
+
+
+ CPPUNIT_TEST_SUITE(SplitOperationHandlerTest);
+ CPPUNIT_TEST(testSimple);
+ CPPUNIT_TEST(testMultiDisk);
+ CPPUNIT_TEST(testMultiDiskNonZeroSourceIndex);
+ CPPUNIT_TEST(testExceptionDuringSplittingEvictsAllBuckets);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void testSimple();
+ void testMultiDisk();
+ void testMultiDiskNonZeroSourceIndex();
+ void testExceptionDuringSplittingEvictsAllBuckets();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(SplitOperationHandlerTest);
+
+void
+SplitOperationHandlerTest::testSimple()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ setupDisks(1);
+
+ for (uint32_t i = 0; i < 100; i++) {
+ uint32_t location = 4;
+ if (i % 2 == 0) {
+ location |= (1 << 16);
+ }
+
+ doPut(location, Timestamp(1000 + i));
+ }
+ flush(document::BucketId(16, 4));
+
+ env()._cache.clear();
+
+ document::BucketId sourceBucket = document::BucketId(16, 4);
+ document::BucketId target1 = document::BucketId(17, 4);
+ document::BucketId target2 = document::BucketId(17, 4 | (1 << 16));
+
+ SplitOperationHandler handler(env());
+ spi::Result result = getPersistenceProvider().split(
+ spi::Bucket(sourceBucket, spi::PartitionId(0)),
+ spi::Bucket(target1, spi::PartitionId(0)),
+ spi::Bucket(target2, spi::PartitionId(0)),
+ context);
+
+ env()._cache.clear();
+
+ {
+ MemFilePtr file(handler.getMemFile(sourceBucket, 0));
+ CPPUNIT_ASSERT_EQUAL(0, (int)file->getSlotCount());
+ }
+
+ {
+ MemFilePtr file(handler.getMemFile(target1, 0));
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ for (uint32_t i = 0; i < file->getSlotCount(); ++i) {
+ file->getDocument((*file)[i], ALL);
+ }
+ }
+
+ {
+ MemFilePtr file(handler.getMemFile(target2, 0));
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ for (uint32_t i = 0; i < file->getSlotCount(); ++i) {
+ file->getDocument((*file)[i], ALL);
+ }
+ }
+}
+
+void
+SplitOperationHandlerTest::doTestMultiDisk(uint16_t sourceDisk,
+ uint16_t targetDisk0,
+ uint16_t targetDisk1)
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ setupDisks(3);
+
+ for (uint32_t i = 0; i < 100; i++) {
+ uint32_t location = 4;
+ if (i % 2 == 0) {
+ location |= (1 << 16);
+ }
+
+ doPutOnDisk(sourceDisk, location, Timestamp(1000 + i));
+ }
+ flush(document::BucketId(16, 4));
+
+ env()._cache.clear();
+
+ document::BucketId sourceBucket = document::BucketId(16, 4);
+ document::BucketId target1 = document::BucketId(17, 4);
+ document::BucketId target2 = document::BucketId(17, 4 | (1 << 16));
+
+ SplitOperationHandler handler(env());
+ spi::Result result = getPersistenceProvider().split(
+ spi::Bucket(sourceBucket, spi::PartitionId(sourceDisk)),
+ spi::Bucket(target1, spi::PartitionId(targetDisk0)),
+ spi::Bucket(target2, spi::PartitionId(targetDisk1)),
+ context);
+
+ env()._cache.clear();
+
+ {
+ MemFilePtr file(handler.getMemFile(sourceBucket, sourceDisk));
+ CPPUNIT_ASSERT_EQUAL(0, (int)file->getSlotCount());
+ }
+
+ {
+ MemFilePtr file(handler.getMemFile(target1, targetDisk0));
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ for (uint32_t i = 0; i < file->getSlotCount(); ++i) {
+ file->getDocument((*file)[i], ALL);
+ }
+ }
+
+ {
+ MemFilePtr file(handler.getMemFile(target2, targetDisk1));
+ CPPUNIT_ASSERT_EQUAL(50, (int)file->getSlotCount());
+ for (uint32_t i = 0; i < file->getSlotCount(); ++i) {
+ file->getDocument((*file)[i], ALL);
+ }
+ }
+}
+
+void
+SplitOperationHandlerTest::testMultiDisk()
+{
+ doTestMultiDisk(0, 1, 2);
+}
+
+void
+SplitOperationHandlerTest::testMultiDiskNonZeroSourceIndex()
+{
+ doTestMultiDisk(1, 2, 0);
+}
+
+void
+SplitOperationHandlerTest::testExceptionDuringSplittingEvictsAllBuckets()
+{
+ spi::Context context(defaultLoadType, spi::Priority(0),
+ spi::Trace::TraceLevel(0));
+ setupDisks(1);
+
+ for (uint32_t i = 0; i < 100; i++) {
+ uint32_t location = 4;
+ if (i % 2 == 0) {
+ location |= (1 << 16);
+ }
+
+ doPut(location, Timestamp(1000 + i));
+ }
+ flush(document::BucketId(16, 4));
+
+ simulateIoErrorsForSubsequentlyOpenedFiles();
+
+ document::BucketId sourceBucket(16, 4);
+ document::BucketId target1(17, 4);
+ document::BucketId target2(17, 4 | (1 << 16));
+
+ try {
+ SplitOperationHandler handler(env());
+ spi::Result result = getPersistenceProvider().split(
+ spi::Bucket(sourceBucket, spi::PartitionId(0)),
+ spi::Bucket(target1, spi::PartitionId(0)),
+ spi::Bucket(target2, spi::PartitionId(0)),
+ context);
+ CPPUNIT_FAIL("Exception not thrown on flush failure");
+ } catch (std::exception&) {
+ }
+
+ CPPUNIT_ASSERT(!env()._cache.contains(sourceBucket));
+ CPPUNIT_ASSERT(!env()._cache.contains(target1));
+ CPPUNIT_ASSERT(!env()._cache.contains(target2));
+
+ unSimulateIoErrorsForSubsequentlyOpenedFiles();
+
+ // Source must not have been deleted
+ {
+ SplitOperationHandler handler(env());
+ MemFilePtr file(handler.getMemFile(sourceBucket, 0));
+ CPPUNIT_ASSERT_EQUAL(100, (int)file->getSlotCount());
+ }
+}
+
+}
+
+}
diff --git a/memfilepersistence/src/tests/testhelper.cpp b/memfilepersistence/src/tests/testhelper.cpp
new file mode 100644
index 00000000000..40a3512e400
--- /dev/null
+++ b/memfilepersistence/src/tests/testhelper.cpp
@@ -0,0 +1,124 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+#include <tests/testhelper.h>
+
+#include <vespa/log/log.h>
+#include <vespa/vespalib/io/fileutil.h>
+
+LOG_SETUP(".testhelper");
+
+namespace storage {
+
+void addStorageDistributionConfig(vdstestlib::DirConfig& dc)
+{
+ vdstestlib::DirConfig::Config* config;
+ config = &dc.getConfig("stor-distribution", true);
+ config->clear();
+ config->set("group[1]");
+ config->set("group[0].name", "foo");
+ config->set("group[0].index", "0");
+ config->set("group[0].nodes[50]");
+
+ for (uint32_t i = 0; i < 50; i++) {
+ std::ostringstream key; key << "group[0].nodes[" << i << "].index";
+ std::ostringstream val; val << i;
+ config->set(key.str(), val.str());
+ }
+}
+
+vdstestlib::DirConfig getStandardConfig(bool storagenode) {
+ vdstestlib::DirConfig dc;
+ vdstestlib::DirConfig::Config* config;
+ config = &dc.addConfig("stor-cluster");
+ config = &dc.addConfig("load-type");
+ config = &dc.addConfig("bucket");
+ config = &dc.addConfig("messagebus");
+ config = &dc.addConfig("stor-prioritymapping");
+ config = &dc.addConfig("stor-bucketdbupdater");
+ config = &dc.addConfig("metricsmanager");
+ config->set("consumer[1]");
+ config->set("consumer[0].name", "\"status\"");
+ config->set("consumer[0].addedmetrics[1]");
+ config->set("consumer[0].addedmetrics[0]", "\"*\"");
+ config = &dc.addConfig("stor-communicationmanager");
+ config->set("rpcport", "0");
+ config->set("mbusport", "0");
+ config = &dc.addConfig("stor-bucketdb");
+ config->set("chunklevel", "0");
+ config = &dc.addConfig("stor-distributormanager");
+ config = &dc.addConfig("stor-opslogger");
+ config = &dc.addConfig("stor-memfilepersistence");
+ // Easier to see what goes wrong with only 1 thread per disk.
+ config->set("minimum_file_meta_slots", "2");
+ config->set("minimum_file_header_block_size", "368");
+ config->set("minimum_file_size", "4096");
+ config->set("threads[1]");
+ config->set("threads[0].lowestpri 255");
+ config->set("dir_spread", "4");
+ config->set("dir_levels", "0");
+ // Unit tests typically use fake low time values, so don't complain
+ // about them or compact/delete them by default. Override in tests testing that
+ // behavior
+ config = &dc.addConfig("persistence");
+ config->set("keep_remove_time_period", "2000000000");
+ config->set("revert_time_period", "2000000000");
+ config = &dc.addConfig("stor-bouncer");
+ config = &dc.addConfig("stor-integritychecker");
+ config = &dc.addConfig("stor-bucketmover");
+ config = &dc.addConfig("stor-messageforwarder");
+ config = &dc.addConfig("stor-server");
+ config->set("enable_dead_lock_detector", "false");
+ config->set("enable_dead_lock_detector_warnings", "false");
+ config->set("max_merges_per_node", "25");
+ config->set("max_merge_queue_size", "20");
+ config->set("root_folder",
+ (storagenode ? "vdsroot" : "vdsroot.distributor"));
+ config->set("is_distributor",
+ (storagenode ? "false" : "true"));
+ config = &dc.addConfig("stor-devices");
+ config->set("root_folder",
+ (storagenode ? "vdsroot" : "vdsroot.distributor"));
+ config = &dc.addConfig("stor-status");
+ config->set("httpport", "0");
+ config = &dc.addConfig("stor-visitor");
+ config->set("defaultdocblocksize", "8192");
+ // By default, need "old" behaviour of maxconcurrent
+ config->set("maxconcurrentvisitors_fixed", "4");
+ config->set("maxconcurrentvisitors_variable", "0");
+ config = &dc.addConfig("stor-visitordispatcher");
+ addFileConfig(dc, "documenttypes", "config-doctypes.cfg");
+ addStorageDistributionConfig(dc);
+ return dc;
+}
+
+void addFileConfig(vdstestlib::DirConfig& dc,
+ const std::string& configDefName,
+ const std::string& fileName)
+{
+ vdstestlib::DirConfig::Config* config;
+ config = &dc.getConfig(configDefName, true);
+ config->clear();
+ std::ifstream in(fileName.c_str());
+ std::string line;
+ while (std::getline(in, line, '\n')) {
+ std::string::size_type pos = line.find(' ');
+ if (pos == std::string::npos) {
+ config->set(line);
+ } else {
+ config->set(line.substr(0, pos), line.substr(pos + 1));
+ }
+ }
+ in.close();
+}
+
+TestName::TestName(const std::string& n)
+ : name(n)
+{
+ LOG(debug, "Starting test %s", name.c_str());
+}
+
+TestName::~TestName() {
+ LOG(debug, "Done with test %s", name.c_str());
+}
+
+} // storage
diff --git a/memfilepersistence/src/tests/testhelper.h b/memfilepersistence/src/tests/testhelper.h
new file mode 100644
index 00000000000..4445086d300
--- /dev/null
+++ b/memfilepersistence/src/tests/testhelper.h
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+#include <vespa/vdstestlib/cppunit/dirconfig.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+
+
+#include <fstream>
+#include <vespa/fastos/fastos.h>
+#include <sstream>
+
+#define ASSERT_REPLY_COUNT(count, dummylink) \
+ { \
+ std::ostringstream msgost; \
+ if ((dummylink).getNumReplies() != count) { \
+ for (uint32_t ijx=0; ijx<(dummylink).getNumReplies(); ++ijx) { \
+ msgost << (dummylink).getReply(ijx)->toString(true) << "\n"; \
+ } \
+ } \
+ CPPUNIT_ASSERT_EQUAL_MSG(msgost.str(), size_t(count), \
+ (dummylink).getNumReplies()); \
+ }
+#define ASSERT_COMMAND_COUNT(count, dummylink) \
+ { \
+ std::ostringstream msgost; \
+ if ((dummylink).getNumCommands() != count) { \
+ for (uint32_t ijx=0; ijx<(dummylink).getNumCommands(); ++ijx) { \
+ msgost << (dummylink).getCommand(ijx)->toString(true) << "\n"; \
+ } \
+ } \
+ CPPUNIT_ASSERT_EQUAL_MSG(msgost.str(), size_t(count), \
+ (dummylink).getNumCommands()); \
+ }
+
+namespace storage {
+
+void addFileConfig(vdstestlib::DirConfig& dc,
+ const std::string& configDefName,
+ const std::string& fileName);
+
+
+void addStorageDistributionConfig(vdstestlib::DirConfig& dc);
+
+vdstestlib::DirConfig getStandardConfig(bool storagenode);
+
+// Class used to print start and end of test. Enable debug when you want to see
+// which test creates what output or where we get stuck
+struct TestName {
+ std::string name;
+ TestName(const std::string& n);
+ ~TestName();
+};
+
+} // storage
+
diff --git a/memfilepersistence/src/tests/testrunner.cpp b/memfilepersistence/src/tests/testrunner.cpp
new file mode 100644
index 00000000000..16027870c47
--- /dev/null
+++ b/memfilepersistence/src/tests/testrunner.cpp
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <iostream>
+#include <vespa/log/log.h>
+#include <vespa/vdstestlib/cppunit/cppunittestrunner.h>
+
+LOG_SETUP("persistencecppunittests");
+
+int
+main(int argc, char **argv)
+{
+ vdstestlib::CppUnitTestRunner testRunner;
+ return testRunner.run(argc, argv);
+}
diff --git a/memfilepersistence/src/tests/tools/.gitignore b/memfilepersistence/src/tests/tools/.gitignore
new file mode 100644
index 00000000000..7e7c0fe7fae
--- /dev/null
+++ b/memfilepersistence/src/tests/tools/.gitignore
@@ -0,0 +1,2 @@
+/.depend
+/Makefile
diff --git a/memfilepersistence/src/tests/tools/CMakeLists.txt b/memfilepersistence/src/tests/tools/CMakeLists.txt
new file mode 100644
index 00000000000..aef718c7633
--- /dev/null
+++ b/memfilepersistence/src/tests/tools/CMakeLists.txt
@@ -0,0 +1,7 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_library(memfilepersistence_testtools
+ SOURCES
+ dumpslotfiletest.cpp
+ vdsdisktooltest.cpp
+ DEPENDS
+)
diff --git a/memfilepersistence/src/tests/tools/dumpslotfiletest.cpp b/memfilepersistence/src/tests/tools/dumpslotfiletest.cpp
new file mode 100644
index 00000000000..112f8840e72
--- /dev/null
+++ b/memfilepersistence/src/tests/tools/dumpslotfiletest.cpp
@@ -0,0 +1,138 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/config/subscription/configuri.h>
+#include <vespa/document/base/testdocrepo.h>
+#include <vespa/memfilepersistence/tools/dumpslotfile.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/vespalib/util/programoptions_testutils.h>
+#include <tests/spi/memfiletestutils.h>
+
+#include <vespa/document/config/config-documenttypes.h>
+
+namespace storage {
+namespace memfile {
+
+class DumpSlotFileTest : public SingleDiskMemFileTestUtils
+{
+ CPPUNIT_TEST_SUITE(DumpSlotFileTest);
+ CPPUNIT_TEST(testSimple);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void testSimple();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(DumpSlotFileTest);
+
+#define ASSERT_MATCH(optstring, pattern) \
+{ \
+ vespalib::AppOptions opts("dumpslotfile " optstring); \
+ std::ostringstream out; \
+ config::ConfigUri configUri(config::ConfigUri::createFromInstance( \
+ document::TestDocRepo::getDefaultConfig())); \
+ std::unique_ptr<document::DocumenttypesConfig> config = config::ConfigGetter<document::DocumenttypesConfig>::getConfig(configUri.getConfigId(), configUri.getContext()); \
+ SlotFileDumper::dump(opts.getArgCount(), opts.getArguments(), \
+ configUri, out, out); \
+ CPPUNIT_ASSERT_MATCH_REGEX(pattern, out.str()); \
+ output = out.str(); \
+}
+
+void
+DumpSlotFileTest::testSimple()
+{
+ std::string output;
+ // Test syntax page
+ ASSERT_MATCH("--help", ".*Usage: dumpslotfile.*");
+ // Test non-existing file. (Handle as empty file)
+ ASSERT_MATCH("00a.0",
+ ".*BucketId\\(0x000000000000000a\\)"
+ ".*document count: 0.*non-existing.*");
+ // Parse bucketid without extension.
+ ASSERT_MATCH("000000000000000a",
+ ".*BucketId\\(0x000000000000000a\\) "
+ "\\(extracted from filename\\).*");
+ // Parse invalid bucket id.
+ ASSERT_MATCH("000010000000000g",
+ ".*Failed to extract bucket id from filename.*");
+ // Test toXml with no data. Thus doesn't require doc config
+ ASSERT_MATCH("--toxml --documentconfig whatevah 000a.0",
+ ".*<vespafeed>.*");
+ // Test invalid arguments
+ ASSERT_MATCH("--foobar", ".*Invalid option 'foobar'\\..*");
+ // What to show in XML doesn't make sense in non-xml mode
+ ASSERT_MATCH("--includeremoveddocs 0.0",
+ ".*Options for what to include in XML makes no sense when not "
+ "printing XML content.*");
+ ASSERT_MATCH("--includeremoveentries 0.0",
+ ".*Options for what to include in XML makes no sense when not "
+ "printing XML content.*");
+ // To binary only works for single doc
+ ASSERT_MATCH("--tobinary 0.0",
+ ".*To binary option only works for a single document.*");
+
+ BucketId bid(1, 0);
+ createTestBucket(bid, 0);
+ ASSERT_MATCH("-nN vdsroot/disks/d0/400000000000000.0",
+ ".*"
+ "Unique document count: 8.*"
+ "Total document size: [0-9]+.*"
+ "Used size: [0-9]+.*"
+ "Filename: .*/d0/.*"
+ "Filesize: 12288.*"
+ "SlotFileHeader.*"
+ "[0-9]+ empty entries.*"
+ "Header block.*"
+ "Content block.*"
+ "Slotfile verified.*"
+ );
+ ASSERT_MATCH("vdsroot/disks/d0/400000000000000.0", ".*ff ff ff ff.*");
+
+ // User friendly output
+ ASSERT_MATCH("--friendly -nN vdsroot/disks/d0/400000000000000.0",
+ ".*id:mail:testdoctype1:n=0:9380.html.*");
+
+ ASSERT_MATCH("--tobinary "
+ "--docid id:mail:testdoctype1:n=0:doesnotexisthere.html "
+ "vdsroot/disks/d0/400000000000000.0",
+ ".*No document with id id:mail:testdoctype1:n=0:doesnotexi.* "
+ "found.*");
+
+ // Should test XML with content.. But needs document config for it to work.
+ // Should be able to create programmatically from testdocman.
+ ASSERT_MATCH("--toxml --documentconfig '' "
+ "vdsroot/disks/d0/400000000000000.0",
+ ".*<vespafeed>\n"
+ "<document documenttype=\"testdoctype1\" "
+ "documentid=\"id:mail:testdoctype1:n=0:9639.html\">\n"
+ "<content>overwritten</content>\n"
+ "</document>.*");
+
+ // To binary
+ ASSERT_MATCH("--tobinary --docid id:mail:testdoctype1:n=0:9380.html "
+ "vdsroot/disks/d0/400000000000000.0",
+ ".*");
+ {
+ TestDocMan docMan;
+ document::ByteBuffer buf(output.c_str(), output.size());
+ document::Document doc(docMan.getTypeRepo(), buf);
+ CPPUNIT_ASSERT_EQUAL(std::string(
+ "<document documenttype=\"testdoctype1\" "
+ "documentid=\"id:mail:testdoctype1:n=0:9380.html\">\n"
+ "<content>To be, or not to be: that is the question:\n"
+ "Whether 'tis nobler in the mind to suffer\n"
+ "The slings and arrows of outrage</content>\n"
+ "</document>"), doc.toXml());
+ }
+
+ // Fail verification
+ {
+ vespalib::LazyFile file("vdsroot/disks/d0/400000000000000.0", 0);
+ file.write("corrupt", 7, 64);
+ }
+ ASSERT_MATCH("-nN vdsroot/disks/d0/400000000000000.0",
+ ".*lot 0 at timestamp [0-9]+ failed checksum verification.*");
+}
+
+} // memfile
+} // storage
diff --git a/memfilepersistence/src/tests/tools/vdsdisktooltest.cpp b/memfilepersistence/src/tests/tools/vdsdisktooltest.cpp
new file mode 100644
index 00000000000..29e780bc900
--- /dev/null
+++ b/memfilepersistence/src/tests/tools/vdsdisktooltest.cpp
@@ -0,0 +1,108 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/config/subscription/configuri.h>
+#include <vespa/memfilepersistence/tools/vdsdisktool.h>
+#include <vespa/storageframework/defaultimplementation/clock/fakeclock.h>
+#include <vespa/vdstestlib/cppunit/macros.h>
+#include <vespa/vespalib/util/programoptions_testutils.h>
+#include <tests/spi/memfiletestutils.h>
+
+namespace storage {
+namespace memfile {
+
+struct VdsDiskToolTest : public SingleDiskMemFileTestUtils
+{
+ framework::defaultimplementation::FakeClock _clock;
+ DeviceManager::LP _deviceManager;
+
+ void setUp();
+ void setupRoot();
+
+ void testSimple();
+
+ CPPUNIT_TEST_SUITE(VdsDiskToolTest);
+ CPPUNIT_TEST(testSimple);
+ CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(VdsDiskToolTest);
+
+#define ASSERT_MATCH(optstring, pattern, exitcode) \
+{ \
+ std::ostringstream out; \
+ int result = 1; \
+ try{ \
+ vespalib::AppOptions opts("vdsdisktool " optstring); \
+ result = VdsDiskTool::run(opts.getArgCount(), opts.getArguments(), \
+ "vdsroot", out, out); \
+ } catch (std::exception& e) { \
+ out << "Application aborted with exception:\n" << e.what() << "\n"; \
+ } \
+ CPPUNIT_ASSERT_MATCH_REGEX(pattern, out.str()); \
+ CPPUNIT_ASSERT_EQUAL(exitcode, result); \
+}
+
+namespace {
+ void createDisk(int i) {
+ std::ostringstream path;
+ path << "vdsroot/mycluster/storage/3/disks/d" << i;
+ CPPUNIT_ASSERT_EQUAL(0, system(("mkdir -p " + path.str()).c_str()));
+ }
+}
+
+void
+VdsDiskToolTest::setUp()
+{
+ system("rm -rf vdsroot");
+ _deviceManager.reset(new DeviceManager(
+ DeviceMapper::UP(new SimpleDeviceMapper), _clock));
+}
+
+void
+VdsDiskToolTest::setupRoot()
+{
+ system("rm -rf vdsroot");
+ createDisk(0);
+}
+
+void
+VdsDiskToolTest::testSimple()
+{
+ // Test syntax page
+ ASSERT_MATCH("--help", ".*Usage: vdsdisktool .*", 0);
+ // No VDS installation
+ ASSERT_MATCH("status", ".*No VDS installations found at all.*", 1);
+ // Common setup
+ setupRoot();
+ ASSERT_MATCH("status", ".*Disks on storage node 3 in cluster mycluster:\\s*"
+ "Disk 0: OK\\s*", 0);
+ // Two disks
+ system("mkdir -p vdsroot/mycluster/storage/3/disks/d1/");
+ ASSERT_MATCH("status", ".*Disks on storage node 3 in cluster mycluster:\\s*"
+ "Disk 0: OK\\s*"
+ "Disk 1: OK\\s*", 0);
+ // Two disks, non-continuous indexes
+ system("rm -rf vdsroot/mycluster/storage/3/disks/d1/");
+ system("mkdir -p vdsroot/mycluster/storage/3/disks/d2/");
+ ASSERT_MATCH("status", ".*Disks on storage node 3 in cluster mycluster:\\s*"
+ "Disk 0: OK\\s*"
+ "Disk 1: NOT_FOUND - Disk not found during scan.*"
+ "Disk 2: OK\\s*", 0);
+ // Status file existing
+ setupRoot();
+ createDisk(1);
+ MountPointList mountPoints("vdsroot/mycluster/storage/3",
+ std::vector<vespalib::string>(),
+ _deviceManager);
+ mountPoints.scanForDisks();
+ CPPUNIT_ASSERT_EQUAL(2u, mountPoints.getSize());
+ mountPoints[1].addEvent(Device::IO_FAILURE, "Bad", "Found in test");
+ mountPoints.writeToFile();
+ ASSERT_MATCH("status", ".*Disks on storage node 3 in cluster mycluster:\\s*"
+ "Disk 0: OK\\s*"
+ "Disk 1: IO_FAILURE - 0 Bad\\s*", 0);
+}
+
+} // memfile
+} // storage