Index: CHANGES.txt =================================================================== --- CHANGES.txt (revision 452205) +++ CHANGES.txt (working copy) @@ -31,6 +31,10 @@ New Fieldable interface for use with the lazy field loading mechanism. (Grant Ingersoll and Chuck Williams via Grant Ingersoll) + 3. LUCENE-678: Added NativeFSLockFactory, which implements locking + using OS native locking (via java.nio.*). + + API Changes 1. LUCENE-438: Remove "final" from Token, implement Cloneable, allow Index: src/test/org/apache/lucene/store/TestLockFactory.java =================================================================== --- src/test/org/apache/lucene/store/TestLockFactory.java (revision 452205) +++ src/test/org/apache/lucene/store/TestLockFactory.java (working copy) @@ -130,7 +130,8 @@ IndexWriter writer = new IndexWriter(indexDirName, new WhitespaceAnalyzer(), true); assertTrue("FSDirectory did not use correct LockFactory: got " + writer.getDirectory().getLockFactory(), - SimpleFSLockFactory.class.isInstance(writer.getDirectory().getLockFactory())); + SimpleFSLockFactory.class.isInstance(writer.getDirectory().getLockFactory()) || + NativeFSLockFactory.class.isInstance(writer.getDirectory().getLockFactory())); IndexWriter writer2 = null; @@ -157,8 +158,11 @@ IndexWriter writer = new IndexWriter(indexDirName, new WhitespaceAnalyzer(), true); assertTrue("FSDirectory did not use correct LockFactory: got " + writer.getDirectory().getLockFactory(), - SimpleFSLockFactory.class.isInstance(writer.getDirectory().getLockFactory())); + SimpleFSLockFactory.class.isInstance(writer.getDirectory().getLockFactory()) || + NativeFSLockFactory.class.isInstance(writer.getDirectory().getLockFactory())); + writer.close(); + // Create a 2nd IndexWriter. This should not fail: IndexWriter writer2 = null; try { @@ -218,10 +222,6 @@ fail("Should not have hit an IOException with locking disabled"); } - // Put back to the correct default for subsequent tests: - System.setProperty("org.apache.lucene.store.FSDirectoryLockFactoryClass", - "org.apache.lucene.store.SimpleFSLockFactory"); - FSDirectory.setDisableLocks(false); writer.close(); if (writer2 != null) { @@ -266,9 +266,19 @@ // IndexWriters over & over in 2 threads and making sure // no unexpected exceptions are raised: public void testStressLocks() throws IOException { + _testStressLocks(null, "index.TestLockFactory6"); + } - String indexDirName = "index.TestLockFactory6"; - FSDirectory fs1 = FSDirectory.getDirectory(indexDirName, true); + // Verify: do stress test, by opening IndexReaders and + // IndexWriters over & over in 2 threads and making sure + // no unexpected exceptions are raised, but use + // NativeFSLockFactory: + public void testStressLocksNativeFSLockFactory() throws IOException { + _testStressLocks(NativeFSLockFactory.getLockFactory(), "index.TestLockFactory7"); + } + + public void _testStressLocks(LockFactory lockFactory, String indexDirName) throws IOException { + FSDirectory fs1 = FSDirectory.getDirectory(indexDirName, true, lockFactory); // fs1.setLockFactory(NoLockFactory.getNoLockFactory()); // First create a 1 doc index: @@ -295,6 +305,31 @@ rmDir(indexDirName); } + // Verify: NativeFSLockFactory works correctly + public void testNativeFSLockFactory() throws IOException { + + // Make sure we get identical instances: + NativeFSLockFactory f = NativeFSLockFactory.getLockFactory(); + NativeFSLockFactory f2 = NativeFSLockFactory.getLockFactory(); + assertTrue("got different NativeFSLockFactory instances for same directory", + f == f2); + + // Make sure we get identical locks: + f.setLockPrefix("test"); + Lock l = f.makeLock("commit"); + Lock l2 = f.makeLock("commit"); + assertTrue("got different Lock instances for same lock name", + l == l2); + + assertTrue("failed to obtain lock", l.obtain()); + assertTrue("succeeded in obtaining lock twice", !l.obtain()); + l.release(); + + // Make sure we can obtain it again: + assertTrue("failed to obtain lock", l.obtain()); + l.release(); + } + private class WriterThread extends Thread { private Directory dir; private int numIteration; Index: src/java/org/apache/lucene/index/FieldsReader.java =================================================================== --- src/java/org/apache/lucene/index/FieldsReader.java (revision 452205) +++ src/java/org/apache/lucene/index/FieldsReader.java (working copy) @@ -55,7 +55,7 @@ } /** - * Cloeses the underlying {@link org.apache.lucene.store.IndexInput} streams, including any ones associated with a + * Closes the underlying {@link org.apache.lucene.store.IndexInput} streams, including any ones associated with a * lazy implementation of a Field. This means that the Fields values will not be accessible. * * @throws IOException Index: src/java/org/apache/lucene/store/LockFactory.java =================================================================== --- src/java/org/apache/lucene/store/LockFactory.java (revision 452205) +++ src/java/org/apache/lucene/store/LockFactory.java (working copy) @@ -28,7 +28,14 @@ protected String lockPrefix = ""; /** - * Set the prefix in use for all locks created in this LockFactory. + * Set the prefix in use for all locks created in this + * LockFactory. This is normally called once, when a + * Directory gets this LockFactory instance. However, you + * can also call this (after this instance is assigned to + * a Directory) to override the prefix in use. This + * is helpful if you're running Lucene on machines that + * have different mount points for the same shared + * directory. */ public void setLockPrefix(String lockPrefix) { this.lockPrefix = lockPrefix; Index: src/java/org/apache/lucene/store/NativeFSLockFactory.java =================================================================== --- src/java/org/apache/lucene/store/NativeFSLockFactory.java (revision 0) +++ src/java/org/apache/lucene/store/NativeFSLockFactory.java (revision 0) @@ -0,0 +1,313 @@ +package org.apache.lucene.store; + +/** + * Copyright 2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.io.File; +import java.io.RandomAccessFile; +import java.io.IOException; +import java.util.Hashtable; +import java.util.Random; + +/** + * Implements {@link LockFactory} using native OS file locks + * (available through java.nio.*). Note that for certain + * filesystems native locks are possible but must be + * explicity configured and enabled (and may be disabled by + * default). For example, for NFS servers there sometimes + * must be a separate lockd process running, and other + * configuration may be required such as running the server + * in kernel mode. Other filesystems may not even support + * native OS locks in which case you must use a different + * {@link LockFactory} implementation. + * + *
The advantage of this lock factory over + * SimpleFSLockFactory is that the locks should be + * "correct", whereas SimpleFSLockFactory uses + * java.io.File.createNewFile which has warnings about not + * using it for locking. Furthermore, if the JVM crashes, + * the OS will free any held locks, whereas + * SimpleFSLockFactory will keep the locks held, requiring + * manual removal before re-running Lucene.
+ * + *Note that, unlike SimpleFSLockFactory, the existence of + * leftover lock files in the filesystem on exiting the JVM + * is fine because the OS will free the locks held against + * these files even though the files still remain.
+ * + *Native locks file names have the substring "-n-", which + * you can use to differentiate them from lock files created + * by SimpleFSLockFactory.
+ * + * @see LockFactory + */ + +public class NativeFSLockFactory extends LockFactory { + + /** + * Directory specified byorg.apache.lucene.lockDir
+ * system property. If that is not set, then java.io.tmpdir
+ * system property is used.
+ */
+
+ public static final String LOCK_DIR =
+ System.getProperty("org.apache.lucene.lockDir",
+ System.getProperty("java.io.tmpdir"));
+
+ private File lockDir;
+
+ /*
+ * The javadocs for FileChannel state that you should have
+ * a single instance of a FileChannel (per JVM) for all
+ * locking against a given file. To do this, we ensure
+ * there's a single instance of NativeFSLockFactory per
+ * canonical lock directory, and then we always use a
+ * single lock instance (per lock name) if it's present.
+ */
+ private static Hashtable LOCK_FACTORIES = new Hashtable();
+
+ private Hashtable locks = new Hashtable();
+
+ protected NativeFSLockFactory(File lockDir)
+ throws IOException {
+
+ this.lockDir = lockDir;
+
+ // Ensure that lockDir exists and is a directory.
+ if (!lockDir.exists()) {
+ if (!lockDir.mkdirs())
+ throw new IOException("Cannot create directory: " +
+ lockDir.getAbsolutePath());
+ } else if (!lockDir.isDirectory()) {
+ throw new IOException("Found regular file where directory expected: " +
+ lockDir.getAbsolutePath());
+ }
+ acquireTestLock();
+ }
+
+ // Simple test to verify locking system is "working". On
+ // NFS, if it's misconfigured, you can hit long (35
+ // second) timeouts which cause Lock.obtain to take far
+ // too long (it assumes the obtain() call takes zero
+ // time). Since it's a configuration problem, we test up
+ // front once on creating the LockFactory:
+ private void acquireTestLock() throws IOException {
+ String randomLockName = "lucene-" + Long.toString(new Random().nextInt(), Character.MAX_RADIX) + "-test.lock";
+
+ Lock l = makeLock(randomLockName);
+ try {
+ l.obtain();
+ } catch (IOException e) {
+ IOException e2 = new IOException("Failed to acquire random test lock; please verify filesystem for lock directory '" + lockDir + "' supports locking");
+ e2.initCause(e);
+ throw e2;
+ }
+
+ l.release();
+ }
+
+ /**
+ * Returns a NativeFSLockFactory instance, storing lock
+ * files into the default LOCK_DIR:
+ * org.apache.lucene.lockDir system property,
+ * or (if that is null) then java.io.tmpdir.
+ */
+ public static NativeFSLockFactory getLockFactory() throws IOException {
+ return getLockFactory(new File(LOCK_DIR));
+ }
+
+ /**
+ * Returns a NativeFSLockFactory instance, storing lock
+ * files into the specified lockDirName:
+ *
+ * @param lockDirName where lock files are created.
+ */
+ public static NativeFSLockFactory getLockFactory(String lockDirName) throws IOException {
+ return getLockFactory(new File(lockDirName));
+ }
+
+ /**
+ * Returns a NativeFSLockFactory instance, storing lock
+ * files into the specified lockDir:
+ *
+ * @param lockDir where lock files are created.
+ */
+ public static NativeFSLockFactory getLockFactory(File lockDir) throws IOException {
+ lockDir = new File(lockDir.getCanonicalPath());
+
+ NativeFSLockFactory f;
+
+ synchronized(LOCK_FACTORIES) {
+ f = (NativeFSLockFactory) LOCK_FACTORIES.get(lockDir);
+ if (f == null) {
+ f = new NativeFSLockFactory(lockDir);
+ LOCK_FACTORIES.put(lockDir, f);
+ }
+ }
+
+ return f;
+ }
+
+ public synchronized Lock makeLock(String lockName) {
+ Lock l = (Lock) locks.get(lockName);
+ if (l == null) {
+ String fullName;
+ if (lockPrefix.equals("")) {
+ fullName = lockName;
+ } else {
+ fullName = lockPrefix + "-n-" + lockName;
+ }
+
+ l = new NativeFSLock(lockDir, fullName);
+ locks.put(lockName, l);
+ }
+ return l;
+ }
+
+ public void clearAllLocks() throws IOException {
+ // Note that this isn't strictly required anymore
+ // because the existence of these files does not mean
+ // they are locked, but, still do this in case people
+ // really want to see the files go away:
+ if (lockDir.exists()) {
+ String[] files = lockDir.list();
+ if (files == null)
+ throw new IOException("Cannot read lock directory " +
+ lockDir.getAbsolutePath());
+ String prefix = lockPrefix + "-n-";
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].startsWith(prefix)) {
+ File lockFile = new File(lockDir, files[i]);
+ if (!lockFile.delete())
+ throw new IOException("Cannot delete " + lockFile);
+ }
+ }
+ }
+ }
+};
+
+class NativeFSLock extends Lock {
+
+ private RandomAccessFile f;
+ private FileChannel channel;
+ private FileLock lock;
+ private File path;
+ private File lockDir;
+
+ public NativeFSLock(File lockDir, String lockFileName) {
+ this.lockDir = lockDir;
+ path = new File(lockDir, lockFileName);
+ }
+
+ public synchronized boolean obtain() throws IOException {
+
+ if (isLocked()) {
+ // We are already locked:
+ return false;
+ }
+
+ // Ensure that lockDir exists and is a directory.
+ if (!lockDir.exists()) {
+ if (!lockDir.mkdirs())
+ throw new IOException("Cannot create directory: " +
+ lockDir.getAbsolutePath());
+ } else if (!lockDir.isDirectory()) {
+ throw new IOException("Found regular file where directory expected: " +
+ lockDir.getAbsolutePath());
+ }
+
+ f = new RandomAccessFile(path, "rw");
+ try {
+ channel = f.getChannel();
+ try {
+ try {
+ lock = channel.tryLock();
+ } catch (IOException e) {
+ // At least on OS X, we will sometimes get an
+ // intermittant "Permission Denied" IOException,
+ // which seems to simply mean "you failed to get
+ // the lock". But other IOExceptions could be
+ // "permanent" (eg, locking is not supported via
+ // the filesystem). So, we record the failure
+ // reason here; the timeout obtain (usually the
+ // one calling us) will use this as "root cause"
+ // if it fails to get the lock.
+ failureReason = e;
+ }
+ } finally {
+ if (lock == null) {
+ try {
+ channel.close();
+ } finally {
+ channel = null;
+ }
+ }
+ }
+ } finally {
+ if (channel == null) {
+ try {
+ f.close();
+ } finally {
+ f = null;
+ }
+ }
+ }
+ return lock != null;
+ }
+
+ public synchronized void release() {
+ try {
+ if (isLocked()) {
+ try {
+ lock.release();
+ } finally {
+ lock = null;
+ try {
+ channel.close();
+ } finally {
+ channel = null;
+ try {
+ f.close();
+ } finally {
+ f = null;
+ }
+ }
+ }
+ path.delete();
+ }
+ } catch (IOException e) {
+ // Not sure how to better message/handle this without
+ // changing API?
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isLocked() {
+ return lock != null;
+ }
+
+ public String toString() {
+ return "NativeFSLock@" + path;
+ }
+
+ public void finalize() {
+ if (isLocked()) {
+ release();
+ }
+ }
+}
Index: src/java/org/apache/lucene/store/SimpleFSLockFactory.java
===================================================================
--- src/java/org/apache/lucene/store/SimpleFSLockFactory.java (revision 452205)
+++ src/java/org/apache/lucene/store/SimpleFSLockFactory.java (working copy)
@@ -80,17 +80,19 @@
}
public void clearAllLocks() throws IOException {
- String[] files = lockDir.list();
- if (files == null)
- throw new IOException("Cannot read lock directory " +
- lockDir.getAbsolutePath());
- String prefix = lockPrefix + "-";
- for (int i = 0; i < files.length; i++) {
- if (!files[i].startsWith(prefix))
- continue;
- File lockFile = new File(lockDir, files[i]);
- if (!lockFile.delete())
- throw new IOException("Cannot delete " + lockFile);
+ if (lockDir.exists()) {
+ String[] files = lockDir.list();
+ if (files == null)
+ throw new IOException("Cannot read lock directory " +
+ lockDir.getAbsolutePath());
+ String prefix = lockPrefix + "-";
+ for (int i = 0; i < files.length; i++) {
+ if (!files[i].startsWith(prefix))
+ continue;
+ File lockFile = new File(lockDir, files[i]);
+ if (!lockFile.delete())
+ throw new IOException("Cannot delete " + lockFile);
+ }
}
}
};
Index: src/java/org/apache/lucene/store/Lock.java
===================================================================
--- src/java/org/apache/lucene/store/Lock.java (revision 452205)
+++ src/java/org/apache/lucene/store/Lock.java (working copy)
@@ -40,6 +40,13 @@
*/
public abstract boolean obtain() throws IOException;
+ /**
+ * If a lock obtain called, this failureReason may be set
+ * with the "root cause" Exception as to why the lock was
+ * not obtained.
+ */
+ protected Throwable failureReason;
+
/** Attempts to obtain an exclusive lock within amount
* of time given. Currently polls once per second until
* lockWaitTimeout is passed.
@@ -48,12 +55,21 @@
* @throws IOException if lock wait times out or obtain() throws an IOException
*/
public boolean obtain(long lockWaitTimeout) throws IOException {
+ failureReason = null;
boolean locked = obtain();
int maxSleepCount = (int)(lockWaitTimeout / LOCK_POLL_INTERVAL);
int sleepCount = 0;
while (!locked) {
if (sleepCount++ == maxSleepCount) {
- throw new IOException("Lock obtain timed out: " + this.toString());
+ String reason = "Lock obtain timed out: " + this.toString();
+ if (failureReason != null) {
+ reason += ": " + failureReason;
+ }
+ IOException e = new IOException(reason);
+ if (failureReason != null) {
+ e.initCause(failureReason);
+ }
+ throw e;
}
try {
Thread.sleep(LOCK_POLL_INTERVAL);
Index: src/java/org/apache/lucene/store/FSDirectory.java
===================================================================
--- src/java/org/apache/lucene/store/FSDirectory.java (revision 452205)
+++ src/java/org/apache/lucene/store/FSDirectory.java (working copy)
@@ -228,8 +228,8 @@
// Set up lockFactory with cascaded defaults: if an instance was passed in,
// use that; else if locks are disabled, use NoLockFactory; else if the
- // system property org.apache.lucene.lockClass is set, instantiate that;
- // else, use SimpleFSLockFactory:
+ // system property org.apache.lucene.store.FSDirectoryLockFactoryClass is set,
+ // instantiate that; else, use SimpleFSLockFactory:
if (lockFactory == null) {