/*
 * 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;

Implements 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 SimpleFSLockFactory worked perfectly in those same environments. For NFS based access to an index, it's recommended that you try SimpleFSLockFactory first and work around the one limitation that a lock file could be left when the JVM exits abnormally.

The primary benefit of NativeFSLockFactory is that locks (not the lock file itself) will be properly removed (by the OS) if the JVM has an abnormal exit.

Note that, unlike 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.

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!

If you suspect that this or any other LockFactory is not working properly in your environment, you can easily test it by using VerifyingLockFactory, LockVerifyServer and LockStressTest.

This is a singleton, you have to use INSTANCE.

See Also:
/** * <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
/** * 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 + ")"; } } }