From 133e70cad64138efdf710b8fd751da1663a5a8ac Mon Sep 17 00:00:00 2001 From: Shai Erera Date: Sun, 23 Jan 2011 05:10:48 +0000 Subject: [PATCH] LUCENE-2720: IndexWriter should throw IndexFormatTooOldExc on open, not later during optimize/getReader/close (trunk) git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1062325 13f79535-47bb-0310-9956-ffa450edef68 --- lucene/CHANGES.txt | 3 ++ .../org/apache/lucene/index/FieldsReader.java | 33 ++++++++++--- .../index/IndexFormatTooOldException.java | 9 +++- .../org/apache/lucene/index/IndexWriter.java | 10 ++-- .../org/apache/lucene/index/SegmentInfo.java | 44 ++++++++++++++++-- .../apache/lucene/index/SegmentReader.java | 1 + .../codecs/DefaultSegmentInfosReader.java | 39 +++++++++++++++- .../codecs/DefaultSegmentInfosWriter.java | 5 +- .../org/apache/lucene/util/Constants.java | 3 ++ .../index/TestBackwardsCompatibility.java | 26 +++-------- .../org/apache/lucene/index/index.31.cfs.zip | Bin 4874 -> 4692 bytes .../apache/lucene/index/index.31.nocfs.zip | Bin 9168 -> 9547 bytes 12 files changed, 133 insertions(+), 40 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index abe2f04bbb0..c8797f5bd1f 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -140,6 +140,9 @@ Changes in Runtime Behavior documents that don't have the field get a norm byte value of 0. Previously, Lucene would populate "fake norms" with Similarity.getDefault() for these documents. (Robert Muir, Mike Mccandless) + +* LUCENE-2720: IndexWriter throws IndexFormatTooOldException on open, rather + than later when e.g. a merge starts. (Shai Erera, Mike McCandless, Uwe Schindler) API Changes diff --git a/lucene/src/java/org/apache/lucene/index/FieldsReader.java b/lucene/src/java/org/apache/lucene/index/FieldsReader.java index 96b58120e50..76c0ed23552 100644 --- a/lucene/src/java/org/apache/lucene/index/FieldsReader.java +++ b/lucene/src/java/org/apache/lucene/index/FieldsReader.java @@ -37,8 +37,10 @@ import java.io.Reader; * Class responsible for access to stored document fields. *

