Index: src/test/org/apache/lucene/index/TestIndexReaderClone.java =================================================================== --- src/test/org/apache/lucene/index/TestIndexReaderClone.java (revision 0) +++ src/test/org/apache/lucene/index/TestIndexReaderClone.java (revision 0) @@ -0,0 +1,173 @@ +package org.apache.lucene.index; + +import java.io.IOException; +import java.util.ArrayList; + +import org.apache.lucene.index.SegmentReader.Norm; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.LockObtainFailedException; +import org.apache.lucene.store.MockRAMDirectory; +import org.apache.lucene.util.LuceneTestCase; + +/** + * Tests cloning multiple types of readers, modifying the deletedDocs and norms + * and verifies copy on write semantics of the deletedDocs and norms is implemented properly + */ +public class TestIndexReaderClone extends LuceneTestCase { + + public void testParallelReader() throws Exception { + final Directory dir1 = new MockRAMDirectory(); + TestIndexReaderReopen.createIndex(dir1, true); + final Directory dir2 = new MockRAMDirectory(); + TestIndexReaderReopen.createIndex(dir2, true); + IndexReader r1 = IndexReader.open(dir1); + IndexReader r2 = IndexReader.open(dir2); + + ParallelReader pr1 = new ParallelReader(); + pr1.add(r1); + pr1.add(r2); + + performDefaultTests(pr1); + pr1.close(); + dir1.close(); + dir2.close(); + } + + /** + * 1. Get a norm from the original reader + * 2. Clone the original reader + * 3. Delete a document and set the norm of the cloned reader + * 4. Verify the norms are not the same on each reader + * 5. Verify the doc deleted is only in the cloned reader + * 6. Try to delete a document in the original reader, an exception should be thrown + * @param r1 IndexReader to perform tests on + * @throws Exception + */ + private void performDefaultTests(IndexReader r1) throws Exception { + float norm1 = Similarity.decodeNorm(r1.norms("field1")[4]); + + IndexReader pr1Clone = (IndexReader)r1.clone(); + pr1Clone.deleteDocument(10); + pr1Clone.setNorm(4, "field1", 0.5f); + assertTrue(Similarity.decodeNorm(r1.norms("field1")[4]) == norm1); + assertTrue(Similarity.decodeNorm(pr1Clone.norms("field1")[4]) != norm1); + + assertTrue(!r1.isDeleted(10)); + assertTrue(pr1Clone.isDeleted(10)); + + // try to update the original reader, which should throw an exception + try { + r1.deleteDocument(11); + fail("Tried to delete doc 11 and an exception should have been thrown"); + } catch (Exception exception) { + // expectted + } + pr1Clone.close(); + } + + public void testMixedReaders() throws Exception { + final Directory dir1 = new MockRAMDirectory(); + TestIndexReaderReopen.createIndex(dir1, true); + final Directory dir2 = new MockRAMDirectory(); + TestIndexReaderReopen.createIndex(dir2, true); + IndexReader r1 = IndexReader.open(dir1); + IndexReader r2 = IndexReader.open(dir2); + + MultiReader multiReader = new MultiReader(new IndexReader[] {r1, r2}); + performDefaultTests(multiReader); + multiReader.close(); + dir1.close(); + dir2.close(); + } + + public void testSegmentReaderUndeleteall() throws Exception { + final Directory dir1 = new MockRAMDirectory(); + TestIndexReaderReopen.createIndex(dir1, false); + SegmentReader origSegmentReader = (SegmentReader)IndexReader.open(dir1); + origSegmentReader.deleteDocument(10); + assertDelDocsRefCountEquals(1, origSegmentReader); + origSegmentReader.undeleteAll(); + assertNull(origSegmentReader.deletedDocsRef); + origSegmentReader.close(); + // need to test norms? + dir1.close(); + } + + public void testSegmentReaderCloseReferencing() throws Exception { + final Directory dir1 = new MockRAMDirectory(); + TestIndexReaderReopen.createIndex(dir1, false); + SegmentReader origSegmentReader = (SegmentReader)IndexReader.open(dir1); + origSegmentReader.deleteDocument(1); + origSegmentReader.setNorm(4, "field1", 0.5f); + + SegmentReader clonedSegmentReader = (SegmentReader)origSegmentReader.clone(); + assertDelDocsRefCountEquals(2, origSegmentReader); + origSegmentReader.close(); + assertDelDocsRefCountEquals(1, origSegmentReader); + // check the norm refs + Norm norm = (Norm)clonedSegmentReader.norms.get("field1"); + assertEquals(1, norm.bytesRef().refCount()); + clonedSegmentReader.close(); + dir1.close(); + } + + public void testSegmentReaderDelDocsReferenceCounting() throws Exception { + final Directory dir1 = new MockRAMDirectory(); + TestIndexReaderReopen.createIndex(dir1, false); + SegmentReader origSegmentReader = (SegmentReader)IndexReader.open(dir1); + // deletedDocsRef should be null because nothing has updated yet + assertNull(origSegmentReader.deletedDocsRef); + + // we deleted a document, so there is now a deletedDocs bitvector and a reference to it + origSegmentReader.deleteDocument(1); + assertDelDocsRefCountEquals(1, origSegmentReader); + + // the cloned segmentreader should have 2 references, 1 to itself, and 1 to the original segmentreader + SegmentReader clonedSegmentReader = (SegmentReader)origSegmentReader.clone(); + assertDelDocsRefCountEquals(2, origSegmentReader); + // deleting a document creates a new deletedDocs bitvector, the refs goes to 1 + clonedSegmentReader.deleteDocument(2); + assertDelDocsRefCountEquals(1, origSegmentReader); + assertDelDocsRefCountEquals(1, clonedSegmentReader); + + // make sure the deletedocs objects are different + assertTrue(origSegmentReader.deletedDocs != clonedSegmentReader.deletedDocs); + + assertDocDeleted(origSegmentReader, clonedSegmentReader, 1); + assertTrue(!origSegmentReader.isDeleted(2)); // doc 2 should not be deleted in original segmentreader + assertTrue(clonedSegmentReader.isDeleted(2)); // doc 2 should be deleted in cloned segmentreader + + // deleting a doc from the original segmentreader should throw an exception + try { + origSegmentReader.deleteDocument(4); + fail("expected exception"); + } catch (LockObtainFailedException lbfe) { + // expected + } + + origSegmentReader.close(); + // try closing the original segment reader to see if it affects the clonedSegmentReader + clonedSegmentReader.deleteDocument(3); + clonedSegmentReader.flush(); + assertDelDocsRefCountEquals(1, clonedSegmentReader); + + // test a reopened reader + SegmentReader reopenedSegmentReader = (SegmentReader)clonedSegmentReader.reopen(); + SegmentReader cloneSegmentReader2 = (SegmentReader)reopenedSegmentReader.clone(); + assertDelDocsRefCountEquals(2, cloneSegmentReader2); + clonedSegmentReader.close(); + reopenedSegmentReader.close(); + cloneSegmentReader2.close(); + + dir1.close(); + } + + private void assertDocDeleted(SegmentReader reader, SegmentReader reader2, int doc) { + assertEquals(reader.isDeleted(doc), reader2.isDeleted(doc)); + } + + private void assertDelDocsRefCountEquals(int refCount, SegmentReader reader) { + assertEquals(refCount, reader.deletedDocsRef.refCount()); + } +} Property changes on: src/test/org/apache/lucene/index/TestIndexReaderClone.java ___________________________________________________________________ Name: svn:eol-style + native Index: src/test/org/apache/lucene/index/TestIndexReaderCloneNorms.java =================================================================== --- src/test/org/apache/lucene/index/TestIndexReaderCloneNorms.java (revision 0) +++ src/test/org/apache/lucene/index/TestIndexReaderCloneNorms.java (revision 0) @@ -0,0 +1,314 @@ +package org.apache.lucene.index; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.Field.Index; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.index.SegmentReader.Norm; +import org.apache.lucene.search.DefaultSimilarity; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.store.MockRAMDirectory; +import org.apache.lucene.util.LuceneTestCase; + +/** + * Tests cloning IndexReader norms + */ +public class TestIndexReaderCloneNorms extends LuceneTestCase { + + private class SimilarityOne extends DefaultSimilarity { + public float lengthNorm(String fieldName, int numTerms) { + return 1; + } + } + + private static final int NUM_FIELDS = 10; + + private Similarity similarityOne; + + private Analyzer anlzr; + + private int numDocNorms; + + private ArrayList norms; + + private ArrayList modifiedNorms; + + private float lastNorm = 0; + + private float normDelta = (float) 0.001; + + public TestIndexReaderCloneNorms(String s) { + super(s); + } + + protected void setUp() throws Exception { + super.setUp(); + similarityOne = new SimilarityOne(); + anlzr = new StandardAnalyzer(); + } + + /** + * Test that norms values are preserved as the index is maintained. Including + * separate norms. Including merging indexes with seprate norms. Including + * optimize. + */ + public void testNorms() throws IOException { + // tmp dir + String tempDir = System.getProperty("java.io.tmpdir"); + if (tempDir == null) { + throw new IOException("java.io.tmpdir undefined, cannot run test"); + } + + // test with a single index: index1 + File indexDir1 = new File(tempDir, "lucenetestindex1"); + Directory dir1 = FSDirectory.getDirectory(indexDir1); + IndexWriter.unlock(dir1); + + norms = new ArrayList(); + modifiedNorms = new ArrayList(); + + createIndex(dir1); + doTestNorms(dir1); + + // test with a single index: index2 + ArrayList norms1 = norms; + ArrayList modifiedNorms1 = modifiedNorms; + int numDocNorms1 = numDocNorms; + + norms = new ArrayList(); + modifiedNorms = new ArrayList(); + numDocNorms = 0; + + File indexDir2 = new File(tempDir, "lucenetestindex2"); + Directory dir2 = FSDirectory.getDirectory(indexDir2); + + createIndex(dir2); + doTestNorms(dir2); + + // add index1 and index2 to a third index: index3 + File indexDir3 = new File(tempDir, "lucenetestindex3"); + Directory dir3 = FSDirectory.getDirectory(indexDir3); + + createIndex(dir3); + IndexWriter iw = new IndexWriter(dir3, anlzr, false, + IndexWriter.MaxFieldLength.LIMITED); + iw.setMaxBufferedDocs(5); + iw.setMergeFactor(3); + iw.addIndexes(new Directory[] { dir1, dir2 }); + iw.close(); + + norms1.addAll(norms); + norms = norms1; + modifiedNorms1.addAll(modifiedNorms); + modifiedNorms = modifiedNorms1; + numDocNorms += numDocNorms1; + + // test with index3 + verifyIndex(dir3); + doTestNorms(dir3); + + // now with optimize + iw = new IndexWriter(dir3, anlzr, false, IndexWriter.MaxFieldLength.LIMITED); + iw.setMaxBufferedDocs(5); + iw.setMergeFactor(3); + iw.optimize(); + iw.close(); + verifyIndex(dir3); + + dir1.close(); + dir2.close(); + dir3.close(); + } + + // try cloning and reopening the norms + private void doTestNorms(Directory dir) throws IOException { + addDocs(dir, 12, true); + IndexReader ir = IndexReader.open(dir); + verifyIndex(ir); + modifyNormsForF1(ir); + IndexReader irc = (IndexReader) ir.clone();// IndexReader.open(dir);//ir.clone(); + verifyIndex(irc); + + modifyNormsForF1(irc); + + IndexReader irc3 = (IndexReader) irc.clone(); + verifyIndex(irc3); + modifyNormsForF1(irc3); + verifyIndex(irc3); + irc3.flush(); + irc3.close(); + } + + public void testNormsClose() throws IOException { + Directory dir1 = new MockRAMDirectory(); + TestIndexReaderReopen.createIndex(dir1, false); + SegmentReader reader1 = (SegmentReader) IndexReader.open(dir1); + reader1.norms("field1"); + Norm r1norm = (Norm)reader1.norms.get("field1"); + SegmentReader.Ref r1BytesRef = r1norm.bytesRef(); + SegmentReader reader2 = (SegmentReader)reader1.clone(); + assertEquals(2, r1norm.bytesRef().refCount()); + reader1.close(); + assertEquals(1, r1BytesRef.refCount()); + reader2.norms("field1"); + reader2.close(); + dir1.close(); + } + + public void testNormsRefCounting() throws IOException { + Directory dir1 = new MockRAMDirectory(); + TestIndexReaderReopen.createIndex(dir1, false); + SegmentReader reader1 = (SegmentReader) IndexReader.open(dir1); + + SegmentReader reader2C = (SegmentReader)reader1.clone(); + reader2C.norms("field1"); // load the norms for the field + Norm reader2CNorm = (Norm)reader2C.norms.get("field1"); + assertTrue("reader2CNorm.bytesRef()=" + reader2CNorm.bytesRef(), reader2CNorm.bytesRef().refCount() == 2); + + // generate a cannot update exception in reader1 + try { + reader1.setNorm(1, "field1", 0.99f); + fail("did not hit expected exception"); + } catch (Exception ex) { + // expected + } + + SegmentReader reader3C = (SegmentReader)reader2C.clone(); + Norm reader3CCNorm = (Norm)reader3C.norms.get("field1"); + assertEquals(3, reader3CCNorm.bytesRef().refCount()); + + // edit a norm and the refcount should be 1 + SegmentReader reader4C = (SegmentReader)reader3C.clone(); + assertEquals(4, reader3CCNorm.bytesRef().refCount()); + reader4C.setNorm(5, "field1", 0.33f); + + // norm values should be different + assertTrue(Similarity.decodeNorm(reader3C.norms("field1")[5]) != Similarity.decodeNorm(reader4C.norms("field1")[5])); + Norm reader4CCNorm = (Norm)reader4C.norms.get("field1"); + assertEquals(3, reader3CCNorm.bytesRef().refCount()); + assertEquals(1, reader4CCNorm.bytesRef().refCount()); + + SegmentReader reader5C = (SegmentReader)reader4C.clone(); + Norm reader5CCNorm = (Norm)reader5C.norms.get("field1"); + reader5C.setNorm(5, "field1", 0.7f); + assertEquals(1, reader5CCNorm.bytesRef().refCount()); + + reader5C.close(); + reader4C.close(); + reader3C.close(); + reader2C.close(); + reader1.close(); + dir1.close(); + } + + private void createIndex(Directory dir) throws IOException { + IndexWriter iw = new IndexWriter(dir, anlzr, true, + IndexWriter.MaxFieldLength.LIMITED); + iw.setMaxBufferedDocs(5); + iw.setMergeFactor(3); + iw.setSimilarity(similarityOne); + iw.setUseCompoundFile(true); + iw.close(); + } + + private void modifyNormsForF1(Directory dir) throws IOException { + IndexReader ir = IndexReader.open(dir); + modifyNormsForF1(ir); + } + + private void modifyNormsForF1(IndexReader ir) throws IOException { + int n = ir.maxDoc(); + // System.out.println("modifyNormsForF1 maxDoc: "+n); + for (int i = 0; i < n; i += 3) { // modify for every third doc + int k = (i * 3) % modifiedNorms.size(); + float origNorm = ((Float) modifiedNorms.get(i)).floatValue(); + float newNorm = ((Float) modifiedNorms.get(k)).floatValue(); + // System.out.println("Modifying: for "+i+" from "+origNorm+" to + // "+newNorm); + // System.out.println(" and: for "+k+" from "+newNorm+" to "+origNorm); + modifiedNorms.set(i, new Float(newNorm)); + modifiedNorms.set(k, new Float(origNorm)); + ir.setNorm(i, "f" + 1, newNorm); + ir.setNorm(k, "f" + 1, origNorm); + // System.out.println("setNorm i: "+i); + // break; + } + // ir.close(); + } + + private void verifyIndex(Directory dir) throws IOException { + IndexReader ir = IndexReader.open(dir); + verifyIndex(ir); + ir.close(); + } + + private void verifyIndex(IndexReader ir) throws IOException { + for (int i = 0; i < NUM_FIELDS; i++) { + String field = "f" + i; + byte b[] = ir.norms(field); + assertEquals("number of norms mismatches", numDocNorms, b.length); + ArrayList storedNorms = (i == 1 ? modifiedNorms : norms); + for (int j = 0; j < b.length; j++) { + float norm = Similarity.decodeNorm(b[j]); + float norm1 = ((Float) storedNorms.get(j)).floatValue(); + assertEquals("stored norm value of " + field + " for doc " + j + " is " + + norm + " - a mismatch!", norm, norm1, 0.000001); + } + } + } + + private void addDocs(Directory dir, int ndocs, boolean compound) + throws IOException { + IndexWriter iw = new IndexWriter(dir, anlzr, false, + IndexWriter.MaxFieldLength.LIMITED); + iw.setMaxBufferedDocs(5); + iw.setMergeFactor(3); + iw.setSimilarity(similarityOne); + iw.setUseCompoundFile(compound); + for (int i = 0; i < ndocs; i++) { + iw.addDocument(newDoc()); + } + iw.close(); + } + + // create the next document + private Document newDoc() { + Document d = new Document(); + float boost = nextNorm(); + for (int i = 0; i < 10; i++) { + Field f = new Field("f" + i, "v" + i, Store.NO, Index.NOT_ANALYZED); + f.setBoost(boost); + d.add(f); + } + return d; + } + + // return unique norm values that are unchanged by encoding/decoding + private float nextNorm() { + float norm = lastNorm + normDelta; + do { + float norm1 = Similarity.decodeNorm(Similarity.encodeNorm(norm)); + if (norm1 > lastNorm) { + // System.out.println(norm1+" > "+lastNorm); + norm = norm1; + break; + } + norm += normDelta; + } while (true); + norms.add(numDocNorms, new Float(norm)); + modifiedNorms.add(numDocNorms, new Float(norm)); + // System.out.println("creating norm("+numDocNorms+"): "+norm); + numDocNorms++; + lastNorm = (norm > 10 ? 0 : norm); // there's a limit to how many distinct + // values can be stored in a ingle byte + return norm; + } +} Property changes on: src/test/org/apache/lucene/index/TestIndexReaderCloneNorms.java ___________________________________________________________________ Name: svn:eol-style + native Index: src/test/org/apache/lucene/index/TestIndexReaderReopen.java =================================================================== --- src/test/org/apache/lucene/index/TestIndexReaderReopen.java (revision 733039) +++ src/test/org/apache/lucene/index/TestIndexReaderReopen.java (working copy) @@ -40,7 +40,7 @@ import org.apache.lucene.search.TermQuery; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; -import org.apache.lucene.store.RAMDirectory; +import org.apache.lucene.store.MockRAMDirectory; import org.apache.lucene.store.AlreadyClosedException; import org.apache.lucene.util.LuceneTestCase; @@ -49,7 +49,7 @@ private File indexDir; public void testReopen() throws Exception { - final Directory dir1 = new RAMDirectory(); + final Directory dir1 = new MockRAMDirectory(); createIndex(dir1, false); performDefaultTests(new TestReopen() { @@ -63,8 +63,9 @@ } }); + dir1.close(); - final Directory dir2 = new RAMDirectory(); + final Directory dir2 = new MockRAMDirectory(); createIndex(dir2, true); performDefaultTests(new TestReopen() { @@ -78,12 +79,13 @@ } }); + dir2.close(); } public void testParallelReaderReopen() throws Exception { - final Directory dir1 = new RAMDirectory(); + final Directory dir1 = new MockRAMDirectory(); createIndex(dir1, true); - final Directory dir2 = new RAMDirectory(); + final Directory dir2 = new MockRAMDirectory(); createIndex(dir2, true); performDefaultTests(new TestReopen() { @@ -101,10 +103,12 @@ } }); + dir1.close(); + dir2.close(); - final Directory dir3 = new RAMDirectory(); + final Directory dir3 = new MockRAMDirectory(); createIndex(dir3, true); - final Directory dir4 = new RAMDirectory(); + final Directory dir4 = new MockRAMDirectory(); createIndex(dir4, true); performTestsWithExceptionInReopen(new TestReopen() { @@ -118,11 +122,15 @@ ParallelReader pr = new ParallelReader(); pr.add(IndexReader.open(dir3)); pr.add(IndexReader.open(dir4)); + // Does not implement reopen, so + // hits exception: pr.add(new FilterIndexReader(IndexReader.open(dir3))); return pr; } }); + dir3.close(); + dir4.close(); } // LUCENE-1228: IndexWriter.commit() does not update the index version @@ -133,17 +141,20 @@ public void testCommitReopenFS () throws IOException { Directory dir = FSDirectory.getDirectory(indexDir); doTestReopenWithCommit(dir, true); + dir.close(); } public void testCommitRecreateFS () throws IOException { Directory dir = FSDirectory.getDirectory(indexDir); doTestReopenWithCommit(dir, false); + dir.close(); } public void testCommitReopenRAM () throws IOException { - Directory dir = new RAMDirectory(); + Directory dir = new MockRAMDirectory(); doTestReopenWithCommit(dir, true); + dir.close(); } public void testCommitRecreateRAM () throws IOException { - Directory dir = new RAMDirectory(); + Directory dir = new MockRAMDirectory(); doTestReopenWithCommit(dir, false); } @@ -188,9 +199,11 @@ } public void testMultiReaderReopen() throws Exception { - final Directory dir1 = new RAMDirectory(); + + final Directory dir1 = new MockRAMDirectory(); createIndex(dir1, true); - final Directory dir2 = new RAMDirectory(); + + final Directory dir2 = new MockRAMDirectory(); createIndex(dir2, true); performDefaultTests(new TestReopen() { @@ -207,10 +220,14 @@ } }); + + dir1.close(); + dir2.close(); - final Directory dir3 = new RAMDirectory(); + final Directory dir3 = new MockRAMDirectory(); createIndex(dir3, true); - final Directory dir4 = new RAMDirectory(); + + final Directory dir4 = new MockRAMDirectory(); createIndex(dir4, true); performTestsWithExceptionInReopen(new TestReopen() { @@ -224,23 +241,26 @@ return new MultiReader(new IndexReader[] {IndexReader.open(dir3), IndexReader.open(dir4), + // Does not implement reopen, so + // hits exception: new FilterIndexReader(IndexReader.open(dir3))}); } }); - + dir3.close(); + dir4.close(); } public void testMixedReaders() throws Exception { - final Directory dir1 = new RAMDirectory(); + final Directory dir1 = new MockRAMDirectory(); createIndex(dir1, true); - final Directory dir2 = new RAMDirectory(); + final Directory dir2 = new MockRAMDirectory(); createIndex(dir2, true); - final Directory dir3 = new RAMDirectory(); + final Directory dir3 = new MockRAMDirectory(); createIndex(dir3, false); - final Directory dir4 = new RAMDirectory(); + final Directory dir4 = new MockRAMDirectory(); createIndex(dir4, true); - final Directory dir5 = new RAMDirectory(); + final Directory dir5 = new MockRAMDirectory(); createIndex(dir5, false); performDefaultTests(new TestReopen() { @@ -263,33 +283,40 @@ pr, mr, IndexReader.open(dir5)}); } }); + dir1.close(); + dir2.close(); + dir3.close(); + dir4.close(); + dir5.close(); } private void performDefaultTests(TestReopen test) throws Exception { + IndexReader index1 = test.openReader(); IndexReader index2 = test.openReader(); TestIndexReader.assertIndexEquals(index1, index2); - + // verify that reopen() does not return a new reader instance // in case the index has no changes ReaderCouple couple = refreshReader(index2, false); assertTrue(couple.refreshedReader == index2); couple = refreshReader(index2, test, 0, true); + index1.close(); index1 = couple.newReader; + IndexReader index2_refreshed = couple.refreshedReader; index2.close(); // test if refreshed reader and newly opened reader return equal results TestIndexReader.assertIndexEquals(index1, index2_refreshed); - - index1.close(); + index2_refreshed.close(); assertReaderClosed(index2, true, true); assertReaderClosed(index2_refreshed, true, true); - + index2 = test.openReader(); for (int i = 1; i < 4; i++) { @@ -313,7 +340,7 @@ public void testReferenceCounting() throws IOException { for (int mode = 0; mode < 4; mode++) { - Directory dir1 = new RAMDirectory(); + Directory dir1 = new MockRAMDirectory(); createIndex(dir1, true); IndexReader reader0 = IndexReader.open(dir1); @@ -408,21 +435,24 @@ assertReaderClosed(reader1, true, true); assertReaderClosed(reader2, true, true); assertReaderClosed(reader3, true, true); + + dir1.close(); } } public void testReferenceCountingMultiReader() throws IOException { for (int mode = 0; mode <=1; mode++) { - Directory dir1 = new RAMDirectory(); + Directory dir1 = new MockRAMDirectory(); createIndex(dir1, false); - Directory dir2 = new RAMDirectory(); + Directory dir2 = new MockRAMDirectory(); createIndex(dir2, true); IndexReader reader1 = IndexReader.open(dir1); assertRefCountEquals(1, reader1); - - IndexReader multiReader1 = new MultiReader(new IndexReader[] {reader1, IndexReader.open(dir2)}, (mode == 0)); + + IndexReader initReader2 = IndexReader.open(dir2); + IndexReader multiReader1 = new MultiReader(new IndexReader[] {reader1, initReader2}, (mode == 0)); modifyIndex(0, dir2); assertRefCountEquals(1 + mode, reader1); @@ -444,6 +474,10 @@ multiReader1.close(); assertRefCountEquals(3 + mode, reader1); + + if (mode == 1) { + initReader2.close(); + } reader1.close(); assertRefCountEquals(3, reader1); @@ -468,15 +502,17 @@ reader3.close(); assertRefCountEquals(0, reader1); assertReaderClosed(reader1, true, true); + dir1.close(); + dir2.close(); } } public void testReferenceCountingParallelReader() throws IOException { for (int mode = 0; mode <=1; mode++) { - Directory dir1 = new RAMDirectory(); + Directory dir1 = new MockRAMDirectory(); createIndex(dir1, false); - Directory dir2 = new RAMDirectory(); + Directory dir2 = new MockRAMDirectory(); createIndex(dir2, true); IndexReader reader1 = IndexReader.open(dir1); @@ -484,7 +520,8 @@ ParallelReader parallelReader1 = new ParallelReader(mode == 0); parallelReader1.add(reader1); - parallelReader1.add(IndexReader.open(dir2)); + IndexReader initReader2 = IndexReader.open(dir2); + parallelReader1.add(initReader2); modifyIndex(1, dir2); assertRefCountEquals(1 + mode, reader1); @@ -507,6 +544,10 @@ parallelReader1.close(); assertRefCountEquals(3 + mode, reader1); + + if (mode == 1) { + initReader2.close(); + } reader1.close(); assertRefCountEquals(3, reader1); @@ -531,12 +572,15 @@ reader3.close(); assertRefCountEquals(0, reader1); assertReaderClosed(reader1, true, true); + + dir1.close(); + dir2.close(); } } public void testNormsRefCounting() throws IOException { - Directory dir1 = new RAMDirectory(); + Directory dir1 = new MockRAMDirectory(); createIndex(dir1, false); SegmentReader reader1 = (SegmentReader) IndexReader.open(dir1); @@ -554,25 +598,31 @@ modifier = IndexReader.open(dir1); modifier.deleteDocument(2); modifier.close(); + SegmentReader reader4 = (SegmentReader) reader3.reopen(); - modifier = IndexReader.open(dir1); modifier.deleteDocument(3); modifier.close(); + SegmentReader reader5 = (SegmentReader) reader3.reopen(); // Now reader2-reader5 references reader1. reader1 and reader2 // share the same norms. reader3, reader4, reader5 also share norms. assertRefCountEquals(5, reader1); assertFalse(reader1.normsClosed()); + reader1.close(); + assertRefCountEquals(4, reader1); assertFalse(reader1.normsClosed()); + reader2.close(); assertRefCountEquals(3, reader1); + // now the norms for field1 and field2 should be closed assertTrue(reader1.normsClosed("field1")); assertTrue(reader1.normsClosed("field2")); + // but the norms for field3 and field4 should still be open assertFalse(reader1.normsClosed("field3")); assertFalse(reader1.normsClosed("field4")); @@ -593,6 +643,8 @@ // the norms that those three readers shared should be // closed as well assertTrue(reader3.normsClosed()); + + dir1.close(); } private void performTestsWithExceptionInReopen(TestReopen test) throws Exception { @@ -610,10 +662,13 @@ // index2 should still be usable and unaffected by the failed reopen() call TestIndexReader.assertIndexEquals(index1, index2); + + index1.close(); + index2.close(); } public void testThreadSafety() throws Exception { - final Directory dir = new RAMDirectory(); + final Directory dir = new MockRAMDirectory(); final int n = 150; IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), IndexWriter.MaxFieldLength.LIMITED); @@ -779,6 +834,8 @@ assertReaderClosed(reader, true, true); assertReaderClosed(firstReader, true, true); + + dir.close(); } private static class ReaderCouple { @@ -829,7 +886,7 @@ return refreshReader(reader, null, -1, hasChanges); } - private ReaderCouple refreshReader(IndexReader reader, TestReopen test, int modify, boolean hasChanges) throws IOException { + ReaderCouple refreshReader(IndexReader reader, TestReopen test, int modify, boolean hasChanges) throws IOException { synchronized (createReaderMutex) { IndexReader r = null; if (test != null) { @@ -837,7 +894,16 @@ r = test.openReader(); } - IndexReader refreshed = reader.reopen(); + IndexReader refreshed = null; + try { + refreshed = reader.reopen(); + } finally { + if (refreshed == null && r != null) { + // Hit exception -- close opened reader + r.close(); + } + } + if (hasChanges) { if (refreshed == reader) { fail("No new IndexReader instance created during refresh."); @@ -852,9 +918,10 @@ } } - private static void createIndex(Directory dir, boolean multiSegment) throws IOException { + public static void createIndex(Directory dir, boolean multiSegment) throws IOException { + IndexWriter.unlock(dir); IndexWriter w = new IndexWriter(dir, new WhitespaceAnalyzer(), IndexWriter.MaxFieldLength.LIMITED); - + w.setMergePolicy(new LogDocMergePolicy()); for (int i = 0; i < 100; i++) { Index: src/java/org/apache/lucene/index/DirectoryIndexReader.java =================================================================== --- src/java/org/apache/lucene/index/DirectoryIndexReader.java (revision 733039) +++ src/java/org/apache/lucene/index/DirectoryIndexReader.java (working copy) @@ -37,7 +37,7 @@ * "own" the directory, which means that they try to acquire a write lock * whenever index modifications are performed. */ -abstract class DirectoryIndexReader extends IndexReader { +abstract class DirectoryIndexReader extends IndexReader implements Cloneable { protected Directory directory; protected boolean closeDirectory; private IndexDeletionPolicy deletionPolicy; @@ -134,29 +134,51 @@ return reader; } - + public final synchronized IndexReader reopen() throws CorruptIndexException, IOException { + return doReopen(false); + } + + public final synchronized Object clone() { + try { + return doReopen(true); + } catch (Exception ex) { + throw new RuntimeException("", ex); + } + } + + protected final synchronized IndexReader doReopen(final boolean doClone) throws CorruptIndexException, IOException { ensureOpen(); - - if (this.hasChanges || this.isCurrent()) { + if (!doClone && (this.hasChanges || this.isCurrent())) { + // if we're cloning, then this does not apply // this has changes, therefore we have the lock and don't need to reopen // OR: the index in the directory hasn't changed - nothing to do here return this; } - final SegmentInfos.FindSegmentsFile finder = new SegmentInfos.FindSegmentsFile(directory) { protected Object doBody(String segmentFileName) throws CorruptIndexException, IOException { - SegmentInfos infos = new SegmentInfos(); - infos.read(directory, segmentFileName); - - DirectoryIndexReader newReader = doReopen(infos); + SegmentInfos infos = null; + if (doClone) { + infos = (SegmentInfos)segmentInfos.clone(); + } else { + infos = new SegmentInfos(); + infos.read(directory, segmentFileName); + } + DirectoryIndexReader newReader = doReopen(infos, doClone); if (DirectoryIndexReader.this != newReader) { newReader.init(directory, infos, closeDirectory, readOnly); newReader.deletionPolicy = deletionPolicy; } - + // copy the writeLock if there is one to the new reader and make the current + // reader readOnly and null the writeLock + if (doClone) { + newReader.writeLock = writeLock; + writeLock = null; + readOnly = true; + hasChanges = false; + } return newReader; } }; @@ -193,7 +215,7 @@ /** * Re-opens the index using the passed-in SegmentInfos */ - protected abstract DirectoryIndexReader doReopen(SegmentInfos infos) throws CorruptIndexException, IOException; + protected abstract DirectoryIndexReader doReopen(SegmentInfos infos, boolean doClone) throws CorruptIndexException, IOException; public void setDeletionPolicy(IndexDeletionPolicy deletionPolicy) { this.deletionPolicy = deletionPolicy; @@ -341,6 +363,9 @@ * @throws IOException if there is a low-level IO error */ protected void acquireWriteLock() throws StaleReaderException, CorruptIndexException, LockObtainFailedException, IOException { + if (readOnly) { + throw new LockObtainFailedException("This IndexReader cannot make any changes to the index (as it is readonly)"); + } if (segmentInfos != null) { ensureOpen(); if (stale) Index: src/java/org/apache/lucene/index/ParallelReader.java =================================================================== --- src/java/org/apache/lucene/index/ParallelReader.java (revision 733039) +++ src/java/org/apache/lucene/index/ParallelReader.java (working copy) @@ -122,7 +122,15 @@ } decrefOnClose.add(Boolean.valueOf(incRefReaders)); } - + + public synchronized Object clone() { + try { + return doReopen(true); + } catch (Exception ex) { + throw new RuntimeException("", ex); + } + } + /** * Tries to reopen the subreaders. *
@@ -142,20 +150,27 @@ * @throws CorruptIndexException if the index is corrupt * @throws IOException if there is a low-level IO error */ - public IndexReader reopen() throws CorruptIndexException, IOException { + public synchronized IndexReader reopen() throws CorruptIndexException, IOException { + return doReopen(false); + } + + protected IndexReader doReopen(boolean doClone) throws CorruptIndexException, IOException { ensureOpen(); boolean reopened = false; List newReaders = new ArrayList(); - List newDecrefOnClose = new ArrayList(); boolean success = false; try { - for (int i = 0; i < readers.size(); i++) { IndexReader oldReader = (IndexReader) readers.get(i); - IndexReader newReader = oldReader.reopen(); + IndexReader newReader = null; + if (doClone) { + newReader = (IndexReader) oldReader.clone(); + } else { + newReader = oldReader.reopen(); + } newReaders.add(newReader); // if at least one of the subreaders was updated we remember that // and return a new MultiReader @@ -163,42 +178,14 @@ reopened = true; } } - - if (reopened) { - ParallelReader pr = new ParallelReader(); - for (int i = 0; i < readers.size(); i++) { - IndexReader oldReader = (IndexReader) readers.get(i); - IndexReader newReader = (IndexReader) newReaders.get(i); - if (newReader == oldReader) { - newDecrefOnClose.add(Boolean.TRUE); - newReader.incRef(); - } else { - // this is a new subreader instance, so on close() we don't - // decRef but close it - newDecrefOnClose.add(Boolean.FALSE); - } - pr.add(newReader, !storedFieldReaders.contains(oldReader)); - } - pr.decrefOnClose = newDecrefOnClose; - pr.incRefReaders = incRefReaders; - success = true; - return pr; - } else { - success = true; - // No subreader was refreshed - return this; - } + success = true; } finally { if (!success && reopened) { for (int i = 0; i < newReaders.size(); i++) { IndexReader r = (IndexReader) newReaders.get(i); - if (r != null) { + if (r != readers.get(i)) { try { - if (((Boolean) newDecrefOnClose.get(i)).booleanValue()) { - r.decRef(); - } else { - r.close(); - } + r.close(); } catch (IOException ignore) { // keep going - we want to clean up as much as possible } @@ -206,6 +193,30 @@ } } } + + if (reopened) { + List newDecrefOnClose = new ArrayList(); + ParallelReader pr = new ParallelReader(); + for (int i = 0; i < readers.size(); i++) { + IndexReader oldReader = (IndexReader) readers.get(i); + IndexReader newReader = (IndexReader) newReaders.get(i); + if (newReader == oldReader) { + newDecrefOnClose.add(Boolean.TRUE); + newReader.incRef(); + } else { + // this is a new subreader instance, so on close() we don't + // decRef but close it + newDecrefOnClose.add(Boolean.FALSE); + } + pr.add(newReader, !storedFieldReaders.contains(oldReader)); + } + pr.decrefOnClose = newDecrefOnClose; + pr.incRefReaders = incRefReaders; + return pr; + } else { + // No subreader was refreshed + return this; + } } Index: src/java/org/apache/lucene/index/SegmentReader.java =================================================================== --- src/java/org/apache/lucene/index/SegmentReader.java (revision 733039) +++ src/java/org/apache/lucene/index/SegmentReader.java (working copy) @@ -48,12 +48,13 @@ FieldInfos fieldInfos; private FieldsReader fieldsReaderOrig = null; - + CloseableThreadLocal fieldsReaderLocal = new FieldsReaderLocal(); TermInfosReader tis; TermVectorsReader termVectorsReaderOrig = null; CloseableThreadLocal termVectorsLocal = new CloseableThreadLocal(); BitVector deletedDocs = null; + Ref deletedDocsRef = null; private boolean deletedDocsDirty = false; private boolean normsDirty = false; private boolean undeleteAll = false; @@ -64,12 +65,12 @@ private boolean rollbackUndeleteAll = false; private int rollbackPendingDeleteCount; private boolean readOnly; - IndexInput freqStream; IndexInput proxStream; // optionally used for the .nrm file shared by multiple norms private IndexInput singleNormStream; + private Ref singleNormRef; // Compound File Reader when based on a compound file segment CompoundFileReader cfsReader = null; @@ -87,43 +88,238 @@ return (FieldsReader) fieldsReaderOrig.clone(); } } - CloseableThreadLocal fieldsReaderLocal = new FieldsReaderLocal(); - private class Norm { - volatile int refCount; - boolean useSingleNormStream; + static class Ref { + private int refCount = 1; - public synchronized void incRef() { + public String toString() { + return "refcount: "+refCount; + } + + public synchronized int refCount() { + return refCount; + } + + public synchronized int incRef() { assert refCount > 0; refCount++; + return refCount; } - public synchronized void decRef() throws IOException { + public synchronized int decRef() { assert refCount > 0; - if (refCount == 1) { - close(); - } refCount--; - + return refCount; } - - public Norm(IndexInput in, boolean useSingleNormStream, int number, long normSeek) - { - refCount = 1; - this.in = in; - this.number = number; - this.normSeek = normSeek; - this.useSingleNormStream = useSingleNormStream; - } + } + + /** + * Byte[] referencing is used because a new norm object needs + * to be created for each clone, and the byte array is all + * that is needed for sharing between cloned readers. The + * current norm referencing is for sharing between readers + * whereas the byte[] referencing is for copy on write which + * is independent of reader references (i.e. incRef, decRef). + */ + final class Norm implements Cloneable { + private int refCount = 1; + + // If this instance is a clone, the originalNorm + // references the Norm that has a real open IndexInput: + private Norm origNorm; + private IndexInput in; + private long normSeek; + + // null until bytes is set + private Ref bytesRef; private byte[] bytes; private boolean dirty; private int number; - private long normSeek; private boolean rollbackDirty; + + public Norm(IndexInput in, int number, long normSeek) { + this.in = in; + this.number = number; + this.normSeek = normSeek; + } - private void reWrite(SegmentInfo si) throws IOException { + public synchronized void incRef() { + assert refCount > 0 && (origNorm == null || origNorm.refCount > 0); + refCount++; + } + + private void closeInput() throws IOException { + if (in != null) { + if (in != singleNormStream) { + // It's private to us -- just close it + in.close(); + } else { + // We are sharing this with others -- decRef and + // maybe close the shared norm stream + if (singleNormRef.decRef() == 0) { + singleNormStream.close(); + singleNormStream = null; + } + } + + in = null; + } + } + + public synchronized void decRef() throws IOException { + assert refCount > 0 && (origNorm == null || origNorm.refCount > 0); + + if (--refCount == 0) { + if (origNorm != null) { + origNorm.decRef(); + origNorm = null; + } else { + closeInput(); + } + + if (bytes != null) { + assert bytesRef != null; + bytesRef.decRef(); + bytes = null; + bytesRef = null; + } else { + assert bytesRef == null; + } + } + } + + // Load bytes but do not cache them if they were not + // already cached + public synchronized void bytes(byte[] bytesOut, int offset, int len) throws IOException { + assert refCount > 0 && (origNorm == null || origNorm.refCount > 0); + if (bytes != null) { + // Already cached -- copy from cache: + assert len <= maxDoc(); + System.arraycopy(bytes, 0, bytesOut, offset, len); + } else { + // Not cached + if (origNorm != null) { + // Ask origNorm to load + origNorm.bytes(bytesOut, offset, len); + } else { + // We are orig -- read ourselves from disk: + synchronized(in) { + in.seek(normSeek); + in.readBytes(bytesOut, offset, len, false); + } + } + } + } + + // Load & cache full bytes array. Returns bytes. + public synchronized byte[] bytes() throws IOException { + assert refCount > 0 && (origNorm == null || origNorm.refCount > 0); + if (bytes == null) { // value not yet read + assert bytesRef == null; + if (origNorm != null) { + // Ask origNorm to load so that for a series of + // reopened readers we share a single read-only + // byte[] + bytes = origNorm.bytes(); + bytesRef = origNorm.bytesRef; + bytesRef.incRef(); + + // Once we've loaded the bytes we no longer need + // origNorm: + origNorm.decRef(); + origNorm = null; + + } else { + // We are the origNorm, so load the bytes for real + // ourself: + final int count = maxDoc(); + bytes = new byte[count]; + + // Since we are orig, in must not be null + assert in != null; + + // Read from disk. + synchronized(in) { + in.seek(normSeek); + in.readBytes(bytes, 0, count, false); + } + + bytesRef = new Ref(); + closeInput(); + } + } + + return bytes; + } + + // Only for testing + Ref bytesRef() { + return bytesRef; + } + + // Called if we intend to change a norm value. We make a + // private copy of bytes if it's shared with others: + public synchronized byte[] copyOnWrite() throws IOException { + assert refCount > 0 && (origNorm == null || origNorm.refCount > 0); + bytes(); + assert bytes != null; + assert bytesRef != null; + if (bytesRef.refCount() > 1) { + // I cannot be the origNorm for another norm + // instance if I'm being changed. Ie, only the + // "head Norm" can be changed: + assert refCount == 1; + final Ref oldRef = bytesRef; + bytes = cloneNormBytes(bytes); + bytesRef = new Ref(); + oldRef.decRef(); + } + dirty = true; + return bytes; + } + + // Returns a copy of this Norm instance that shares + // IndexInput & bytes with the original one + public synchronized Object clone() { + assert refCount > 0 && (origNorm == null || origNorm.refCount > 0); + + Norm clone; + try { + clone = (Norm) super.clone(); + } catch (CloneNotSupportedException cnse) { + // Cannot happen + throw new RuntimeException("unexpected CloneNotSupportedException", cnse); + } + clone.refCount = 1; + + if (bytes != null) { + assert bytesRef != null; + assert origNorm == null; + + // Clone holds a reference to my bytes: + clone.bytesRef.incRef(); + } else { + assert bytesRef == null; + if (origNorm == null) { + // I become the origNorm for the clone: + clone.origNorm = this; + } + clone.origNorm.incRef(); + } + + // Only the origNorm will actually readBytes from in: + clone.in = null; + + return clone; + } + + // Flush all pending changes to the next generation + // separate norms file. + public void reWrite(SegmentInfo si) throws IOException { + assert refCount > 0 && (origNorm == null || origNorm.refCount > 0): "refCount=" + refCount + " origNorm=" + origNorm; + // NOTE: norms are re-written in regular directory, not cfs si.advanceNormGen(this.number); IndexOutput out = directory().createOutput(si.getNormFileName(this.number)); @@ -134,54 +330,47 @@ } this.dirty = false; } - - /** Closes the underlying IndexInput for this norm. - * It is still valid to access all other norm properties after close is called. - * @throws IOException - */ - private synchronized void close() throws IOException { - if (in != null && !useSingleNormStream) { - in.close(); - } - in = null; - } } - - /** - * Increments the RC of this reader, as well as - * of all norms this reader is using - */ + public synchronized void incRef() { super.incRef(); Iterator it = norms.values().iterator(); while (it.hasNext()) { - Norm norm = (Norm) it.next(); - norm.incRef(); + ((Norm) it.next()).incRef(); } + if (deletedDocsRef != null) { + deletedDocsRef.incRef(); + } } - /** - * only increments the RC of this reader, not tof - * he norms. This is important whenever a reopen() - * creates a new SegmentReader that doesn't share - * the norms with this one - */ private synchronized void incRefReaderNotNorms() { super.incRef(); } - + public synchronized void decRef() throws IOException { super.decRef(); Iterator it = norms.values().iterator(); while (it.hasNext()) { - Norm norm = (Norm) it.next(); - norm.decRef(); + ((Norm) it.next()).decRef(); } + + if (deletedDocsRef != null) { + deletedDocsRef.decRef(); + } } private synchronized void decRefReaderNotNorms() throws IOException { super.decRef(); } + + private synchronized void decRefDeletedDocs() { + if (deletedDocs != null) { + assert deletedDocsRef != null; + deletedDocsRef.decRef(); + } else { + assert deletedDocsRef == null; + } + } Map norms = new HashMap(); @@ -410,6 +599,7 @@ // NOTE: the bitvector is stored using the regular directory, not cfs if (hasDeletions(si)) { deletedDocs = new BitVector(directory(), si.getDelFileName()); + deletedDocsRef = new Ref(); assert si.getDelCount() == deletedDocs.count() : "delete count mismatch: info=" + si.getDelCount() + " vs BitVector=" + deletedDocs.count(); @@ -423,13 +613,33 @@ assert si.getDelCount() == 0; } - protected synchronized DirectoryIndexReader doReopen(SegmentInfos infos) throws CorruptIndexException, IOException { + /** + * Clones the norm bytes. May be overridden by subclasses. New and experimental. + * @param bv Byte array to clone + * @return New BitVector + */ + protected byte[] cloneNormBytes(byte[] bytes) { + byte[] cloneBytes = new byte[bytes.length]; + System.arraycopy(bytes, 0, cloneBytes, 0, bytes.length); + return cloneBytes; + } + + /** + * Clones the deleteDocs BitVector. May be overridden by subclasses. New and experimental. + * @param bv BitVector to clone + * @return New BitVector + */ + protected BitVector cloneDeletedDocs(BitVector bv) { + return (BitVector)bv.clone(); + } + + protected synchronized DirectoryIndexReader doReopen(SegmentInfos infos, boolean doClone) throws CorruptIndexException, IOException { DirectoryIndexReader newReader; if (infos.size() == 1) { SegmentInfo si = infos.info(0); if (segment.equals(si.name) && si.getUseCompoundFile() == SegmentReader.this.si.getUseCompoundFile()) { - newReader = reopenSegment(si); + newReader = reopenSegment(si, doClone); } else { // segment not referenced anymore, reopen not possible // or segment format changed @@ -437,41 +647,43 @@ } } else { if (readOnly) - return new ReadOnlyMultiSegmentReader(directory, infos, closeDirectory, new SegmentReader[] {this}, null, null); + return new ReadOnlyMultiSegmentReader(directory, infos, closeDirectory, new SegmentReader[] {this}, null, null, doClone); else - return new MultiSegmentReader(directory, infos, closeDirectory, new SegmentReader[] {this}, null, null, false); + return new MultiSegmentReader(directory, infos, closeDirectory, new SegmentReader[] {this}, null, null, false, doClone); } return newReader; } - synchronized SegmentReader reopenSegment(SegmentInfo si) throws CorruptIndexException, IOException { + synchronized SegmentReader reopenSegment(SegmentInfo si, boolean doClone) throws CorruptIndexException, IOException { boolean deletionsUpToDate = (this.si.hasDeletions() == si.hasDeletions()) && (!si.hasDeletions() || this.si.getDelFileName().equals(si.getDelFileName())); boolean normsUpToDate = true; - boolean[] fieldNormsChanged = new boolean[fieldInfos.size()]; - if (normsUpToDate) { - for (int i = 0; i < fieldInfos.size(); i++) { - if (!this.si.getNormFileName(i).equals(si.getNormFileName(i))) { - normsUpToDate = false; - fieldNormsChanged[i] = true; - } + final int fieldCount = fieldInfos.size(); + for (int i = 0; i < fieldCount; i++) { + if (!this.si.getNormFileName(i).equals(si.getNormFileName(i))) { + normsUpToDate = false; + fieldNormsChanged[i] = true; } } - if (normsUpToDate && deletionsUpToDate) { + // if we're cloning we need to run through the reopenSegment logic + if (normsUpToDate && deletionsUpToDate && !doClone) { return this; } - // clone reader SegmentReader clone; - if (readOnly) - clone = new ReadOnlySegmentReader(); - else - clone = new SegmentReader(); + try { + if (readOnly) + clone = (SegmentReader)READONLY_IMPL.newInstance(); + else + clone = (SegmentReader)IMPL.newInstance(); + } catch (Exception e) { + throw new RuntimeException("cannot load SegmentReader class: " + e, e); + } boolean success = false; try { @@ -482,7 +694,7 @@ clone.readBufferSize = readBufferSize; clone.cfsReader = cfsReader; clone.storeCFSReader = storeCFSReader; - + clone.fieldInfos = fieldInfos; clone.tis = tis; clone.freqStream = freqStream; @@ -493,71 +705,67 @@ clone.fieldsReaderOrig = (FieldsReader) fieldsReaderOrig.clone(); } - if (!deletionsUpToDate) { - // load deleted docs - clone.deletedDocs = null; - clone.loadDeletedDocs(); - } else { - clone.deletedDocs = this.deletedDocs; - } + final String fieldsSegment; - clone.norms = new HashMap(); - if (!normsUpToDate) { - // load norms - for (int i = 0; i < fieldNormsChanged.length; i++) { - // copy unchanged norms to the cloned reader and incRef those norms - if (!fieldNormsChanged[i]) { - String curField = fieldInfos.fieldInfo(i).name; - Norm norm = (Norm) this.norms.get(curField); - norm.incRef(); - clone.norms.put(curField, norm); - } + Directory storeDir = directory(); + + if (si.getDocStoreOffset() != -1) { + fieldsSegment = si.getDocStoreSegment(); + if (storeCFSReader != null) { + storeDir = storeCFSReader; } - - clone.openNorms(si.getUseCompoundFile() ? cfsReader : directory(), readBufferSize); } else { - Iterator it = norms.keySet().iterator(); - while (it.hasNext()) { - String field = (String) it.next(); - Norm norm = (Norm) norms.get(field); - norm.incRef(); - clone.norms.put(field, norm); + fieldsSegment = segment; + if (cfsReader != null) { + storeDir = cfsReader; } } - - if (clone.singleNormStream == null) { - for (int i = 0; i < fieldInfos.size(); i++) { - FieldInfo fi = fieldInfos.fieldInfo(i); - if (fi.isIndexed && !fi.omitNorms) { - Directory d = si.getUseCompoundFile() ? cfsReader : directory(); - String fileName = si.getNormFileName(fi.number); - if (si.hasSeparateNorms(fi.number)) { - continue; - } - - if (fileName.endsWith("." + IndexFileNames.NORMS_EXTENSION)) { - clone.singleNormStream = d.openInput(fileName, readBufferSize); - break; - } - } - } - } - + if (deletedDocsRef != null) { + deletedDocsRef.incRef(); + } + if (doClone) { + clone.deletedDocs = deletedDocs; + clone.deletedDocsRef = deletedDocsRef; + } else { + if (!deletionsUpToDate) { + // load deleted docs + clone.deletedDocs = null; + clone.deletedDocsRef = null; + clone.loadDeletedDocs(); + } else { + clone.deletedDocs = deletedDocs; + clone.deletedDocsRef = deletedDocsRef; + } + } + + clone.norms = new HashMap(); + + // Clone norms + for (int i = 0; i < fieldNormsChanged.length; i++) { + + // Clone unchanged norms to the cloned reader + if (doClone || !fieldNormsChanged[i]) { + final String curField = fieldInfos.fieldInfo(i).name; + Norm norm = (Norm) this.norms.get(curField); + clone.norms.put(curField, norm.clone()); + } + } + + // If we are not cloning, then this will open anew + // any norms that have changed: + clone.openNorms(si.getUseCompoundFile() ? cfsReader : directory(), readBufferSize); + success = true; } finally { if (this.referencedSegmentReader != null) { - // this reader shares resources with another SegmentReader, - // so we increment the other readers refCount. We don't - // increment the refCount of the norms because we did - // that already for the shared norms + // This reader shares resources with another SegmentReader, + // so we increment the other reader's refCount. clone.referencedSegmentReader = this.referencedSegmentReader; - referencedSegmentReader.incRefReaderNotNorms(); } else { - // this reader wasn't reopened, so we increment this - // readers refCount + // We are the original SegmentReader clone.referencedSegmentReader = this; - incRefReaderNotNorms(); } + clone.referencedSegmentReader.incRefReaderNotNorms(); if (!success) { // An exception occured during reopen, we have to decRef the norms @@ -570,6 +778,7 @@ } protected void commitChanges() throws IOException { + if (deletedDocsDirty) { // re-write deleted si.advanceDelGen(); @@ -603,35 +812,24 @@ FieldsReader getFieldsReader() { return (FieldsReader) fieldsReaderLocal.get(); } - + protected void doClose() throws IOException { boolean hasReferencedReader = (referencedSegmentReader != null); termVectorsLocal.close(); fieldsReaderLocal.close(); - + if (hasReferencedReader) { referencedSegmentReader.decRefReaderNotNorms(); referencedSegmentReader = null; } - deletedDocs = null; - - // close the single norms stream - if (singleNormStream != null) { - // we can close this stream, even if the norms - // are shared, because every reader has it's own - // singleNormStream - singleNormStream.close(); - singleNormStream = null; - } - if (!hasReferencedReader) { // close everything, nothing is shared anymore with other readers if (tis != null) { tis.close(); } - + if (freqStream != null) freqStream.close(); if (proxStream != null) @@ -676,8 +874,21 @@ } protected void doDelete(int docNum) { - if (deletedDocs == null) + if (deletedDocs == null) { deletedDocs = new BitVector(maxDoc()); + deletedDocsRef = new Ref(); + } + // there is more than 1 SegmentReader with a reference to this + // deletedDocs BitVector so decRef the current deletedDocsRef, + // clone the BitVector, create a new deletedDocsRef + if (deletedDocsRef.refCount() > 1) { + synchronized (deletedDocsRef) { + Ref oldRef = deletedDocsRef; + deletedDocs = cloneDeletedDocs(deletedDocs); + deletedDocsRef = new Ref(); + oldRef.decRef(); + } + } deletedDocsDirty = true; undeleteAll = false; if (!deletedDocs.getAndSet(docNum)) @@ -685,9 +896,16 @@ } protected void doUndeleteAll() { + deletedDocsDirty = false; + undeleteAll = true; + if (deletedDocs != null) { + assert deletedDocsRef != null; + deletedDocsRef.decRef(); deletedDocs = null; - deletedDocsDirty = false; - undeleteAll = true; + deletedDocsRef = null; + } else { + assert deletedDocsRef == null; + } } List files() throws IOException { @@ -836,17 +1054,7 @@ protected synchronized byte[] getNorms(String field) throws IOException { Norm norm = (Norm) norms.get(field); if (norm == null) return null; // not indexed, or norms not stored - synchronized(norm) { - if (norm.bytes == null) { // value not yet read - byte[] bytes = new byte[maxDoc()]; - norms(field, bytes, 0); - norm.bytes = bytes; // cache it - // it's OK to close the underlying IndexInput as we have cached the - // norms and will never read them again. - norm.close(); - } - return norm.bytes; - } + return norm.bytes(); } // returns fake norms if norms aren't available @@ -863,10 +1071,8 @@ if (norm == null) // not an indexed field return; - norm.dirty = true; // mark it dirty normsDirty = true; - - norms(field)[doc] = value; // set the value + norm.copyOnWrite()[doc] = value; // set the value } /** Read norms into a pre-allocated array. */ @@ -879,24 +1085,8 @@ System.arraycopy(fakeNorms(), 0, bytes, offset, maxDoc()); return; } - - synchronized(norm) { - if (norm.bytes != null) { // can copy from cache - System.arraycopy(norm.bytes, 0, bytes, offset, maxDoc()); - return; - } - - // Read from disk. norm.in may be shared across multiple norms and - // should only be used in a synchronized context. - IndexInput normStream; - if (norm.useSingleNormStream) { - normStream = singleNormStream; - } else { - normStream = norm.in; - } - normStream.seek(norm.normSeek); - normStream.readBytes(bytes, offset, maxDoc()); - } + + norm.bytes(bytes, offset, maxDoc()); } @@ -924,8 +1114,11 @@ if (singleNormFile) { normSeek = nextNormSeek; - if (singleNormStream==null) { + if (singleNormStream == null) { singleNormStream = d.openInput(fileName, readBufferSize); + singleNormRef = new Ref(); + } else { + singleNormRef.incRef(); } // All norms in the .nrm file can share a single IndexInput since // they are only used in a synchronized context. @@ -936,13 +1129,12 @@ normInput = d.openInput(fileName); } - norms.put(fi.name, new Norm(normInput, singleNormFile, fi.number, normSeek)); + norms.put(fi.name, new Norm(normInput, fi.number, normSeek)); nextNormSeek += maxDoc; // increment also if some norms are separate } } } - // for testing only boolean normsClosed() { if (singleNormStream != null) { return false; @@ -956,11 +1148,22 @@ } return true; } - + + /* + void normsRefs() { + System.out.println(" refs for " + this); + Iterator it = norms.values().iterator(); + while (it.hasNext()) { + Norm norm = (Norm) it.next(); + System.out.println(" norm field=" + fieldInfos.fieldInfo(norm.number).name + " RC=" + norm.refCount + " this=" + norm + " origNorm=" + norm.origNorm); + } + } + */ + // for testing only boolean normsClosed(String field) { - Norm norm = (Norm) norms.get(field); - return norm.refCount == 0; + Norm norm = (Norm) norms.get(field); + return norm.refCount == 0; } /** Index: src/java/org/apache/lucene/index/MultiReader.java =================================================================== --- src/java/org/apache/lucene/index/MultiReader.java (revision 733039) +++ src/java/org/apache/lucene/index/MultiReader.java (working copy) @@ -33,7 +33,7 @@ * * @version $Id$ */ -public class MultiReader extends IndexReader { +public class MultiReader extends IndexReader implements Cloneable { protected IndexReader[] subReaders; private int[] starts; // 1st docno for each segment private boolean[] decrefOnClose; // remember which subreaders to decRef on close @@ -87,7 +87,7 @@ } starts[subReaders.length] = maxDoc; } - + /** * Tries to reopen the subreaders. *
@@ -107,53 +107,60 @@ * @throws CorruptIndexException if the index is corrupt * @throws IOException if there is a low-level IO error */ - public IndexReader reopen() throws CorruptIndexException, IOException { + public synchronized IndexReader reopen() throws CorruptIndexException, IOException { + return doReopen(false); + } + + /** + * Clones the subreaders. + * (see {@link IndexReader#clone()}). + *
+ *

+ * If subreaders are shared, then the reference count of those + * readers is increased to ensure that the subreaders remain open + * until the last referring reader is closed. + */ + public synchronized Object clone() { + try { + return doReopen(true); + } catch (Exception ex) { + throw new RuntimeException("", ex); + } + } + + /** + * If clone is true then we clone each of the subreaders + * @param doClone + * @return + * @throws CorruptIndexException + * @throws IOException + */ + protected IndexReader doReopen(boolean doClone) throws CorruptIndexException, IOException { ensureOpen(); boolean reopened = false; IndexReader[] newSubReaders = new IndexReader[subReaders.length]; - boolean[] newDecrefOnClose = new boolean[subReaders.length]; boolean success = false; try { for (int i = 0; i < subReaders.length; i++) { - newSubReaders[i] = subReaders[i].reopen(); + if (doClone) + newSubReaders[i] = (IndexReader) subReaders[i].clone(); + else + newSubReaders[i] = subReaders[i].reopen(); // if at least one of the subreaders was updated we remember that // and return a new MultiReader if (newSubReaders[i] != subReaders[i]) { reopened = true; - // this is a new subreader instance, so on close() we don't - // decRef but close it - newDecrefOnClose[i] = false; } } - - if (reopened) { - for (int i = 0; i < subReaders.length; i++) { - if (newSubReaders[i] == subReaders[i]) { - newSubReaders[i].incRef(); - newDecrefOnClose[i] = true; - } - } - - MultiReader mr = new MultiReader(newSubReaders); - mr.decrefOnClose = newDecrefOnClose; - success = true; - return mr; - } else { - success = true; - return this; - } + success = true; } finally { if (!success && reopened) { for (int i = 0; i < newSubReaders.length; i++) { - if (newSubReaders[i] != null) { + if (newSubReaders[i] != subReaders[i]) { try { - if (newDecrefOnClose[i]) { - newSubReaders[i].decRef(); - } else { - newSubReaders[i].close(); - } + newSubReaders[i].close(); } catch (IOException ignore) { // keep going - we want to clean up as much as possible } @@ -161,6 +168,23 @@ } } } + + if (reopened) { + boolean[] newDecrefOnClose = new boolean[subReaders.length]; + for (int i = 0; i < subReaders.length; i++) { + if (newSubReaders[i] == subReaders[i]) { + newSubReaders[i].incRef(); + newDecrefOnClose[i] = true; + } + } + MultiReader mr = new MultiReader(newSubReaders); + mr.decrefOnClose = newDecrefOnClose; + success = true; + return mr; + } else { + success = true; + return this; + } } public TermFreqVector[] getTermFreqVectors(int n) throws IOException { Index: src/java/org/apache/lucene/index/IndexReader.java =================================================================== --- src/java/org/apache/lucene/index/IndexReader.java (revision 733039) +++ src/java/org/apache/lucene/index/IndexReader.java (working copy) @@ -67,7 +67,7 @@ @version $Id$ */ -public abstract class IndexReader { +public abstract class IndexReader implements Cloneable { // NOTE: in 3.0 this will change to true final static boolean READ_ONLY_DEFAULT = false; @@ -328,11 +328,6 @@ * If the index has not changed since this instance was (re)opened, then this * call is a NOOP and returns this instance. Otherwise, a new instance is * returned. The old instance is not closed and remains usable.
- * Note: The re-opened reader instance and the old instance might share - * the same resources. For this reason no index modification operations - * (e. g. {@link #deleteDocument(int)}, {@link #setNorm(int, String, byte)}) - * should be performed using one of the readers until the old reader instance - * is closed. Otherwise, the behavior of the readers is undefined. *

* You can determine whether a reader was actually reopened by comparing the * old instance with the instance returned by this method: @@ -354,6 +349,23 @@ public synchronized IndexReader reopen() throws CorruptIndexException, IOException { throw new UnsupportedOperationException("This reader does not support reopen()."); } + + /** + * Clones the IndexReader. + *

+ * On cloning a reader with pending modifications, the original reader then + * becomes readOnly (in addition to passing the write lock to the + * cloned reader). The deletedDocs and norms are cloned on update to the + * cloned IndexReader constituting a "copy on write" (rather than in the clone + * method to avoid excessive duplication). + *

+ * The originator of the clone (this) will throw a LockObtainFailedException if + * {@link #deleteDocument(int)}, {@link #setNorm(int, String, byte)} are called + * after it is cloned. + */ + public synchronized Object clone() { + throw new UnsupportedOperationException("This reader does not support clone()."); + } /** * Returns the directory associated with this index. The Default @@ -970,7 +982,7 @@ * No other methods should be called after this has been called. * @throws IOException if there is a low-level IO error */ - public final synchronized void close() throws IOException { + public synchronized void close() throws IOException { if (!closed) { decRef(); closed = true; Index: src/java/org/apache/lucene/index/DocumentsWriter.java =================================================================== --- src/java/org/apache/lucene/index/DocumentsWriter.java (revision 733039) +++ src/java/org/apache/lucene/index/DocumentsWriter.java (working copy) @@ -919,9 +919,13 @@ if (reader != null) { try { if (success) - reader.doCommit(); + reader.commit(); } finally { - reader.doClose(); + // Force reader to not have changes; if we hit + // an exception during commit, we don't want + // close to retry the commit: + reader.hasChanges = false; + reader.close(); } } } Index: src/java/org/apache/lucene/index/MultiSegmentReader.java =================================================================== --- src/java/org/apache/lucene/index/MultiSegmentReader.java (revision 733039) +++ src/java/org/apache/lucene/index/MultiSegmentReader.java (working copy) @@ -33,7 +33,7 @@ /** * An IndexReader which reads indexes with multiple segments. */ -class MultiSegmentReader extends DirectoryIndexReader { +class MultiSegmentReader extends DirectoryIndexReader implements Cloneable { protected SegmentReader[] subReaders; private int[] starts; // 1st docno for each segment private Map normsCache = new HashMap(); @@ -71,7 +71,7 @@ } /** This contructor is only used for {@link #reopen()} */ - MultiSegmentReader(Directory directory, SegmentInfos infos, boolean closeDirectory, SegmentReader[] oldReaders, int[] oldStarts, Map oldNormsCache, boolean readOnly) throws IOException { + MultiSegmentReader(Directory directory, SegmentInfos infos, boolean closeDirectory, SegmentReader[] oldReaders, int[] oldStarts, Map oldNormsCache, boolean readOnly, boolean doClone) throws IOException { super(directory, infos, closeDirectory, readOnly); // we put the old SegmentReaders in a map, that allows us @@ -109,7 +109,7 @@ // this is a new reader; in case we hit an exception we can close it safely newReader = SegmentReader.get(readOnly, infos.info(i)); } else { - newReader = (SegmentReader) newReaders[i].reopenSegment(infos.info(i)); + newReader = (SegmentReader) newReaders[i].reopenSegment(infos.info(i), doClone); } if (newReader == newReaders[i]) { // this reader will be shared between the old and the new one, @@ -181,7 +181,7 @@ } } } - + private void initialize(SegmentReader[] subReaders) { this.subReaders = subReaders; starts = new int[subReaders.length + 1]; // build starts array @@ -195,15 +195,15 @@ starts[subReaders.length] = maxDoc; } - protected synchronized DirectoryIndexReader doReopen(SegmentInfos infos) throws CorruptIndexException, IOException { + protected synchronized DirectoryIndexReader doReopen(SegmentInfos infos, boolean doClone) throws CorruptIndexException, IOException { if (infos.size() == 1) { // The index has only one segment now, so we can't refresh the MultiSegmentReader. // Return a new [ReadOnly]SegmentReader instead return SegmentReader.get(readOnly, infos, infos.info(0), false); } else if (readOnly) { - return new ReadOnlyMultiSegmentReader(directory, infos, closeDirectory, subReaders, starts, normsCache); + return new ReadOnlyMultiSegmentReader(directory, infos, closeDirectory, subReaders, starts, normsCache, doClone); } else { - return new MultiSegmentReader(directory, infos, closeDirectory, subReaders, starts, normsCache, false); + return new MultiSegmentReader(directory, infos, closeDirectory, subReaders, starts, normsCache, false, doClone); } } Index: src/java/org/apache/lucene/index/ReadOnlyMultiSegmentReader.java =================================================================== --- src/java/org/apache/lucene/index/ReadOnlyMultiSegmentReader.java (revision 733039) +++ src/java/org/apache/lucene/index/ReadOnlyMultiSegmentReader.java (working copy) @@ -27,8 +27,8 @@ super(directory, sis, closeDirectory, true); } - ReadOnlyMultiSegmentReader(Directory directory, SegmentInfos infos, boolean closeDirectory, SegmentReader[] oldReaders, int[] oldStarts, Map oldNormsCache) throws IOException { - super(directory, infos, closeDirectory, oldReaders, oldStarts, oldNormsCache, true); + ReadOnlyMultiSegmentReader(Directory directory, SegmentInfos infos, boolean closeDirectory, SegmentReader[] oldReaders, int[] oldStarts, Map oldNormsCache, boolean doClone) throws IOException { + super(directory, infos, closeDirectory, oldReaders, oldStarts, oldNormsCache, true, doClone); } protected void acquireWriteLock() { Index: src/java/org/apache/lucene/util/BitVector.java =================================================================== --- src/java/org/apache/lucene/util/BitVector.java (revision 733039) +++ src/java/org/apache/lucene/util/BitVector.java (working copy) @@ -35,7 +35,7 @@ @version $Id$ */ -public final class BitVector { +public final class BitVector implements Cloneable { private byte[] bits; private int size; @@ -46,7 +46,19 @@ size = n; bits = new byte[(size >> 3) + 1]; } - + + BitVector(byte[] bits, int size, int count) { + this.bits = bits; + this.size = size; + this.count = count; + } + + public Object clone() { + byte[] copyBits = new byte[bits.length]; + System.arraycopy(bits, 0, copyBits, 0, bits.length); + return new BitVector(copyBits, size, count); + } + /** Sets the value of bit to one. */ public final void set(int bit) { if (bit >= size) {