--- /dev/null
+package org.apache.lucene.index;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Random;
+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.IndexInput;
+import org.apache.lucene.analysis.KeywordAnalyzer;
+import org.apache.lucene.analysis.MockAnalyzer;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+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.LuceneTestCase;
+import org.apache.lucene.util.ThreadInterruptedException;
+import org.junit.Test;
+
+//
+// This was developed for Lucene In Action,
+// http://lucenebook.com
+//
+
+public class TestSnapshotDeletionPolicy extends LuceneTestCase {
+ public static final String INDEX_PATH = "test.snapshots";
+
+ protected IndexWriterConfig getConfig(Random random, IndexDeletionPolicy dp) {
+ IndexWriterConfig conf = newIndexWriterConfig( TEST_VERSION_CURRENT, new MockAnalyzer(random));
+ 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 {
+ Directory fsDir = newDirectory();
+ runTest(random, fsDir);
+ fsDir.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(
+ TEST_VERSION_CURRENT, new MockAnalyzer(random)).setIndexDeletionPolicy(dp)
+ .setMaxBufferedDocs(2));
+ writer.commit();
+
+ final Thread t = new Thread() {
+ @Override
+ public void run() {
+ Document doc = new Document();
+ doc.add(newField("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(newField("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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ }
+
+}