| /* |
| * 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. |
| */ |
| package org.apache.lucene.store; |
| |
| |
| import java.nio.channels.FileChannel; |
| import java.nio.channels.FileLock; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.StandardOpenOption; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.nio.file.attribute.FileTime; |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| import org.apache.lucene.util.IOUtils; |
| |
| /** |
| * <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 locks (not the lock file itself) 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 is fine because the OS |
| * will free the locks held against these files even though the |
| * files still remain. Lucene will never actively remove the lock |
| * files, so although you see them, the index may not be locked.</p> |
| * |
| * <p>Special care needs to be taken if you change the locking |
| * implementation: First be certain that no writer is in fact |
| * writing to the index otherwise you can easily corrupt |
| * your index. Be sure to do the LockFactory change on all Lucene |
| * instances and clean up all leftover lock files before starting |
| * the new configuration for the first time. Different implementations |
| * can not work together!</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> |
| * |
| * <p>This is a singleton, you have to use {@link #INSTANCE}. |
| * |
| * @see LockFactory |
| */ |
| |
| public final class NativeFSLockFactory extends FSLockFactory { |
| |
| /** |
| * Singleton instance |
| */ |
| public static final NativeFSLockFactory INSTANCE = new NativeFSLockFactory(); |
| |
| private static final Set<String> LOCK_HELD = Collections.synchronizedSet(new HashSet<String>()); |
| |
| private NativeFSLockFactory() {} |
| |
| @Override |
| protected Lock obtainFSLock(FSDirectory dir, String lockName) throws IOException { |
| Path lockDir = dir.getDirectory(); |
| |
| // Ensure that lockDir exists and is a directory. |
| // note: this will fail if lockDir is a symlink |
| Files.createDirectories(lockDir); |
| |
| Path lockFile = lockDir.resolve(lockName); |
| |
| IOException creationException = null; |
| try { |
| Files.createFile(lockFile); |
| } catch (IOException ignore) { |
| // we must create the file to have a truly canonical path. |
| // if it's already created, we don't care. if it cant be created, it will fail below. |
| creationException = ignore; |
| } |
| |
| // fails if the lock file does not exist |
| final Path realPath; |
| try { |
| realPath = lockFile.toRealPath(); |
| } catch (IOException e) { |
| // if we couldn't resolve the lock file, it might be because we couldn't create it. |
| // so append any exception from createFile as a suppressed exception, in case its useful |
| if (creationException != null) { |
| e.addSuppressed(creationException); |
| } |
| throw e; |
| } |
| |
| // used as a best-effort check, to see if the underlying file has changed |
| final FileTime creationTime = Files.readAttributes(realPath, BasicFileAttributes.class).creationTime(); |
| |
| if (LOCK_HELD.add(realPath.toString())) { |
| FileChannel channel = null; |
| FileLock lock = null; |
| try { |
| channel = FileChannel.open(realPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| lock = channel.tryLock(); |
| if (lock != null) { |
| return new NativeFSLock(lock, channel, realPath, creationTime); |
| } else { |
| throw new LockObtainFailedException("Lock held by another program: " + realPath); |
| } |
| } finally { |
| if (lock == null) { // not successful - clear up and move out |
| IOUtils.closeWhileHandlingException(channel); // TODO: addSuppressed |
| clearLockHeld(realPath); // clear LOCK_HELD last |
| } |
| } |
| } else { |
| throw new LockObtainFailedException("Lock held by this virtual machine: " + realPath); |
| } |
| } |
| |
| private static final void clearLockHeld(Path path) throws IOException { |
| boolean remove = LOCK_HELD.remove(path.toString()); |
| if (remove == false) { |
| throw new AlreadyClosedException("Lock path was cleared but never marked as held: " + path); |
| } |
| } |
| |
| // TODO: kind of bogus we even pass channel: |
| // FileLock has an accessor, but mockfs doesnt yet mock the locks, too scary atm. |
| |
| static final class NativeFSLock extends Lock { |
| final FileLock lock; |
| final FileChannel channel; |
| final Path path; |
| final FileTime creationTime; |
| volatile boolean closed; |
| |
| NativeFSLock(FileLock lock, FileChannel channel, Path path, FileTime creationTime) { |
| this.lock = lock; |
| this.channel = channel; |
| this.path = path; |
| this.creationTime = creationTime; |
| } |
| |
| @Override |
| public void ensureValid() throws IOException { |
| if (closed) { |
| throw new AlreadyClosedException("Lock instance already released: " + this); |
| } |
| // check we are still in the locks map (some debugger or something crazy didn't remove us) |
| if (!LOCK_HELD.contains(path.toString())) { |
| throw new AlreadyClosedException("Lock path unexpectedly cleared from map: " + this); |
| } |
| // check our lock wasn't invalidated. |
| if (!lock.isValid()) { |
| throw new AlreadyClosedException("FileLock invalidated by an external force: " + this); |
| } |
| // try to validate the underlying file descriptor. |
| // this will throw IOException if something is wrong. |
| long size = channel.size(); |
| if (size != 0) { |
| throw new AlreadyClosedException("Unexpected lock file size: " + size + ", (lock=" + this + ")"); |
| } |
| // try to validate the backing file name, that it still exists, |
| // and has the same creation time as when we obtained the lock. |
| // if it differs, someone deleted our lock file (and we are ineffective) |
| FileTime ctime = Files.readAttributes(path, BasicFileAttributes.class).creationTime(); |
| if (!creationTime.equals(ctime)) { |
| throw new AlreadyClosedException("Underlying file changed by an external force at " + ctime + ", (lock=" + this + ")"); |
| } |
| } |
| |
| @Override |
| public synchronized void close() throws IOException { |
| if (closed) { |
| return; |
| } |
| // NOTE: we don't validate, as unlike SimpleFSLockFactory, we can't break others locks |
| // first release the lock, then the channel |
| try (FileChannel channel = this.channel; |
| FileLock lock = this.lock) { |
| assert lock != null; |
| assert channel != null; |
| } finally { |
| closed = true; |
| clearLockHeld(path); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "NativeFSLock(path=" + path + ",impl=" + lock + ",creationTime=" + creationTime + ")"; |
| } |
| } |
| } |