blob: c61e4a30ebb82f5889dfc66806d436ea4c2f23fa [file] [log] [blame]
/*
* 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 + ")";
}
}
}