* It uses <segment>.fdt and <segment>.fdx; files. + * + * @lucene.internal */ -final class FieldsReader implements Cloneable { +public final class FieldsReader implements Cloneable { private final static int FORMAT_SIZE = 4; private final FieldInfos fieldInfos; @@ -74,6 +76,23 @@ final class FieldsReader implements Cloneable { ensureOpen(); return new FieldsReader(fieldInfos, numTotalDocs, size, format, docStoreOffset, cloneableFieldsStream, cloneableIndexStream); } + + /** Verifies that the code version which wrote the segment is supported. */ + public static void checkCodeVersion(Directory dir, String segment) throws IOException { + final String indexStreamFN = IndexFileNames.segmentFileName(segment, "", IndexFileNames.FIELDS_INDEX_EXTENSION); + IndexInput idxStream = dir.openInput(indexStreamFN, 1024); + + try { + int format = idxStream.readInt(); + if (format < FieldsWriter.FORMAT_MINIMUM) + throw new IndexFormatTooOldException(indexStreamFN, format, FieldsWriter.FORMAT_MINIMUM, FieldsWriter.FORMAT_CURRENT); + if (format > FieldsWriter.FORMAT_CURRENT) + throw new IndexFormatTooNewException(indexStreamFN, format, FieldsWriter.FORMAT_MINIMUM, FieldsWriter.FORMAT_CURRENT); + } finally { + idxStream.close(); + } + + } // Used only by clone private FieldsReader(FieldInfos fieldInfos, int numTotalDocs, int size, int format, int docStoreOffset, @@ -89,11 +108,11 @@ final class FieldsReader implements Cloneable { indexStream = (IndexInput) cloneableIndexStream.clone(); } - FieldsReader(Directory d, String segment, FieldInfos fn) throws IOException { + public FieldsReader(Directory d, String segment, FieldInfos fn) throws IOException { this(d, segment, fn, BufferedIndexInput.BUFFER_SIZE, -1, 0); } - FieldsReader(Directory d, String segment, FieldInfos fn, int readBufferSize, int docStoreOffset, int size) throws IOException { + public FieldsReader(Directory d, String segment, FieldInfos fn, int readBufferSize, int docStoreOffset, int size) throws IOException { boolean success = false; isOriginal = true; try { @@ -157,7 +176,7 @@ final class FieldsReader implements Cloneable { * * @throws IOException */ - final void close() throws IOException { + public final void close() throws IOException { if (!closed) { if (fieldsStream != null) { fieldsStream.close(); @@ -178,7 +197,7 @@ final class FieldsReader implements Cloneable { } } - final int size() { + public final int size() { return size; } @@ -186,7 +205,7 @@ final class FieldsReader implements Cloneable { indexStream.seek(FORMAT_SIZE + (docID + docStoreOffset) * 8L); } - final Document doc(int n, FieldSelector fieldSelector) throws CorruptIndexException, IOException { + public final Document doc(int n, FieldSelector fieldSelector) throws CorruptIndexException, IOException { seekIndex(n); long position = indexStream.readLong(); fieldsStream.seek(position); @@ -237,7 +256,7 @@ final class FieldsReader implements Cloneable { * contiguous range of length numDocs starting with * startDocID. Returns the IndexInput (the fieldStream), * already seeked to the starting point for startDocID.*/ - final IndexInput rawDocs(int[] lengths, int startDocID, int numDocs) throws IOException { + public final IndexInput rawDocs(int[] lengths, int startDocID, int numDocs) throws IOException { seekIndex(startDocID); long startOffset = indexStream.readLong(); long lastOffset = startOffset; diff --git a/lucene/src/java/org/apache/lucene/index/IndexFormatTooOldException.java b/lucene/src/java/org/apache/lucene/index/IndexFormatTooOldException.java index 9be38a91e2a..b8f9356cfd4 100644 --- a/lucene/src/java/org/apache/lucene/index/IndexFormatTooOldException.java +++ b/lucene/src/java/org/apache/lucene/index/IndexFormatTooOldException.java @@ -23,10 +23,15 @@ package org.apache.lucene.index; */ public class IndexFormatTooOldException extends CorruptIndexException { + public IndexFormatTooOldException(String filename, String version) { + super("Format version is not supported" + (filename!=null ? (" in file '" + filename + "'") : "") + + ": " + version + ". This version of Lucene only supports indexes created with release 3.0 and later."); + } + public IndexFormatTooOldException(String filename, int version, int minVersion, int maxVersion) { super("Format version is not supported" + (filename!=null ? (" in file '" + filename + "'") : "") + - ": " + version + " (needs to be between " + minVersion + " and " + maxVersion + - "). This version of Lucene only supports indexes created with release 3.0 and later."); + ": " + version + " (needs to be between " + minVersion + " and " + maxVersion + + "). This version of Lucene only supports indexes created with release 3.0 and later."); } } diff --git a/lucene/src/java/org/apache/lucene/index/IndexWriter.java b/lucene/src/java/org/apache/lucene/index/IndexWriter.java index 34b13037cc0..5bd874ade0a 100644 --- a/lucene/src/java/org/apache/lucene/index/IndexWriter.java +++ b/lucene/src/java/org/apache/lucene/index/IndexWriter.java @@ -605,8 +605,6 @@ public class IndexWriter implements Closeable { } } - - /** * Obtain the number of deleted docs for a pooled reader. * If the reader isn't being pooled, the segmentInfo's @@ -715,11 +713,8 @@ public class IndexWriter implements Closeable { boolean success = false; - // TODO: we should check whether this index is too old, - // and throw an IndexFormatTooOldExc up front, here, - // instead of later when merge, applyDeletes, getReader - // is attempted. I think to do this we should store the - // oldest segment's version in segments_N. + // If index is too old, reading the segments will throw + // IndexFormatTooOldException. segmentInfos = new SegmentInfos(codecs); try { if (create) { @@ -982,6 +977,7 @@ public class IndexWriter implements Closeable { * @throws CorruptIndexException if the index is corrupt * @throws IOException if there is a low-level IO error */ + @Override public void close() throws CorruptIndexException, IOException { close(true); } diff --git a/lucene/src/java/org/apache/lucene/index/SegmentInfo.java b/lucene/src/java/org/apache/lucene/index/SegmentInfo.java index 2c91efc636b..24f6d58d15f 100644 --- a/lucene/src/java/org/apache/lucene/index/SegmentInfo.java +++ b/lucene/src/java/org/apache/lucene/index/SegmentInfo.java @@ -20,6 +20,7 @@ package org.apache.lucene.index; import org.apache.lucene.store.Directory; import org.apache.lucene.store.IndexOutput; import org.apache.lucene.store.IndexInput; +import org.apache.lucene.util.Constants; import org.apache.lucene.index.codecs.Codec; import org.apache.lucene.index.codecs.CodecProvider; import org.apache.lucene.index.codecs.DefaultSegmentInfosWriter; @@ -87,6 +88,13 @@ public final class SegmentInfo { private Map diagnostics; + // Tracks the Lucene version this segment was created with, since 3.1. Null + // indicates an older than 3.0 index, and it's used to detect a too old index. + // The format expected is "x.y" - "2.x" for pre-3.0 indexes (or null), and + // specific versions afterwards ("3.0", "3.1" etc.). + // see Constants.LUCENE_MAIN_VERSION. + private String version; + public SegmentInfo(String name, int docCount, Directory dir, boolean isCompoundFile, boolean hasProx, SegmentCodecs segmentCodecs, boolean hasVectors) { this.name = name; @@ -99,6 +107,7 @@ public final class SegmentInfo { this.segmentCodecs = segmentCodecs; this.hasVectors = hasVectors; delCount = 0; + version = Constants.LUCENE_MAIN_VERSION; } /** @@ -106,6 +115,7 @@ public final class SegmentInfo { */ void reset(SegmentInfo src) { clearFiles(); + version = src.version; name = src.name; docCount = src.docCount; dir = src.dir; @@ -145,6 +155,9 @@ public final class SegmentInfo { */ public SegmentInfo(Directory dir, int format, IndexInput input, CodecProvider codecs) throws IOException { this.dir = dir; + if (format <= DefaultSegmentInfosWriter.FORMAT_3_1) { + version = input.readString(); + } name = input.readString(); docCount = input.readInt(); delGen = input.readLong(); @@ -293,6 +306,7 @@ public final class SegmentInfo { si.normGen = normGen.clone(); } si.hasVectors = hasVectors; + si.version = version; return si; } @@ -433,6 +447,8 @@ public final class SegmentInfo { public void write(IndexOutput output) throws IOException { assert delCount <= docCount: "delCount=" + delCount + " docCount=" + docCount + " segment=" + name; + // Write the Lucene version that created this segment, since 3.1 + output.writeString(version); output.writeString(name); output.writeInt(docCount); output.writeLong(delGen); @@ -574,8 +590,9 @@ public final class SegmentInfo { /** Used for debugging. Format may suddenly change. * *

Current format looks like - * _a:c45/4->_1, which means the segment's - * name is _a; it's using compound file + * _a(3.1):c45/4->_1, which means the segment's + * name is _a; it was created with Lucene 3.1 (or + * '?' if it's unkown); it's using compound file * format (would be C if not compound); it * has 45 documents; it has 4 deletions (this part is * left off when there are no deletions); it's using the @@ -585,7 +602,7 @@ public final class SegmentInfo { public String toString(Directory dir, int pendingDelCount) { StringBuilder s = new StringBuilder(); - s.append(name).append(':'); + s.append(name).append('(').append(version == null ? "?" : version).append(')').append(':'); char cfs = getUseCompoundFile() ? 'c' : 'C'; s.append(cfs); @@ -633,4 +650,25 @@ public final class SegmentInfo { public int hashCode() { return dir.hashCode() + name.hashCode(); } + + /** + * Used by DefaultSegmentInfosReader to upgrade a 3.0 segment to record its + * version is "3.0". This method can be removed when we're not required to + * support 3x indexes anymore, e.g. in 5.0. + *

+ * NOTE: this method is used for internal purposes only - you should + * not modify the version of a SegmentInfo, or it may result in unexpected + * exceptions thrown when you attempt to open the index. + * + * @lucene.internal + */ + public void setVersion(String version) { + this.version = version; + } + + /** Returns the version of the code which wrote the segment. */ + public String getVersion() { + return version; + } + } diff --git a/lucene/src/java/org/apache/lucene/index/SegmentReader.java b/lucene/src/java/org/apache/lucene/index/SegmentReader.java index e8c5160d367..37aee483c0c 100644 --- a/lucene/src/java/org/apache/lucene/index/SegmentReader.java +++ b/lucene/src/java/org/apache/lucene/index/SegmentReader.java @@ -226,6 +226,7 @@ public class SegmentReader extends IndexReader implements Cloneable { assert storeDir != null; } + // nocommit: this can be simplified to always be si.getDocStoreSegment() final String storesSegment; if (si.getDocStoreOffset() != -1) { storesSegment = si.getDocStoreSegment(); diff --git a/lucene/src/java/org/apache/lucene/index/codecs/DefaultSegmentInfosReader.java b/lucene/src/java/org/apache/lucene/index/codecs/DefaultSegmentInfosReader.java index 02a1609c125..1ee057a7e76 100644 --- a/lucene/src/java/org/apache/lucene/index/codecs/DefaultSegmentInfosReader.java +++ b/lucene/src/java/org/apache/lucene/index/codecs/DefaultSegmentInfosReader.java @@ -19,7 +19,10 @@ package org.apache.lucene.index.codecs; import java.io.IOException; +import org.apache.lucene.index.CompoundFileReader; import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.FieldsReader; +import org.apache.lucene.index.IndexFileNames; import org.apache.lucene.index.IndexFormatTooOldException; import org.apache.lucene.index.IndexFormatTooNewException; import org.apache.lucene.index.SegmentInfo; @@ -55,7 +58,41 @@ public class DefaultSegmentInfosReader extends SegmentInfosReader { infos.counter = input.readInt(); // read counter for (int i = input.readInt(); i > 0; i--) { // read segmentInfos - infos.add(new SegmentInfo(directory, format, input, codecs)); + SegmentInfo si = new SegmentInfo(directory, format, input, codecs); + if (si.getVersion() == null) { + // Could be a 3.0 - try to open the doc stores - if it fails, it's a + // 2.x segment, and an IndexFormatTooOldException will be thrown, + // which is what we want. + Directory dir = directory; + if (si.getDocStoreOffset() != -1) { + if (si.getDocStoreIsCompoundFile()) { + dir = new CompoundFileReader(dir, IndexFileNames.segmentFileName( + si.getDocStoreSegment(), "", + IndexFileNames.COMPOUND_FILE_STORE_EXTENSION), 1024); + } + } else if (si.getUseCompoundFile()) { + dir = new CompoundFileReader(dir, IndexFileNames.segmentFileName( + si.name, "", IndexFileNames.COMPOUND_FILE_EXTENSION), 1024); + } + + try { + FieldsReader.checkCodeVersion(dir, si.getDocStoreSegment()); + } finally { + // If we opened the directory, close it + if (dir != directory) dir.close(); + } + + // Above call succeeded, so it's a 3.0 segment. Upgrade it so the next + // time the segment is read, its version won't be null and we won't + // need to open FieldsReader every time for each such segment. + si.setVersion("3.0"); + } else if (si.getVersion().equals("2.x")) { + // If it's a 3x index touched by 3.1+ code, then segments record their + // version, whether they are 2.x ones or not. We detect that and throw + // appropriate exception. + throw new IndexFormatTooOldException(si.name, si.getVersion()); + } + infos.add(si); } infos.userData = input.readStringStringMap(); diff --git a/lucene/src/java/org/apache/lucene/index/codecs/DefaultSegmentInfosWriter.java b/lucene/src/java/org/apache/lucene/index/codecs/DefaultSegmentInfosWriter.java index c89fe948072..f034a412f52 100644 --- a/lucene/src/java/org/apache/lucene/index/codecs/DefaultSegmentInfosWriter.java +++ b/lucene/src/java/org/apache/lucene/index/codecs/DefaultSegmentInfosWriter.java @@ -38,9 +38,12 @@ public class DefaultSegmentInfosWriter extends SegmentInfosWriter { /** Each segment records whether it has term vectors */ public static final int FORMAT_HAS_VECTORS = -10; + /** Each segment records the Lucene version that created it. */ + public static final int FORMAT_3_1 = -11; + /** Each segment records whether its postings are written * in the new flex format */ - public static final int FORMAT_4_0 = -11; + public static final int FORMAT_4_0 = -12; /** This must always point to the most recent file format. * whenever you add a new format, make it 1 smaller (negative version logic)! */ diff --git a/lucene/src/java/org/apache/lucene/util/Constants.java b/lucene/src/java/org/apache/lucene/util/Constants.java index c991eb62366..13b67c9f69a 100644 --- a/lucene/src/java/org/apache/lucene/util/Constants.java +++ b/lucene/src/java/org/apache/lucene/util/Constants.java @@ -70,6 +70,9 @@ public final class Constants { return s.toString(); } + // NOTE: we track per-segment version as a String with the "X.Y" format, e.g. + // "4.0", "3.1", "3.0". Therefore when we change this constant, we should keep + // the format. public static final String LUCENE_MAIN_VERSION = ident("4.0"); public static final String LUCENE_VERSION; diff --git a/lucene/src/test/org/apache/lucene/index/TestBackwardsCompatibility.java b/lucene/src/test/org/apache/lucene/index/TestBackwardsCompatibility.java index ffa6238b072..7324889411a 100644 --- a/lucene/src/test/org/apache/lucene/index/TestBackwardsCompatibility.java +++ b/lucene/src/test/org/apache/lucene/index/TestBackwardsCompatibility.java @@ -171,15 +171,7 @@ public class TestBackwardsCompatibility extends LuceneTestCase { try { writer = new IndexWriter(dir, newIndexWriterConfig( - TEST_VERSION_CURRENT, new MockAnalyzer()) - .setMergeScheduler(new SerialMergeScheduler()) // no threads! - ); - // TODO: Make IndexWriter fail on open! - if (random.nextBoolean()) { - writer.optimize(); - } else { - reader = writer.getReader(); - } + TEST_VERSION_CURRENT, new MockAnalyzer())); fail("IndexWriter creation should not pass for "+unsupportedNames[i]); } catch (IndexFormatTooOldException e) { // pass @@ -188,17 +180,13 @@ public class TestBackwardsCompatibility extends LuceneTestCase { e.printStackTrace(System.out); } } finally { - if (reader != null) reader.close(); - reader = null; + // we should fail to open IW, and so it should be null when we get here. + // However, if the test fails (i.e., IW did not fail on open), we need + // to close IW. However, if merges are run, IW may throw + // IndexFormatTooOldException, and we don't want to mask the fail() + // above, so close without waiting for merges. if (writer != null) { - try { - writer.close(); - } catch (IndexFormatTooOldException e) { - // OK -- since IW gives merge scheduler a chance - // to merge at close, it's possible and fine to - // hit this exc here - writer.close(false); - } + writer.close(false); } writer = null; } diff --git a/lucene/src/test/org/apache/lucene/index/index.31.cfs.zip b/lucene/src/test/org/apache/lucene/index/index.31.cfs.zip index 02eac4579565dd46dfbfa2694d87b35d4c652ee7..bfbe17911cec2418b826720eaab303dc050fb07a 100644 GIT binary patch literal 4692 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 literal 4874 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 literal 9168 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