package org.apache.lucene.index;
import static org.junit.Assert.*;
import java.util.Collection;
import java.util.Map;
import java.util.Random;
import java.io.File;
import java.io.IOException;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.analysis.MockAnalyzer;
import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.KeepOnlyLastCommitDeletionPolicy;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.SnapshotDeletionPolicy;
import org.apache.lucene.util.LuceneTestCaseJ4;
import org.apache.lucene.util.ThreadInterruptedException;
import org.apache.lucene.util._TestUtil;
import org.junit.Before;
import org.junit.Test;
//
// This was developed for Lucene In Action,
// http://lucenebook.com
//
public class TestSnapshotDeletionPolicy extends LuceneTestCaseJ4 {
protected Random random;
public static final String INDEX_PATH = "test.snapshots";
@Before
@Override
public void setUp() throws Exception {
super.setUp();
random = newRandom();
}
protected IndexWriterConfig getConfig(Random random, IndexDeletionPolicy dp) {
IndexWriterConfig conf = newIndexWriterConfig(random, TEST_VERSION_CURRENT, new MockAnalyzer());
if (dp != null) {
conf.setIndexDeletionPolicy(dp);
}
return conf;
}
protected void checkSnapshotExists(Directory dir, IndexCommit c) throws Exception {
String segFileName = c.getSegmentsFileName();
assertTrue("segments file not found in directory: " + segFileName, dir.fileExists(segFileName));
}
protected void checkMaxDoc(IndexCommit commit, int expectedMaxDoc) throws Exception {
IndexReader reader = IndexReader.open(commit, true);
try {
assertEquals(expectedMaxDoc, reader.maxDoc());
} finally {
reader.close();
}
}
protected void prepareIndexAndSnapshots(SnapshotDeletionPolicy sdp,
IndexWriter writer, int numSnapshots, String snapshotPrefix)
throws RuntimeException, IOException {
for (int i = 0; i < numSnapshots; i++) {
// create dummy document to trigger commit.
writer.addDocument(new Document());
writer.commit();
sdp.snapshot(snapshotPrefix + i);
}
}
protected SnapshotDeletionPolicy getDeletionPolicy() throws IOException {
return getDeletionPolicy(null);
}
protected SnapshotDeletionPolicy getDeletionPolicy(Map<String, String> snapshots) throws IOException {
return new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy(), snapshots);
}
protected void assertSnapshotExists(Directory dir, SnapshotDeletionPolicy sdp, int numSnapshots) throws Exception {
for (int i = 0; i < numSnapshots; i++) {
IndexCommit snapshot = sdp.getSnapshot("snapshot" + i);
checkMaxDoc(snapshot, i + 1);
checkSnapshotExists(dir, snapshot);
}
}
@Test
public void testSnapshotDeletionPolicy() throws Exception {
File dir = _TestUtil.getTempDir(INDEX_PATH);
try {
Directory fsDir = FSDirectory.open(dir);
runTest(random, fsDir);
fsDir.close();
} finally {
_TestUtil.rmDir(dir);
}
Directory dir2 = newDirectory(random);
runTest(random, dir2);
dir2.close();
}
private void runTest(Random random, Directory dir) throws Exception {
// Run for ~1 seconds
final long stopTime = System.currentTimeMillis() + 1000;
SnapshotDeletionPolicy dp = getDeletionPolicy();
final IndexWriter writer = new IndexWriter(dir, newIndexWriterConfig(random,
TEST_VERSION_CURRENT, new MockAnalyzer()).setIndexDeletionPolicy(dp)
.setMaxBufferedDocs(2));
writer.commit();
final Thread t = new Thread() {
@Override
public void run() {
Document doc = new Document();
doc.add(new Field("content", "aaa", Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
do {
for(int i=0;i<27;i++) {
try {
writer.addDocument(doc);
} catch (Throwable t) {
t.printStackTrace(System.out);
fail("addDocument failed");
}
if (i%2 == 0) {
try {
writer.commit();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
try {
Thread.sleep(1);
} catch (InterruptedException ie) {
throw new ThreadInterruptedException(ie);
}
} while(System.currentTimeMillis() < stopTime);
}
};
t.start();
// While the above indexing thread is running, take many
// backups:
do {
backupIndex(dir, dp);
Thread.sleep(20);
} while(t.isAlive());
t.join();
// Add one more document to force writer to commit a
// final segment, so deletion policy has a chance to
// delete again:
Document doc = new Document();
doc.add(new Field("content", "aaa", Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
writer.addDocument(doc);
// Make sure we don't have any leftover files in the
// directory:
writer.close();
TestIndexWriter.assertNoUnreferencedFiles(dir, "some files were not deleted but should have been");
}
/**
* Example showing how to use the SnapshotDeletionPolicy to take a backup.
* This method does not really do a backup; instead, it reads every byte of
* every file just to test that the files indeed exist and are readable even
* while the index is changing.
*/
public void backupIndex(Directory dir, SnapshotDeletionPolicy dp) throws Exception {
// To backup an index we first take a snapshot:
try {
copyFiles(dir, dp.snapshot("id"));
} finally {
// Make sure to release the snapshot, otherwise these
// files will never be deleted during this IndexWriter
// session:
dp.release("id");
}
}
private void copyFiles(Directory dir, IndexCommit cp) throws Exception {
// While we hold the snapshot, and nomatter how long
// we take to do the backup, the IndexWriter will
// never delete the files in the snapshot:
Collection<String> files = cp.getFileNames();
for (final String fileName : files) {
// NOTE: in a real backup you would not use
// readFile; you would need to use something else
// that copies the file to a backup location. This
// could even be a spawned shell process (eg "tar",
// "zip") that takes the list of files and builds a
// backup.
readFile(dir, fileName);
}
}
byte[] buffer = new byte[4096];
private void readFile(Directory dir, String name) throws Exception {
IndexInput input = dir.openInput(name);
try {
long size = dir.fileLength(name);
long bytesLeft = size;
while (bytesLeft > 0) {
final int numToRead;
if (bytesLeft < buffer.length)
numToRead = (int) bytesLeft;
else
numToRead = buffer.length;
input.readBytes(buffer, 0, numToRead, false);
bytesLeft -= numToRead;
}
// Don't do this in your real backups! This is just
// to force a backup to take a somewhat long time, to
// make sure we are exercising the fact that the
// IndexWriter should not delete this file even when I
// take my time reading it.
Thread.sleep(1);
} finally {
input.close();
}
}
@Test
public void testBasicSnapshots() throws Exception {
int numSnapshots = 3;
SnapshotDeletionPolicy sdp = getDeletionPolicy();
// Create 3 snapshots: snapshot0, snapshot1, snapshot2
Directory dir = newDirectory(random);
IndexWriter writer = new IndexWriter(dir, getConfig(random, sdp));
prepareIndexAndSnapshots(sdp, writer, numSnapshots, "snapshot");
writer.close();
assertSnapshotExists(dir, sdp, numSnapshots);
// open a reader on a snapshot - should succeed.
IndexReader.open(sdp.getSnapshot("snapshot0"), true).close();
// open a new IndexWriter w/ no snapshots to keep and assert that all snapshots are gone.
sdp = getDeletionPolicy();
writer = new IndexWriter(dir, getConfig(random, sdp));
writer.deleteUnusedFiles();
writer.close();
assertEquals("no snapshots should exist", 1, IndexReader.listCommits(dir).size());
for (int i = 0; i < numSnapshots; i++) {
try {
sdp.getSnapshot("snapshot" + i);
fail("snapshot shouldn't have existed, but did: snapshot" + i);
} catch (IllegalStateException e) {
// expected - snapshot should not exist
}
}
dir.close();
}
@Test
public void testMultiThreadedSnapshotting() throws Exception {
Directory dir = newDirectory(random);
final SnapshotDeletionPolicy sdp = getDeletionPolicy();
final IndexWriter writer = new IndexWriter(dir, getConfig(random, sdp));
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread() {
@Override
public void run() {
try {
writer.addDocument(new Document());
writer.commit();
sdp.snapshot(getName());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
threads[i].setName("t" + i);
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
// Do one last commit, so that after we release all snapshots, we stay w/ one commit
writer.addDocument(new Document());
writer.commit();
for (Thread t : threads) {
sdp.release(t.getName());
writer.deleteUnusedFiles();
}
assertEquals(1, IndexReader.listCommits(dir).size());
writer.close();
dir.close();
}
@Test
public void testRollbackToOldSnapshot() throws Exception {
int numSnapshots = 2;
Directory dir = newDirectory(random);
SnapshotDeletionPolicy sdp = getDeletionPolicy();
IndexWriter writer = new IndexWriter(dir, getConfig(random, sdp));
prepareIndexAndSnapshots(sdp, writer, numSnapshots, "snapshot");
writer.close();
// now open the writer on "snapshot0" - make sure it succeeds
writer = new IndexWriter(dir, getConfig(random, sdp).setIndexCommit(sdp.getSnapshot("snapshot0")));
// this does the actual rollback
writer.commit();
writer.deleteUnusedFiles();
assertSnapshotExists(dir, sdp, numSnapshots - 1);
writer.close();
// but 'snapshot1' files will still exist (need to release snapshot before they can be deleted).
String segFileName = sdp.getSnapshot("snapshot1").getSegmentsFileName();
assertTrue("snapshot files should exist in the directory: " + segFileName, dir.fileExists(segFileName));
dir.close();
}
@Test
public void testReleaseSnapshot() throws Exception {
Directory dir = newDirectory(random);
SnapshotDeletionPolicy sdp = getDeletionPolicy();
IndexWriter writer = new IndexWriter(dir, getConfig(random, sdp));
prepareIndexAndSnapshots(sdp, writer, 1, "snapshot");
// Create another commit - we must do that, because otherwise the "snapshot"
// files will still remain in the index, since it's the last commit.
writer.addDocument(new Document());
writer.commit();
// Release
String snapId = "snapshot0";
String segFileName = sdp.getSnapshot(snapId).getSegmentsFileName();
sdp.release(snapId);
try {
sdp.getSnapshot(snapId);
fail("should not have succeeded to get an unsnapshotted id");
} catch (IllegalStateException e) {
// expected
}
assertNull(sdp.getSnapshots().get(snapId));
writer.deleteUnusedFiles();
writer.close();
assertFalse("segments file should not be found in dirctory: " + segFileName, dir.fileExists(segFileName));
dir.close();
}
@Test
public void testExistingSnapshots() throws Exception {
// Tests the ability to construct a SDP from existing snapshots, and
// asserts that those snapshots/commit points are protected.
int numSnapshots = 3;
Directory dir = newDirectory(random);
SnapshotDeletionPolicy sdp = getDeletionPolicy();
IndexWriter writer = new IndexWriter(dir, getConfig(random, sdp));
prepareIndexAndSnapshots(sdp, writer, numSnapshots, "snapshot");
writer.close();
// Make a new policy and initialize with snapshots.
sdp = getDeletionPolicy(sdp.getSnapshots());
writer = new IndexWriter(dir, getConfig(random, sdp));
// attempt to delete unused files - the snapshotted files should not be deleted
writer.deleteUnusedFiles();
writer.close();
assertSnapshotExists(dir, sdp, numSnapshots);
dir.close();
}
@Test
public void testSnapshotLastCommitTwice() throws Exception {
Directory dir = newDirectory(random);
SnapshotDeletionPolicy sdp = getDeletionPolicy();
IndexWriter writer = new IndexWriter(dir, getConfig(random, sdp));
writer.addDocument(new Document());
writer.commit();
String s1 = "s1";
String s2 = "s2";
IndexCommit ic1 = sdp.snapshot(s1);
IndexCommit ic2 = sdp.snapshot(s2);
assertTrue(ic1 == ic2); // should be the same instance
// create another commit
writer.addDocument(new Document());
writer.commit();
// release "s1" should not delete "s2"
sdp.release(s1);
writer.deleteUnusedFiles();
checkSnapshotExists(dir, ic2);
writer.close();
dir.close();
}
@Test
public void testMissingCommits() throws Exception {
// Tests the behavior of SDP when commits that are given at ctor are missing
// on onInit().
Directory dir = newDirectory(random);
SnapshotDeletionPolicy sdp = getDeletionPolicy();
IndexWriter writer = new IndexWriter(dir, getConfig(random, sdp));
writer.addDocument(new Document());
writer.commit();
IndexCommit ic = sdp.snapshot("s1");
// create another commit, not snapshotted.
writer.addDocument(new Document());
writer.close();
// open a new writer w/ KeepOnlyLastCommit policy, so it will delete "s1"
// commit.
new IndexWriter(dir, getConfig(random, null)).close();
assertFalse("snapshotted commit should not exist", dir.fileExists(ic.getSegmentsFileName()));
// Now reinit SDP from the commits in the index - the snapshot id should not
// exist anymore.
sdp = getDeletionPolicy(sdp.getSnapshots());
new IndexWriter(dir, getConfig(random, sdp)).close();
try {
sdp.getSnapshot("s1");
fail("snapshot s1 should not exist");
} catch (IllegalStateException e) {
// expected.
}
dir.close();
}
}