blob: 119f7922db371e7b28dd977d679ac79218f02fb9 [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.ratis.server.storage;
import org.apache.ratis.util.AtomicFileOutputStream;
import org.apache.ratis.util.FileUtils;
import org.apache.ratis.util.SizeInBytes;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.management.ManagementFactory;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import static java.nio.file.Files.newDirectoryStream;
class RaftStorageDirectoryImpl implements RaftStorageDirectory {
private static final String IN_USE_LOCK_NAME = "in_use.lock";
private static final String META_FILE_NAME = "raft-meta";
private static final String CONF_EXTENSION = ".conf";
private static final String JVM_NAME = ManagementFactory.getRuntimeMXBean().getName();
enum StorageState {
UNINITIALIZED,
NON_EXISTENT,
NOT_FORMATTED,
NO_SPACE,
NORMAL
}
private final File root; // root directory
private FileLock lock; // storage lock
private final SizeInBytes freeSpaceMin;
/**
* Constructor
* @param dir directory corresponding to the storage
*/
RaftStorageDirectoryImpl(File dir, SizeInBytes freeSpaceMin) {
this.root = dir;
this.lock = null;
this.freeSpaceMin = freeSpaceMin;
}
@Override
public File getRoot() {
return root;
}
/**
* Clear and re-create storage directory.
* <p>
* Removes contents of the current directory and creates an empty directory.
*
* This does not fully format storage directory.
* It cannot write the version file since it should be written last after
* all other storage type dependent files are written.
* Derived storage is responsible for setting specific storage values and
* writing the version file to disk.
*/
void clearDirectory() throws IOException {
clearDirectory(getCurrentDir());
clearDirectory(getStateMachineDir());
}
private static void clearDirectory(File dir) throws IOException {
if (dir.exists()) {
LOG.info("{} already exists. Deleting it ...", dir);
FileUtils.deleteFully(dir);
}
FileUtils.createDirectories(dir);
}
File getMetaFile() {
return new File(getCurrentDir(), META_FILE_NAME);
}
File getMetaTmpFile() {
return AtomicFileOutputStream.getTemporaryFile(getMetaFile());
}
File getMetaConfFile() {
return new File(getCurrentDir(), META_FILE_NAME + CONF_EXTENSION);
}
/**
* Check to see if current/ directory is empty.
*/
boolean isCurrentEmpty() throws IOException {
File currentDir = getCurrentDir();
if(!currentDir.exists()) {
// if current/ does not exist, it's safe to format it.
return true;
}
try(DirectoryStream<Path> dirStream =
newDirectoryStream(currentDir.toPath())) {
if (dirStream.iterator().hasNext()) {
return false;
}
}
return true;
}
/**
* Check consistency of the storage directory.
*
* @return state {@link StorageState} of the storage directory
*/
StorageState analyzeStorage(boolean toLock) throws IOException {
Objects.requireNonNull(root, "root directory is null");
String rootPath = root.getCanonicalPath();
try { // check that storage exists
if (!root.exists()) {
LOG.info("The storage directory {} does not exist. Creating ...", rootPath);
FileUtils.createDirectories(root);
}
// or is inaccessible
if (!root.isDirectory()) {
LOG.warn("{} is not a directory", rootPath);
return StorageState.NON_EXISTENT;
}
if (!Files.isWritable(root.toPath())) {
LOG.warn("The storage directory {} is not writable.", rootPath);
return StorageState.NON_EXISTENT;
}
} catch(SecurityException ex) {
LOG.warn("Cannot access storage directory " + rootPath, ex);
return StorageState.NON_EXISTENT;
}
if (toLock) {
this.lock(); // lock storage if it exists
}
// check enough space
final long freeSpace = root.getFreeSpace();
if (freeSpace < freeSpaceMin.getSize()) {
LOG.warn("{} in directory {}: free space = {} < required = {}",
StorageState.NO_SPACE, rootPath, freeSpace, freeSpaceMin);
return StorageState.NO_SPACE;
}
// check whether current directory is valid
if (isHealthy()) {
return StorageState.NORMAL;
} else {
return StorageState.NOT_FORMATTED;
}
}
@Override
public boolean isHealthy() {
return getMetaFile().exists();
}
/**
* Lock storage to provide exclusive access.
*
* <p> Locking is not supported by all file systems.
* E.g., NFS does not consistently support exclusive locks.
*
* <p> If locking is supported we guarantee exclusive access to the
* storage directory. Otherwise, no guarantee is given.
*
* @throws IOException if locking fails
*/
void lock() throws IOException {
final File lockF = new File(root, IN_USE_LOCK_NAME);
final FileLock newLock = FileUtils.attempt(() -> tryLock(lockF), () -> "tryLock " + lockF);
if (newLock == null) {
String msg = "Cannot lock storage " + this.root
+ ". The directory is already locked";
LOG.info(msg);
throw new IOException(msg);
}
// Don't overwrite lock until success - this way if we accidentally
// call lock twice, the internal state won't be cleared by the second
// (failed) lock attempt
lock = newLock;
}
/**
* Attempts to acquire an exclusive lock on the storage.
*
* @return A lock object representing the newly-acquired lock or
* <code>null</code> if storage is already locked.
* @throws IOException if locking fails.
*/
@SuppressWarnings({"squid:S2095"}) // Suppress closeable warning
private FileLock tryLock(File lockF) throws IOException {
boolean deletionHookAdded = false;
if (!lockF.exists()) {
lockF.deleteOnExit();
deletionHookAdded = true;
}
RandomAccessFile file = new RandomAccessFile(lockF, "rws");
FileLock res;
try {
res = file.getChannel().tryLock();
if (null == res) {
LOG.error("Unable to acquire file lock on path {}", lockF);
throw new OverlappingFileLockException();
}
file.write(JVM_NAME.getBytes(StandardCharsets.UTF_8));
LOG.info("Lock on {} acquired by nodename {}", lockF, JVM_NAME);
} catch (OverlappingFileLockException oe) {
// Cannot read from the locked file on Windows.
LOG.error("It appears that another process "
+ "has already locked the storage directory: " + root, oe);
file.close();
throw new IOException("Failed to lock storage " + this.root + ". The directory is already locked", oe);
} catch(IOException e) {
LOG.error("Failed to acquire lock on " + lockF
+ ". If this storage directory is mounted via NFS, "
+ "ensure that the appropriate nfs lock services are running.", e);
file.close();
throw e;
}
if (!deletionHookAdded) {
// If the file existed prior to our startup, we didn't
// call deleteOnExit above. But since we successfully locked
// the dir, we can take care of cleaning it up.
lockF.deleteOnExit();
}
return res;
}
/**
* Unlock storage.
*/
void unlock() throws IOException {
if (this.lock == null) {
return;
}
this.lock.release();
lock.channel().close();
lock = null;
}
@Override
public String toString() {
return "Storage Directory " + this.root;
}
}