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; 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 * {@link SimpleFSLockFactory} is that the locks should be * "correct", whereas {@link 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 * {@link SimpleFSLockFactory} will keep the locks held, requiring * manual removal before re-running Lucene.

* *

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.

* *

Native locks file names have the substring "-n-", which * you can use to differentiate them from lock files created * by {@link 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. */ private File lockDir; // 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(); } /** * 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 { 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(); } public synchronized Lock makeLock(String lockName) { if (lockPrefix != null) lockName = lockPrefix + "-n-" + lockName; return new NativeFSLock(lockDir, lockName); } 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()) { if (lockPrefix != null) { lockName = lockPrefix + "-n-" + lockName; } File lockFile = new File(lockDir, lockName); if (lockFile.exists() && !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; /* * 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 ensure 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. */ private static HashSet LOCK_HELD = new HashSet(); public NativeFSLock(File lockDir, String lockFileName) { this.lockDir = lockDir; path = new File(lockDir, lockFileName); } public synchronized boolean obtain() throws IOException { if (isLocked()) { // 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 intermittant "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 // 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; } } } } } finally { if (markedHeld && !isLocked()) { synchronized(LOCK_HELD) { if (LOCK_HELD.contains(canonicalPath)) { LOCK_HELD.remove(canonicalPath); } } } } return isLocked(); } 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; synchronized(LOCK_HELD) { LOCK_HELD.remove(path.getCanonicalPath()); } } } } 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() throws Throwable { try { if (isLocked()) { release(); } } finally { super.finalize(); } } }