Author: catholicon
Date: Fri Aug 2 05:24:31 2019
New Revision: 1864197
URL: http://svn.apache.org/viewvc?rev=1864197&view=rev
Log:
OAK-8513: Concurrent index access via CopyOnRead directory can lead to reading
directly off of remote
Modified:
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexCopier.java
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/CopyOnReadDirectory.java
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/LocalIndexFile.java
jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/ConcurrentCopyOnReadDirectoryTest.java
Modified:
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexCopier.java
URL:
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexCopier.java?rev=1864197&r1=1864196&r2=1864197&view=diff
==============================================================================
---
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexCopier.java
(original)
+++
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexCopier.java
Fri Aug 2 05:24:31 2019
@@ -28,6 +28,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -43,6 +44,7 @@ import javax.management.openmbean.Tabula
import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Monitor;
import org.apache.commons.io.FileUtils;
import
org.apache.jackrabbit.oak.plugins.index.lucene.directory.CopyOnReadDirectory;
import
org.apache.jackrabbit.oak.plugins.index.lucene.directory.CopyOnWriteDirectory;
@@ -98,6 +100,7 @@ public class IndexCopier implements Copy
private final AtomicLong downloadTime = new AtomicLong();
private final AtomicLong uploadTime = new AtomicLong();
+ private final Monitor copyCompletionMonitor = new Monitor();
private final Map<String, String> indexPathVersionMapping =
newConcurrentMap();
private final ConcurrentMap<String, LocalIndexFile> failedToDeleteFiles =
newConcurrentMap();
@@ -356,8 +359,61 @@ public class IndexCopier implements Copy
return copyInProgressFiles.contains(file);
}
+ /**
+ * Waits for maximum of {@code timeoutMillis} while checking if {@code
file} isn't being copied already.
+ * The method can return before {@code timeoutMillis} if it got
interrupted. So, if the method reports false,
+ * caller should apply its own logic to see if a wait should be repeated
or not.
+ * @param file
+ * @param timeoutMillis
+ */
+ public void waitForCopyCompletion(LocalIndexFile file, long timeoutMillis)
{
+ final Monitor.Guard notCopyingGuard = new
Monitor.Guard(copyCompletionMonitor) {
+ @Override
+ public boolean isSatisfied() {
+ return !isCopyInProgress(file);
+ }
+ };
+ long localLength = file.actualSize();
+ long lastLocalLength = localLength;
+
+ boolean notCopying = !isCopyInProgress(file);
+ while (!notCopying) {
+ try {
+ if (log.isDebugEnabled()) {
+ log.debug("Checking for copy completion of {} - {}",
file.getKey(), file.copyLog());
+ }
+ notCopying = copyCompletionMonitor.enterWhen(notCopyingGuard,
timeoutMillis, TimeUnit.MILLISECONDS);
+ if (notCopying) {
+ copyCompletionMonitor.leave();
+ }
+ } catch (InterruptedException e) {
+ // ignore
+ }
+
+ localLength = file.actualSize();
+
+ // Break out if local file length hasn't changed since last we
checked.
+ // Do note that our assumption is that our monitor would return
false only on timeout.
+ // BUT that's not true and the monitor could be interrupted as
well. We are ignoring that
+ // gotcha explicitly as we don't really want to over complicate
here for a rare race of
+ // concurrent copying and avoid reading from remote.
+ if (localLength <= lastLocalLength) {
+ log.warn("Breaking out of waiting for copy to finish as
current local length ({})" +
+ " hasn't increased from {}",
+ localLength, lastLocalLength);
+ break;
+ }
+ lastLocalLength = localLength;
+ }
+ }
+
public void doneCopy(LocalIndexFile file, long start) {
- copyInProgressFiles.remove(file);
+ copyCompletionMonitor.enter();
+ try {
+ copyInProgressFiles.remove(file);
+ } finally {
+ copyCompletionMonitor.leave();
+ }
copyInProgressCount.decrementAndGet();
copyInProgressSize.addAndGet(-file.getSize());
Modified:
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/CopyOnReadDirectory.java
URL:
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/CopyOnReadDirectory.java?rev=1864197&r1=1864196&r2=1864197&view=diff
==============================================================================
---
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/CopyOnReadDirectory.java
(original)
+++
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/CopyOnReadDirectory.java
Fri Aug 2 05:24:31 2019
@@ -67,6 +67,11 @@ public class CopyOnReadDirectory extends
private final Executor executor;
private final AtomicBoolean closed = new AtomicBoolean();
+ // exported as package private to be useful in tests
+ static final String WAIT_OTHER_COPY_SYSPROP_NAME = "cor.waitCopyMillis";
+
+ long waitOtherCopyTimeoutMillis =
Long.getLong(WAIT_OTHER_COPY_SYSPROP_NAME, TimeUnit.SECONDS.toMillis(30));
+
private final ConcurrentMap<String, CORFileReference> files =
newConcurrentMap();
public CopyOnReadDirectory(IndexCopier indexCopier, Directory remote,
Directory local, boolean prefetch,
@@ -212,19 +217,24 @@ public class CopyOnReadDirectory extends
name, humanReadableByteCount(fileSize));
}
} else {
- long localLength = local.fileLength(name);
long remoteLength = remote.fileLength(name);
+ LocalIndexFile file = new LocalIndexFile(local, name,
remoteLength, true);
+ // as a local file exists, attempt a wait for completion of
any potential ongoing concurrent copy
+ indexCopier.waitForCopyCompletion(file,
waitOtherCopyTimeoutMillis);
+
+ long localLength = local.fileLength(name);
+
//Do a simple consistency check. Ideally Lucene index files
are never
//updated but still do a check if the copy is consistent
if (localLength != remoteLength) {
- LocalIndexFile file = new LocalIndexFile(local, name,
remoteLength, true);
if (!indexCopier.isCopyInProgress(file)) {
log.warn("[{}] Found local copy for {} in {} but size
of local {} differs from remote {}. " +
"Content would be read from remote
file only",
indexPath, name, local, localLength,
remoteLength);
indexCopier.foundInvalidFile();
} else {
+
logRemoteAccess("[{}] Found in progress copy of file
{}. Would read from remote", indexPath, name);
}
} else {
Modified:
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/LocalIndexFile.java
URL:
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/LocalIndexFile.java?rev=1864197&r1=1864196&r2=1864197&view=diff
==============================================================================
---
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/LocalIndexFile.java
(original)
+++
jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/LocalIndexFile.java
Fri Aug 2 05:24:31 2019
@@ -111,7 +111,7 @@ public final class LocalIndexFile {
return actualSize() * 1.0f / size * 100;
}
- private long actualSize(){
+ public long actualSize(){
return dir != null ? new File(dir, name).length() : 0;
}
Modified:
jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/ConcurrentCopyOnReadDirectoryTest.java
URL:
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/ConcurrentCopyOnReadDirectoryTest.java?rev=1864197&r1=1864196&r2=1864197&view=diff
==============================================================================
---
jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/ConcurrentCopyOnReadDirectoryTest.java
(original)
+++
jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/directory/ConcurrentCopyOnReadDirectoryTest.java
Fri Aug 2 05:24:31 2019
@@ -17,45 +17,79 @@
package org.apache.jackrabbit.oak.plugins.index.lucene.directory;
-import com.google.common.io.Closer;
+import com.google.common.collect.Iterables;
+import org.apache.commons.compress.utils.Lists;
import org.apache.jackrabbit.oak.InitialContentHelper;
import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.commons.junit.TemporarySystemProperty;
import org.apache.jackrabbit.oak.plugins.index.lucene.IndexCopier;
import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexDefinition;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.lucene.store.*;
-import org.junit.*;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.util.concurrent.*;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import static
com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor;
+import static
org.apache.jackrabbit.oak.plugins.index.lucene.directory.CopyOnReadDirectory.WAIT_OTHER_COPY_SYSPROP_NAME;
import static
org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.INDEX_DATA_CHILD_NAME;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
public class ConcurrentCopyOnReadDirectoryTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder(new
File("target"));
- private final Closer closer = Closer.create();
- private ExecutorService executorService = Executors.newFixedThreadPool(2);
+ @Rule
+ public TemporarySystemProperty tempSysProp = new TemporarySystemProperty();
+
+ private ExecutorService executorService = null;
private Directory remote;
+ private IndexCopier copier;
+
+ private Directory firstCoR = null;
+ private List<Future<String>> leechingCoRFutures = Lists.newArrayList();
+ private List<Directory> leechingCoRs = Lists.newArrayList();
+
+ private CountDownLatch firstCoRBlocker;
+ private Future<String> firstCoRFutre;
+ private LuceneIndexDefinition defn;
+
+ private static final String REMOTE_INPUT_PREFIX = "Remote - ";
@Before
public void setup() throws Exception {
- executorService = Executors.newFixedThreadPool(2);
- closer.register(new ExecutorCloser(executorService, 1,
TimeUnit.SECONDS));
+ System.setProperty(WAIT_OTHER_COPY_SYSPROP_NAME,
String.valueOf(TimeUnit.MILLISECONDS.toMillis(30)));
+ executorService = Executors.newFixedThreadPool(5);
// normal remote directory
remote = new RAMDirectory() {
@Override
public IndexInput openInput(String name, IOContext context) throws
IOException {
- return new RemoteIndexInput(super.openInput(name, context));
+ IndexInput ret = spy(super.openInput(name, context));
+ when(ret.toString())
+ .thenAnswer(invocationOnMock -> REMOTE_INPUT_PREFIX +
invocationOnMock.callRealMethod());
+ return ret;
}
};
IndexOutput output = remote.createOutput("file", IOContext.DEFAULT);
@@ -64,86 +98,86 @@ public class ConcurrentCopyOnReadDirecto
IndexInput remoteInput = remote.openInput("file", IOContext.READ);
assertTrue(remoteInput.length() > 1);
+
+ copier = new IndexCopier(sameThreadExecutor(),
temporaryFolder.newFolder(), true);
+
+ NodeState root = InitialContentHelper.INITIAL_CONTENT;
+ defn = new LuceneIndexDefinition(root, root, "/foo");
}
@After
- public void tearDown() throws Exception {
- closer.close();
+ public void tearDown() {
+ // This is no-op usually but would save us in case first CoR is stuck
in wait
+ firstCoRBlocker.countDown();
+
+ if (executorService != null) {
+ new ExecutorCloser(executorService, 1, TimeUnit.SECONDS).close();
+ }
}
- @Ignore("OAK-8513")
@Test
public void concurrentPrefetch() throws Exception {
- // create filtering remote that would block on opening an input
- CountDownLatch copyWaiter = new CountDownLatch(1);
- CountDownLatch copyBlocker = new CountDownLatch(1);
- Directory blockingRemote = new BlockingInputDirectory(copyWaiter,
copyBlocker, remote);
-
- IndexCopier copier = new IndexCopier(sameThreadExecutor(),
temporaryFolder.newFolder(), true);
-
- NodeState root = InitialContentHelper.INITIAL_CONTENT;
- final LuceneIndexDefinition defn = new LuceneIndexDefinition(root,
root, "/foo");
-
- // create a CoR instance to start pre-fetching in a separate thread as
we'd be blocking it
- Future<String> concCoR = executorService.submit(() -> {
- try {
- openCoR(copier, blockingRemote, defn);
- return null;
- } catch (Throwable t) {
- return getThrowableAsString(t);
- }
- });
+ // setup one primary CoR and 2 subsequent ones to read. Each would run
concurrently.
+ setupCopiers(2);
+ // let of go of CoR1 to finish its work
+ firstCoRBlocker.countDown();
+
+ assertNull("First CoR must not throw exception", firstCoRFutre.get());
+
+ waitForLeechingCoRsToFinish();
+
+ for (Directory d : Iterables.concat(Collections.singleton(firstCoR),
leechingCoRs)) {
+ IndexInput input = d.openInput("file", IOContext.READ);
+ assertFalse(d + " must not be reading from remote",
+ input.toString().startsWith(REMOTE_INPUT_PREFIX));
+ }
+ }
- try {
- // wait for CoR to start fetching which we're blocking its
completion via copyBlocker latch
- copyWaiter.await();
+ @Test
+ public void concurrentPrefetchWithTimeout() throws Exception {
+ // setup one primary CoR and 2 subsequent ones to read. Each would run
concurrently.
+ setupCopiers(2);
- // get another directory instance with normal remote while the
previous is blocked by us
- Directory dir = openCoR(copier, remote, defn);
- IndexInput input = dir.openInput("file", IOContext.READ);
+ // don't unblock firstCor so that leeching CoRs time out
+ waitForLeechingCoRsToFinish();
- copyBlocker.countDown();
+ // let it go now as leeching CoRs have finished
+ firstCoRBlocker.countDown();
- String futureException = concCoR.get();
+ assertNull("First CoR must not throw exception", firstCoRFutre.get());
- assertNull("Concurrent CoR must not throw exception",
futureException);
+ IndexInput input = firstCoR.openInput("file", IOContext.READ);
+ assertFalse(firstCoR + " must not be reading from remote",
+ input.toString().startsWith(REMOTE_INPUT_PREFIX));
- assertFalse("Must not be reading from remote", input instanceof
RemoteIndexInput);
- } finally {
- copyBlocker.countDown();
+ for (Directory d : leechingCoRs) {
+ input = d.openInput("file", IOContext.READ);
+ assertTrue(d + " must be reading from remote",
+ input.toString().startsWith(REMOTE_INPUT_PREFIX));
}
}
- private static Directory openCoR(IndexCopier copier, Directory remote,
LuceneIndexDefinition defn) throws IOException {
- return copier.wrapForRead("/oak:index/foo", defn, remote,
INDEX_DATA_CHILD_NAME);
- }
+ private void setupCopiers(int numLeechers) throws Exception {
+ // 1 thread each for leeching copier and another one for the first one
+ executorService = Executors.newFixedThreadPool(numLeechers + 1);
- private static String getThrowableAsString(Throwable t) {
- StringBuilder sb = new StringBuilder(t.getMessage() + "\n");
- StringWriter sw = new StringWriter();
- t.printStackTrace(new PrintWriter(sw));
- sb.append(sw.getBuffer());
- return sb.toString();
+ setupFirstCoR();
+ setupLeechingCoRs(numLeechers);
}
- static class BlockingInputDirectory extends FilterDirectory {
- private final CountDownLatch copyWaiter;
- private final CountDownLatch copyBlocker;
-
- BlockingInputDirectory(CountDownLatch copyWaiter, CountDownLatch
copyBlocker, Directory in) {
- super(in);
- this.copyWaiter = copyWaiter;
- this.copyBlocker = copyBlocker;
- }
+ private void setupFirstCoR() throws Exception {
+ firstCoRBlocker = new CountDownLatch(1);
+ CountDownLatch firstCoRWaiter = new CountDownLatch(1);
- @Override
- public IndexInput openInput(String name, IOContext context) throws
IOException {
+ // Create a blocking remote for CoR1 to signal how open input
progresses
+ Directory blockingRemote = spy(remote);
+ doAnswer(invocationOnMock -> {
IndexInput input;
try {
- input = super.openInput(name, context);
+ input = (IndexInput) invocationOnMock.callRealMethod();
} finally {
// signal that input has been opened
- copyWaiter.countDown();
+ firstCoRWaiter.countDown();
}
// wait while we're signalled that we can be done with opening
input
@@ -151,7 +185,7 @@ public class ConcurrentCopyOnReadDirecto
while (wait) {
try {
// block until we are signalled to call super
- copyBlocker.await();
+ firstCoRBlocker.await();
wait = false;
} catch (InterruptedException e) {
// ignore
@@ -159,45 +193,75 @@ public class ConcurrentCopyOnReadDirecto
}
return input;
- }
+ }).when(blockingRemote).openInput(any(), any());
+
+ // create CoR instance to start pre-fetching in a separate thread as
we want to block it mid-way
+ firstCoRFutre = executorService.submit(() -> {
+ try {
+ String description = "firstCoR";
+ Thread.currentThread().setName(description);
+ firstCoR = openCoR(copier, blockingRemote, defn, description);
+ return null;
+ } catch (Throwable t) {
+ return getThrowableAsString(t);
+ }
+ });
+
+ // wait for CoR to start fetching which we're blocking its completion
via cor1Blocker latch
+ firstCoRWaiter.await();
}
- static class RemoteIndexInput extends IndexInput {
- private final IndexInput delegate;
+ private void setupLeechingCoRs(int numLeechers) throws Exception {
+ CountDownLatch leechingCoRsWaiter = new CountDownLatch(numLeechers);
+ // Create a blocking copier for leeching CoRs to signal when it starts
to wait for it to wait for copy completion
+ IndexCopier blockingCopier = spy(copier);
+ doAnswer(invocationOnMock -> {
+ leechingCoRsWaiter.countDown();
+ return invocationOnMock.callRealMethod();
+ }).when(blockingCopier).isCopyInProgress(any());
- RemoteIndexInput(IndexInput delegate) {
- super("Remote " + delegate.toString());
- this.delegate = delegate;
+ for (int i = 0; i < numLeechers; i++) {
+ final String leecherName = "CoR-" + (i + 1);
+ leechingCoRFutures.add(executorService.submit(() ->
createLeechingCoR(blockingCopier, defn, leecherName)));
}
- @Override
- public void close() throws IOException {
- delegate.close();
- }
+ // wait for leeching CoRs to start
+ leechingCoRsWaiter.await();
+ }
- @Override
- public void seek(long pos) throws IOException {
- delegate.seek(pos);
- }
+ private String createLeechingCoR(IndexCopier blockingCopier,
LuceneIndexDefinition defn, String threadName) {
+ Thread.currentThread().setName(threadName);
- @Override
- public long length() {
- return delegate.length();
- }
+ // get another directory instance with normal remote while the
previous is blocked by us
+ try {
+ CopyOnReadDirectory dir = (CopyOnReadDirectory)
openCoR(blockingCopier, remote, defn, threadName);
+ leechingCoRs.add(dir);
- @Override
- public long getFilePointer() {
- return delegate.getFilePointer();
+ return null;
+ } catch (Throwable t) {
+ return getThrowableAsString(t);
}
+ }
- @Override
- public byte readByte() throws IOException {
- return delegate.readByte();
+ private void waitForLeechingCoRsToFinish() throws Exception {
+ for (Future<String> corFuture : leechingCoRFutures) {
+ assertNull("Leeching CoR must not throw exception",
corFuture.get());
}
+ }
- @Override
- public void readBytes(byte[] b, int offset, int len) throws
IOException {
- delegate.readBytes(b, offset, len);
- }
+ private static Directory openCoR(IndexCopier copier, Directory remote,
LuceneIndexDefinition defn,
+ String description) throws IOException {
+ Directory d = spy(copier.wrapForRead("/oak:index/foo", defn, remote,
INDEX_DATA_CHILD_NAME));
+ when(d.toString())
+ .thenAnswer(invocationOnMock -> description);
+ return d;
+ }
+
+ private static String getThrowableAsString(Throwable t) {
+ StringBuilder sb = new StringBuilder(t.getMessage() + "\n");
+ StringWriter sw = new StringWriter();
+ t.printStackTrace(new PrintWriter(sw));
+ sb.append(sw.getBuffer());
+ return sb.toString();
}
}
\ No newline at end of file