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 by org.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) {