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() { 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) { 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) { 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).close(); 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<>(); 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()) { // TODO: NoSuchDirectoryException instead? 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 close() 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) { close(); } } } } @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) close(); return !obtained; } catch (IOException ioe) { return false; } } @Override public String toString() { return "NativeFSLock@" + path; } }