/*
* Copyright 2014 WANdisco
*
* WANdisco 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 c5db.log;
import c5db.LogConstants;
import c5db.util.CheckedSupplier;
import com.google.common.collect.ImmutableList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
/**
* LogPersistenceService using FilePersistence objects (Files and FileChannels).
*/
public class LogFileService implements LogPersistenceService<FilePersistence> {
private final Path logRootDir;
public LogFileService(Path basePath) throws IOException {
this.logRootDir = basePath.resolve(LogConstants.LOG_ROOT_DIRECTORY_RELATIVE_PATH);
createDirectoryStructure();
}
@Nullable
@Override
public FilePersistence getCurrent(String quorumId) throws IOException {
final Path currentLink = getCurrentLink(quorumId);
if (currentLink == null) {
return null;
} else {
return new FilePersistence(Files.readSymbolicLink(currentLink));
}
}
@NotNull
@Override
public FilePersistence create(String quorumId) throws IOException {
return new FilePersistence(getNewLogFilePath(quorumId));
}
@Override
public void append(String quorumId, @NotNull FilePersistence persistence) throws IOException {
final Path currentLink = getCurrentLink(quorumId);
final long linkId;
if (currentLink == null) {
linkId = 1;
} else {
linkId = linkIdOfFile(currentLink.toFile()) + 1;
}
Files.createSymbolicLink(pathForLinkId(linkId, quorumId), persistence.path);
}
@Override
public void truncate(String quorumId) throws IOException {
Files.delete(getCurrentLink(quorumId));
}
@Override
public ImmutableList<CheckedSupplier<FilePersistence, IOException>> getList(String quorumId) throws IOException {
ImmutableList.Builder<CheckedSupplier<FilePersistence, IOException>> persistenceSupplierBuilder =
ImmutableList.builder();
for (Path path : getLinkPathMap(quorumId).descendingMap().values()) {
persistenceSupplierBuilder.add(
() -> new FilePersistence(Files.readSymbolicLink(path)));
}
return persistenceSupplierBuilder.build();
}
/**
* Delete all the logs stored in the wal root directory.
*
* @throws IOException
*/
public void clearAllLogs() throws IOException {
Files.walkFileTree(logRootDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (!attrs.isDirectory()) {
Files.delete(file);
}
return FileVisitResult.CONTINUE;
}
});
}
/**
* Delete links to all logs for a given quorum except the current log, effectively
* removing the past logs from the record but keeping the data.
*
* @throws IOException
*/
public void archiveAllButCurrent(String quorumId) throws IOException {
NavigableMap<Long, Path> linkPathMap = getLinkPathMap(quorumId);
if (linkPathMap.isEmpty()) {
return;
}
long lastLinkId = linkPathMap.lastEntry().getKey();
for (Map.Entry<Long, Path> linkEntry : linkPathMap.entrySet()) {
if (linkEntry.getKey() != lastLinkId) {
Files.delete(linkEntry.getValue());
}
}
}
private Path getNewLogFilePath(String quorumId) throws IOException {
String fileName = String.valueOf(System.nanoTime());
createQuorumDirectoryIfNeeded(quorumId);
return logFileDir(quorumId).resolve(fileName);
}
@Nullable
private Path getCurrentLink(String quorumId) throws IOException {
Map.Entry<Long, Path> lastEntry = getLinkPathMap(quorumId).lastEntry();
if (lastEntry == null) {
return null;
} else {
return lastEntry.getValue();
}
}
private NavigableMap<Long, Path> getLinkPathMap(String quorumId) throws IOException {
NavigableMap<Long, Path> linkPathMap = new TreeMap<>();
for (File file : allFilesInDirectory(quorumDir(quorumId))) {
if (!Files.isSymbolicLink(file.toPath())) {
continue;
}
long fileId = linkIdOfFile(file);
linkPathMap.put(fileId, file.toPath());
}
return linkPathMap;
}
private long linkIdOfFile(File file) {
return Long.parseLong(file.getName());
}
private Path pathForLinkId(long linkId, String quorumId) {
return quorumDir(quorumId).resolve(String.valueOf(linkId));
}
private Path quorumDir(String quorumId) {
return logRootDir.resolve(quorumId);
}
private Path logFileDir(String quorumId) {
return quorumDir(quorumId).resolve(LogConstants.LOG_FILE_SUBDIRECTORY_RELATIVE_PATH);
}
private void createDirectoryStructure() throws IOException {
Files.createDirectories(logRootDir);
}
private void createQuorumDirectoryIfNeeded(String quorumId) throws IOException {
Files.createDirectories(quorumDir(quorumId));
Files.createDirectories(logFileDir(quorumId));
}
private static File[] allFilesInDirectory(Path dirPath) {
File[] files = dirPath.toFile().listFiles();
if (files == null) {
return new File[]{};
} else {
return files;
}
}
}