/* * 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. */ package org.apache.lucene.index; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import org.apache.lucene.index.DocumentsWriterDeleteQueue.DeleteSlice; import org.apache.lucene.index.PrefixCodedTerms.TermIterator; import org.apache.lucene.search.TermQuery; import org.apache.lucene.util.BytesRefBuilder; import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.ThreadInterruptedException; /** * Unit test for {@link DocumentsWriterDeleteQueue} */ public class TestDocumentsWriterDeleteQueue extends LuceneTestCase { public void testUpdateDelteSlices() { DocumentsWriterDeleteQueue queue = new DocumentsWriterDeleteQueue(); final int size = 200 + random().nextInt(500) * RANDOM_MULTIPLIER; Integer[] ids = new Integer[size]; for (int i = 0; i < ids.length; i++) { ids[i] = random().nextInt(); } DeleteSlice slice1 = queue.newSlice(); DeleteSlice slice2 = queue.newSlice(); BufferedUpdates bd1 = new BufferedUpdates("bd1"); BufferedUpdates bd2 = new BufferedUpdates("bd2"); int last1 = 0; int last2 = 0; Set<Term> uniqueValues = new HashSet<>(); for (int j = 0; j < ids.length; j++) { Integer i = ids[j]; // create an array here since we compare identity below against tailItem Term[] term = new Term[] {new Term("id", i.toString())}; uniqueValues.add(term[0]); queue.addDelete(term); if (random().nextInt(20) == 0 || j == ids.length - 1) { queue.updateSlice(slice1); assertTrue(slice1.isTailItem(term)); slice1.apply(bd1, j); assertAllBetween(last1, j, bd1, ids); last1 = j + 1; } if (random().nextInt(10) == 5 || j == ids.length - 1) { queue.updateSlice(slice2); assertTrue(slice2.isTailItem(term)); slice2.apply(bd2, j); assertAllBetween(last2, j, bd2, ids); last2 = j + 1; } assertEquals(j+1, queue.numGlobalTermDeletes()); } assertEquals(uniqueValues, bd1.terms.keySet()); assertEquals(uniqueValues, bd2.terms.keySet()); HashSet<Term> frozenSet = new HashSet<>(); BytesRefBuilder bytesRef = new BytesRefBuilder(); TermIterator iter = queue.freezeGlobalBuffer(null).termIterator(); while (iter.next() != null) { bytesRef.copyBytes(iter.bytes); frozenSet.add(new Term(iter.field(), bytesRef.toBytesRef())); } assertEquals(uniqueValues, frozenSet); assertEquals("num deletes must be 0 after freeze", 0, queue .numGlobalTermDeletes()); } private void assertAllBetween(int start, int end, BufferedUpdates deletes, Integer[] ids) { for (int i = start; i <= end; i++) { assertEquals(Integer.valueOf(end), deletes.terms.get(new Term("id", ids[i].toString()))); } } public void testClear() { DocumentsWriterDeleteQueue queue = new DocumentsWriterDeleteQueue(); assertFalse(queue.anyChanges()); queue.clear(); assertFalse(queue.anyChanges()); final int size = 200 + random().nextInt(500) * RANDOM_MULTIPLIER; for (int i = 0; i < size; i++) { Term term = new Term("id", "" + i); if (random().nextInt(10) == 0) { queue.addDelete(new TermQuery(term)); } else { queue.addDelete(term); } assertTrue(queue.anyChanges()); if (random().nextInt(10) == 0) { queue.clear(); queue.tryApplyGlobalSlice(); assertFalse(queue.anyChanges()); } } } public void testAnyChanges() { DocumentsWriterDeleteQueue queue = new DocumentsWriterDeleteQueue(); final int size = 200 + random().nextInt(500) * RANDOM_MULTIPLIER; int termsSinceFreeze = 0; int queriesSinceFreeze = 0; for (int i = 0; i < size; i++) { Term term = new Term("id", "" + i); if (random().nextInt(10) == 0) { queue.addDelete(new TermQuery(term)); queriesSinceFreeze++; } else { queue.addDelete(term); termsSinceFreeze++; } assertTrue(queue.anyChanges()); if (random().nextInt(5) == 0) { FrozenBufferedUpdates freezeGlobalBuffer = queue.freezeGlobalBuffer(null); assertEquals(termsSinceFreeze, freezeGlobalBuffer.terms.size()); assertEquals(queriesSinceFreeze, freezeGlobalBuffer.queries.length); queriesSinceFreeze = 0; termsSinceFreeze = 0; assertFalse(queue.anyChanges()); } } } public void testPartiallyAppliedGlobalSlice() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, InterruptedException { final DocumentsWriterDeleteQueue queue = new DocumentsWriterDeleteQueue(); ReentrantLock lock = queue.globalBufferLock; lock.lock(); Thread t = new Thread() { @Override public void run() { queue.addDelete(new Term("foo", "bar")); } }; t.start(); t.join(); lock.unlock(); assertTrue("changes in del queue but not in slice yet", queue.anyChanges()); queue.tryApplyGlobalSlice(); assertTrue("changes in global buffer", queue.anyChanges()); FrozenBufferedUpdates freezeGlobalBuffer = queue.freezeGlobalBuffer(null); assertTrue(freezeGlobalBuffer.any()); assertEquals(1, freezeGlobalBuffer.terms.size()); assertFalse("all changes applied", queue.anyChanges()); } public void testStressDeleteQueue() throws InterruptedException { DocumentsWriterDeleteQueue queue = new DocumentsWriterDeleteQueue(); Set<Term> uniqueValues = new HashSet<>(); final int size = 10000 + random().nextInt(500) * RANDOM_MULTIPLIER; Integer[] ids = new Integer[size]; for (int i = 0; i < ids.length; i++) { ids[i] = random().nextInt(); uniqueValues.add(new Term("id", ids[i].toString())); } CountDownLatch latch = new CountDownLatch(1); AtomicInteger index = new AtomicInteger(0); final int numThreads = 2 + random().nextInt(5); UpdateThread[] threads = new UpdateThread[numThreads]; for (int i = 0; i < threads.length; i++) { threads[i] = new UpdateThread(queue, index, ids, latch); threads[i].start(); } latch.countDown(); for (int i = 0; i < threads.length; i++) { threads[i].join(); } for (UpdateThread updateThread : threads) { DeleteSlice slice = updateThread.slice; queue.updateSlice(slice); BufferedUpdates deletes = updateThread.deletes; slice.apply(deletes, BufferedUpdates.MAX_INT); assertEquals(uniqueValues, deletes.terms.keySet()); } queue.tryApplyGlobalSlice(); Set<Term> frozenSet = new HashSet<>(); BytesRefBuilder builder = new BytesRefBuilder(); TermIterator iter = queue.freezeGlobalBuffer(null).termIterator(); while (iter.next() != null) { builder.copyBytes(iter.bytes); frozenSet.add(new Term(iter.field(), builder.toBytesRef())); } assertEquals("num deletes must be 0 after freeze", 0, queue .numGlobalTermDeletes()); assertEquals(uniqueValues.size(), frozenSet.size()); assertEquals(uniqueValues, frozenSet); } private static class UpdateThread extends Thread { final DocumentsWriterDeleteQueue queue; final AtomicInteger index; final Integer[] ids; final DeleteSlice slice; final BufferedUpdates deletes; final CountDownLatch latch; protected UpdateThread(DocumentsWriterDeleteQueue queue, AtomicInteger index, Integer[] ids, CountDownLatch latch) { this.queue = queue; this.index = index; this.ids = ids; this.slice = queue.newSlice(); deletes = new BufferedUpdates("deletes"); this.latch = latch; } @Override public void run() { try { latch.await(); } catch (InterruptedException e) { throw new ThreadInterruptedException(e); } int i = 0; while ((i = index.getAndIncrement()) < ids.length) { Term term = new Term("id", ids[i].toString()); queue.add(term, slice); assertTrue(slice.isTailItem(term)); slice.apply(deletes, BufferedUpdates.MAX_INT); } } } }