package io.eguan.nrs;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* Licensed 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.
* #L%
*/
import io.eguan.configuration.MetaConfiguration;
import io.eguan.net.MsgClientStartpoint;
import io.eguan.proto.nrs.NrsRemote.NrsVersion;
import io.eguan.utils.UuidCharSequence;
import io.eguan.utils.UuidT;
import io.eguan.utils.Files.OpenedFileHandler;
import io.eguan.utils.mapper.FileMapper;
import io.eguan.utils.mapper.FileMapperConfigKey;
import io.eguan.utils.mapper.FileMapper.Type;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.FileStore;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class manages the {@link NrsFile}s. Handles the creation, the deletion, the opening and the closing of these
* files. Each {@link NrsFile} must have at most one instance at a time to handle read/write access to its contents.
*
* @author oodrive
* @author llambert
* @author pwehrle
*
*/
public final class NrsFileJanitor {
private static final Logger LOGGER = LoggerFactory.getLogger(NrsFileJanitor.class);
/** Constant string key to flag a {@link NrsFile} as 'sealed'. */
private static final String ATTR_SEALED = "NrsJanitor.sealed";
/** The {@link FileMapper} used for {@link NrsFile}s. */
private final FileMapper imagesFileMapper;
/** The {@link FileMapper} used for {@link NrsFileBlock}s. */
private final FileMapper blocksFileMapper;
/** target directory for NRS file storage. */
private final File directory;
/** The limit blocking space left percentage in percent. */
private final int limitPercentage;
/** The cluster size in bytes. */
private final int clusterSize;
/** Directory to store the {@link NrsFile}s */
private final File imagesDirectory;
/** Handler of opened NrsFiles */
private OpenedFileHandler<NrsFile, UuidT<NrsFile>> openedFileHandler;
/** Optional client start point for remote notification */
private final AtomicReference<NrsMsgPostOffice> postOfficeRef = new AtomicReference<>();
public NrsFileJanitor(@Nonnull final MetaConfiguration configuration) {
LOGGER.trace(String.format("Constructing a new instance of %s", NrsFileJanitor.class.getSimpleName()));
this.clusterSize = NrsClusterSizeConfigKey.getInstance().getTypedValue(Objects.requireNonNull(configuration))
.intValue();
this.directory = NrsStorageConfigKey.getInstance().getTypedValue(configuration);
// Images
{
final File imagesDir = ImagesFileDirectoryConfigKey.getInstance().getTypedValue(configuration);
this.imagesDirectory = new File(this.directory, imagesDir.getPath());
if (!imagesDirectory.exists() && !imagesDirectory.mkdirs()) {
throw new IllegalStateException("Failed to create image storage directory '" + imagesDirectory + "'");
}
this.imagesFileMapper = getFileMapperFromConfig(configuration, imagesDirectory);
}
// Blocks
{
final File blocksDir = BlkCacheDirectoryConfigKey.getInstance().getTypedValue(configuration);
final File blocksDirectory = new File(this.directory, blocksDir.getPath());
if (!blocksDirectory.exists() && !blocksDirectory.mkdirs()) {
throw new IllegalStateException("Failed to create block cache storage directory '" + blocksDirectory
+ "'");
}
this.blocksFileMapper = getFileMapperFromConfig(configuration, blocksDirectory);
}
this.limitPercentage = RemainingSpaceCreateLimitConfigKey.getInstance().getTypedValue(configuration).intValue();
}
/**
* Initialize the handling of {@link NrsFile}s.
*/
public final void init() {
// Opened files
this.openedFileHandler = io.eguan.utils.Files.newOpenedFileHandler();
}
/**
* Release internal resources.
*/
public final void fini() {
// Close opened files
try {
openedFileHandler.cancel();
}
catch (final Throwable t) {
LOGGER.warn("Error while cancelling task", t);
}
try {
openedFileHandler.closeAll();
}
catch (final Throwable t) {
LOGGER.warn("Error while closing NrsFiles", t);
}
openedFileHandler = null;
// Flush the optional post office
flushPostOffice();
}
/**
* Sets the {@link MsgClientStartpoint} for the remote update and synchronization of {@link NrsFile}.
* <p>
* Note: the {@link MsgClientStartpoint} is set when a file is opened. The caller should close the {@link NrsFile}s
* before setting the <code>clientStartpoint</code>.
*
* @param startpoint
* the {@link MsgClientStartpoint} for remote update or <code>null</code> to disable remote update.
* @param enhancer
* message enhancer, may be <code>null</code>
*/
public final void setClientStartpoint(final MsgClientStartpoint startpoint, final NrsMsgEnhancer enhancer) {
// Flush previous instance (if any)
flushPostOffice();
this.postOfficeRef.set(startpoint == null ? null : new NrsMsgPostOffice(startpoint, enhancer));
}
private final void flushPostOffice() {
final NrsMsgPostOffice postOffice = postOfficeRef.get();
if (postOffice != null) {
postOffice.flush();
}
}
/**
* Send now the pending messages for a file.
*
* @param nrsFileUuid
* UUID of the {@link NrsFile}.
*/
private final void flushNrsFileMessages(final UuidT<NrsFile> nrsFileUuid) {
final NrsMsgPostOffice postOffice = postOfficeRef.get();
if (postOffice != null) {
postOffice.flush(nrsFileUuid);
}
}
/**
* Clear {@link NrsFile} cache. For unit test purpose only, to actually create new instances for files. Fails if
* some files are opened.
*/
final void clearCache() {
openedFileHandler.cacheClear();
}
/**
* Constructs a new NRS file.
*
* This method gets all information not contained in the descriptor from the {@link MetaConfiguration} values
* obtained upon {@link #NrsFileJanitor(MetaConfiguration) construction}.
*
* @param header
* the header of the persistent image
* @return a functional instance of {@link NrsFile}
* @throws NrsException
* if the file cannot be created for any reason
*/
public final NrsFile createNrsFile(final NrsFileHeader<NrsFile> header) throws NrsException {
final NrsFile result;
// checks the remaining space on the file store above alert level
try {
final FileStore targetStore = Files.getFileStore(directory.toPath());
if (io.eguan.utils.Files.getRemainingUsablePercentage(targetStore) < limitPercentage) {
throw new NrsException("Remaining storage space limit percentage reached");
}
result = new NrsFile(imagesFileMapper, header, postOfficeRef.get());
result.create();
// Create the block file if necessary
try {
if (header.isBlocks()) {
final NrsFileHeader<NrsFileBlock> headerBlocks = header.newBlocksHeader();
final NrsFileBlock nrsFileBlock = new NrsFileBlock(blocksFileMapper, headerBlocks,
postOfficeRef.get());
nrsFileBlock.create();
result.setFileBlock(nrsFileBlock);
}
}
catch (IOException | RuntimeException | Error e) {
result.delete();
throw e;
}
}
catch (final NrsException ne) {
throw ne;
}
catch (final IOException ie) {
throw new NrsException("Exception while creating persistent storage", ie);
}
openedFileHandler.cachePut(result.getDescriptor().getFileId(), result);
return result;
}
/**
* Loads an existing {@link NrsFile} from the provided file.
*
* @param sourceFile
* the file from which to load the {@link NrsFile}
* @return a functional instance read from the given file
* @throws NrsException
* if load failed
*/
public final NrsFile loadNrsFile(final Path sourceFile) throws NrsException {
// Atomic get/create instance
final NrsFileHeader<NrsFile> header = loadNrsFileHeader(sourceFile);
final Lock nrsFileInstancesLock = openedFileHandler.getCacheWriteLock();
nrsFileInstancesLock.lock();
try {
final UuidT<NrsFile> id = header.getFileId();
final NrsFile nrsFile = openedFileHandler.cacheLookup(id);
if (nrsFile != null) {
return nrsFile;
}
final NrsFile result = new NrsFile(imagesFileMapper, header, postOfficeRef.get());
assert result.getDescriptor().getFileId().equals(id);
// Load the block file if necessary
if (header.isBlocks()) {
// Need to read from the file to get the stored parameters (cluster size, ...)
final CharSequence blockId = new UuidCharSequence(id);
final Path blockFile = blocksFileMapper.mapIdToFile(blockId).toPath();
final NrsFileHeader<NrsFileBlock> headerBlocks = loadNrsFileHeader(blockFile);
// Some basic checks
assert headerBlocks.getDeviceId().equals(header.newBlocksHeader().getDeviceId());
assert headerBlocks.getParentId().equals(header.newBlocksHeader().getParentId());
assert headerBlocks.getFileId().equals(header.newBlocksHeader().getFileId());
final NrsFileBlock nrsFileBlock = new NrsFileBlock(blocksFileMapper, headerBlocks, postOfficeRef.get());
result.setFileBlock(nrsFileBlock);
}
openedFileHandler.cachePut(id, result);
return result;
}
finally {
nrsFileInstancesLock.unlock();
}
}
public final NrsFile loadNrsFile(final UuidT<NrsFile> uuid) throws NrsException {
// Look for an existing instance in the cache
final NrsFile nrsFile = openedFileHandler.cacheLookup(uuid);
if (nrsFile != null) {
return nrsFile;
}
final CharSequence id = new UuidCharSequence(uuid);
final Path imageFile = imagesFileMapper.mapIdToFile(id).toPath();
return loadNrsFile(imageFile);
}
public final <U> NrsFileHeader<U> loadNrsFileHeader(final Path sourceFile) throws NrsException {
try (FileChannel readChannel = FileChannel.open(sourceFile, StandardOpenOption.READ)) {
final ByteBuffer readBuffer = ByteBuffer.allocate(NrsFileHeader.HEADER_LENGTH);
readBuffer.order(NrsAbstractFile.NRS_BYTE_ORDER);
final int readLen = readChannel.read(readBuffer);
if (readLen != NrsFileHeader.HEADER_LENGTH) {
throw new NrsException("Error reading file '" + sourceFile + "' header, readLen=" + readLen
+ ", headerlen=" + NrsFileHeader.HEADER_LENGTH);
}
readBuffer.position(0);
return NrsFileHeader.readFromBuffer(readBuffer);
}
catch (final NrsException ne) {
throw ne;
}
catch (final IOException ie) {
throw new NrsException("Error reading file '" + sourceFile + "' header", ie);
}
}
/**
* Opens the {@link NrsFile} of ID <code>uuid</code>.
*
* @param uuid
* ID of the file to open
* @param readOnly
* <code>true</code> for a file opened read-only.
* @return the opened file
* @throws IllegalStateException
* @throws IOException
*/
public final NrsFile openNrsFile(final UuidT<NrsFile> uuid, final boolean readOnly) throws IOException {
final NrsFile nrsFile = loadNrsFile(uuid);
return openedFileHandler.open(nrsFile, readOnly);
}
/**
* Decrements the opened count and closes the {@link NrsFile} if the counter reached 0.
*
* @param nrsFile
* opened file to close
* @param setReadOnly
* if <code>true</code>, sets the file 'not writable' after the close.
* @throws IOException
*/
public final void closeNrsFile(final NrsFile nrsFile, final boolean setReadOnly) {
openedFileHandler.close(nrsFile);
if (setReadOnly) {
assert nrsFile.isWriteable();
try {
// Do not set the file read-only yet: some update messages may be still pending
sealNrsFile(nrsFile);
}
catch (final IOException e) {
LOGGER.warn("Failed to seal file '" + nrsFile.getFile() + "'", e);
}
}
// Flush messages related to this file
if (nrsFile.wasWritten()) {
flushNrsFileMessages(nrsFile.getDescriptor().getFileId());
}
}
/**
* Decrements the opened count of the {@link NrsFile} but does not close it.
*
* @param nrsFile
* opened file to unlock
* @throws IOException
*/
public final void unlockNrsFile(final NrsFile nrsFile) {
openedFileHandler.unlock(nrsFile);
}
/**
* Close the file if it is not locked.
*
* @param nrsFile
* to flush/close.
*/
public final void flushNrsFile(final NrsFile nrsFile) {
openedFileHandler.flush(nrsFile);
}
/**
* Tells if the file may be written.
*
* @param nrsFile
* @return <code>true</code> if the file can be written.
*/
public final boolean isNrsFileWritable(final NrsFile nrsFile) {
return nrsFile.isWriteable() && !isSealed(nrsFile);
}
/**
* Sets the file as read-only.
*
* @param nrsFile
* file to set read-only.
*/
public final void setNrsFileNoWritable(final NrsFile nrsFile) {
// First seal the file if not done yet
try {
sealNrsFile(nrsFile);
}
catch (final IOException e) {
LOGGER.warn("Failed to seal file " + nrsFile, e);
}
finally {
nrsFile.setNotWritable();
}
}
/**
* Prepare the update of the given {@link NrsFile}.
*
* @param nrsFile
* @param nrsVersion
* @throws IOException
*/
public final void prepareNrsFileUpdate(final NrsFile nrsFile, final NrsVersion nrsVersion) throws IOException {
// Must open the file in write mode
if (!nrsFile.isWriteable()) {
nrsFile.setWritable();
}
// Open, but do not lock the file
final NrsFile nrsFileOpened = openNrsFile(nrsFile.getDescriptor().getFileId(), false);
unlockNrsFile(nrsFileOpened);
assert nrsFileOpened == nrsFile;
}
/**
* Wait for the end of the update of the file and restore its status.
*
* @param nrsFile
* @param nrsVersion
* @return <code>true</code> if the update of the file was aborted for some reason.
*/
public final boolean endNrsFileUpdate(final NrsFile nrsFile, final NrsVersion nrsVersion) {
// Wait for the end of the update
final boolean inProgress = nrsFile.waitUpdateEnd(60, TimeUnit.SECONDS);
if (inProgress) {
nrsFile.resetUpdate();
}
// Restore write status
if (!nrsVersion.getWritable()) {
// File should not be in use locally: can safely close it
flushNrsFile(nrsFile);
nrsFile.setNotWritable();
}
return nrsFile.isLastUpdateAborted();
}
/**
* Deletes the given file.
*
* @param nrsFile
* an existing file created by this janitor
* @throws IOException
* if deletion fails
*/
public final void deleteNrsFile(final NrsFile nrsFile) throws IOException {
nrsFile.delete();
}
/**
* Visit the {@link NrsFile}s. TODO: visit NrsFiles, not Paths.
*
* @throws IOException
*/
public final void visitImages(final FileVisitor<? super Path> visitor) throws IOException {
Files.walkFileTree(imagesDirectory.toPath(), visitor);
}
/**
* Create a new {@link NrsFileHeader} builder, corresponding to the configuration of this janitor.
*
* @return a new builder.
*/
public final NrsFileHeader.Builder<NrsFile> newNrsFileHeaderBuilder() {
final NrsFileHeader.Builder<NrsFile> builder = new NrsFileHeader.Builder<>();
builder.clusterSize(clusterSize);
return builder;
}
/**
* Tag the file as sealed. Once sealed, a file should not be modified, except for update from a remote version if
* necessary.
*
* @param nrsFile
* {@link NrsFile} to seal.
* @throws IOException
*/
public final void sealNrsFile(final NrsFile nrsFile) throws IOException {
if (!isSealed(nrsFile)) {
final Path nrsFilePath = nrsFile.getFile();
io.eguan.utils.Files.setUserAttr(nrsFilePath, ATTR_SEALED);
}
}
/**
* Tells is the {@link NrsFile} is sealed.
*
* @param nrsFile
* @return <code>true</code> if the file is sealed.
* @throws IOException
*/
public final boolean isSealed(final NrsFile nrsFile) {
return isSealed(nrsFile.getFile());
}
/**
* Tells is the {@link NrsFile} denoted by the given path is sealed.
*
* @param nrsFilePath
* @return <code>true</code> if the file is sealed.
* @throws IOException
*/
public final boolean isSealed(final Path nrsFilePath) {
return io.eguan.utils.Files.isUserAttrSet(nrsFilePath, ATTR_SEALED);
}
/**
* Instantiates the {@link FileMapper} from the provided {@link MetaConfiguration}.
*
* @param the
* {@link MetaConfiguration}
* @return the {@link FileMapper} configured in the provided {@link MetaConfiguration}
*/
private static final FileMapper getFileMapperFromConfig(final MetaConfiguration configuration, final File baseDir) {
final Type fileMapperValue = FileMapperConfigKey.getInstance().getTypedValue(configuration);
assert fileMapperValue != null;
return fileMapperValue.newInstance(baseDir, 32, configuration);
}
}