--- /dev/null
+package org.apache.lucene.store;
+
+/**
+ * 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.
+ */
+
+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.HashSet;
+
+/**
+ * <p>Implements {@link LockFactory} using native OS file
+ * locks. Note that because this LockFactory relies on
+ * java.nio.* APIs for locking, any problems with those APIs
+ * will cause locking to fail. Specifically, on certain NFS
+ * environments the java.nio.* locks will fail (the lock can
+ * incorrectly be double acquired) whereas {@link
+ * SimpleFSLockFactory} worked perfectly in those same
+ * environments. For NFS based access to an index, it's
+ * recommended that you try {@link SimpleFSLockFactory}
+ * first and work around the one limitation that a lock file
+ * could be left when the JVM exits abnormally.</p>
+ *
+ * <p>The primary benefit of {@link NativeFSLockFactory} is
+ * that lock files will be properly removed (by the OS) if
+ * the JVM has an abnormal exit.</p>
+ *
+ * <p>Note that, unlike {@link 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.</p>
+ *
+ * <p>If you suspect that this or any other LockFactory is
+ * not working properly in your environment, you can easily
+ * test it by using {@link VerifyingLockFactory}, {@link
+ * LockVerifyServer} and {@link LockStressTest}.</p>
+ *
+ * @see LockFactory
+ */
+
+public class NativeFSLockFactory extends FSLockFactory {
+
+ /**
+ * Create a NativeFSLockFactory instance, with null (unset)
+ * lock directory. When you pass this factory to a {@link FSDirectory}
+ * subclass, the lock directory is automatically set to the
+ * directory itself. Be sure to create one instance for each directory
+ * your create!
+ */
+ public NativeFSLockFactory() throws IOException {
+ this((File) null);
+ }
+
+ /**
+ * Create a NativeFSLockFactory instance, storing lock
+ * files into the specified lockDirName:
+ *
+ * @param lockDirName where lock files are created.
+ */
+ public NativeFSLockFactory(String lockDirName) throws IOException {
+ this(new File(lockDirName));
+ }
+
+ /**
+ * Create a NativeFSLockFactory instance, storing lock
+ * files into the specified lockDir:
+ *
+ * @param lockDir where lock files are created.
+ */
+ public NativeFSLockFactory(File lockDir) throws IOException {
+ setLockDir(lockDir);
+ }
+
+ @Override
+ public synchronized Lock makeLock(String lockName) {
+ if (lockPrefix != null)
+ lockName = lockPrefix + "-" + lockName;
+ return new NativeFSLock(lockDir, lockName);
+ }
+
+ @Override
+ public void clearLock(String lockName) 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()) {
+
+ // Try to release the lock first - if it's held by another process, this
+ // method should not silently fail.
+ // NOTE: makeLock fixes the lock name by prefixing it w/ lockPrefix.
+ // Therefore it should be called before the code block next which prefixes
+ // the given name.
+ makeLock(lockName).release();
+
+ if (lockPrefix != null) {
+ lockName = lockPrefix + "-" + lockName;
+ }
+
+ // As mentioned above, we don't care if the deletion of the file failed.
+ new File(lockDir, lockName).delete();
+ }
+ }
+}
+
+class NativeFSLock extends Lock {
+
+ private RandomAccessFile f;
+ private FileChannel channel;
+ private FileLock lock;
+ private File path;
+ 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 (locks are tracked per
+ * FileChannel instance in Java 1.4/1.5). Even using the same
+ * FileChannel instance is not completely thread-safe with Java
+ * 1.4/1.5 though. To work around this, we have a single (static)
+ * HashSet that contains the file paths of all currently
+ * locked locks. This protects against possible cases
+ * where different Directory instances in one JVM (each
+ * with their own NativeFSLockFactory instance) have set
+ * the same lock dir and lock prefix. However, this will not
+ * work when LockFactorys are created by different
+ * classloaders (eg multiple webapps).
+ *
+ * TODO: Java 1.6 tracks system wide locks in a thread safe manner
+ * (same FileChannel instance or not), so we may want to
+ * change this when Lucene moves to Java 1.6.
+ */
+ private static HashSet<String> LOCK_HELD = new HashSet<String>();
+
+ public NativeFSLock(File lockDir, String lockFileName) {
+ this.lockDir = lockDir;
+ path = new File(lockDir, lockFileName);
+ }
+
+ private synchronized boolean lockExists() {
+ return lock != null;
+ }
+
+ @Override
+ public synchronized boolean obtain() throws IOException {
+
+ if (lockExists()) {
+ // Our instance is 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());
+ }
+
+ String canonicalPath = path.getCanonicalPath();
+
+ boolean markedHeld = false;
+
+ try {
+
+ // Make sure nobody else in-process has this lock held
+ // already, and, mark it held if not:
+
+ synchronized(LOCK_HELD) {
+ if (LOCK_HELD.contains(canonicalPath)) {
+ // Someone else in this JVM already has the lock:
+ return false;
+ } else {
+ // This "reserves" the fact that we are the one
+ // thread trying to obtain this lock, so we own
+ // the only instance of a channel against this
+ // file:
+ LOCK_HELD.add(canonicalPath);
+ markedHeld = true;
+ }
+ }
+
+ try {
+ f = new RandomAccessFile(path, "rw");
+ } catch (IOException e) {
+ // On Windows, we can get intermittent "Access
+ // Denied" here. So, we treat this as failure to
+ // acquire the lock, but, store the reason in case
+ // there is in fact a real error case.
+ failureReason = e;
+ f = null;
+ }
+
+ if (f != null) {
+ try {
+ channel = f.getChannel();
+ try {
+ lock = channel.tryLock();
+ } catch (IOException e) {
+ // At least on OS X, we will sometimes get an
+ // intermittent "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;
+ }
+ }
+ }
+ }
+
+ } finally {
+ if (markedHeld && !lockExists()) {
+ synchronized(LOCK_HELD) {
+ if (LOCK_HELD.contains(canonicalPath)) {
+ LOCK_HELD.remove(canonicalPath);
+ }
+ }
+ }
+ }
+ return lockExists();
+ }
+
+ @Override
+ public synchronized void release() throws IOException {
+ if (lockExists()) {
+ try {
+ lock.release();
+ } finally {
+ lock = null;
+ try {
+ channel.close();
+ } finally {
+ channel = null;
+ try {
+ f.close();
+ } finally {
+ f = null;
+ synchronized(LOCK_HELD) {
+ LOCK_HELD.remove(path.getCanonicalPath());
+ }
+ }
+ }
+ }
+ // LUCENE-2421: we don't care anymore if the file cannot be deleted
+ // because it's held up by another process (e.g. AntiVirus). NativeFSLock
+ // does not depend on the existence/absence of the lock file
+ path.delete();
+ } else {
+ // if we don't hold the lock, and somebody still called release(), for
+ // example as a result of calling IndexWriter.unlock(), we should attempt
+ // to obtain the lock and release it. If the obtain fails, it means the
+ // lock cannot be released, and we should throw a proper exception rather
+ // than silently failing/not doing anything.
+ boolean obtained = false;
+ try {
+ if (!(obtained = obtain())) {
+ throw new LockReleaseFailedException(
+ "Cannot forcefully unlock a NativeFSLock which is held by another indexer component: "
+ + path);
+ }
+ } finally {
+ if (obtained) {
+ release();
+ }
+ }
+ }
+ }
+
+ @Override
+ public synchronized boolean isLocked() {
+ // The test for is isLocked is not directly possible with native file locks:
+
+ // First a shortcut, if a lock reference in this instance is available
+ if (lockExists()) return true;
+
+ // Look if lock file is present; if not, there can definitely be no lock!
+ if (!path.exists()) return false;
+
+ // Try to obtain and release (if was locked) the lock
+ try {
+ boolean obtained = obtain();
+ if (obtained) release();
+ return !obtained;
+ } catch (IOException ioe) {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "NativeFSLock@" + path;
+ }
+}