/*
* 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.brooklyn.core.mgmt.persist;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
import org.apache.brooklyn.core.server.BrooklynServerConfig;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.FatalConfigurationRuntimeException;
import org.apache.brooklyn.util.io.FileUtil;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.os.Os.DeletionResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
/**
* @author Andrea Turli
*/
public class FileBasedObjectStore implements PersistenceObjectStore {
private static final Logger log = LoggerFactory.getLogger(FileBasedObjectStore.class);
private static final int SHUTDOWN_TIMEOUT_MS = 10*1000;
private static boolean WARNED_ON_NON_ATOMIC_FILE_UPDATES = false;
private final File basedir;
private final ListeningExecutorService executor;
private ManagementContext mgmt;
private boolean prepared = false;
private boolean deferredBackupNeeded = false;
private AtomicBoolean doneFirstContentiousWrite = new AtomicBoolean(false);
/**
* @param basedir
*/
public FileBasedObjectStore(File basedir) {
this.basedir = checkPersistenceDirPlausible(basedir);
this.executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
log.debug("File-based objectStore will use directory {}", basedir);
// don't check accessible yet, we do that when we prepare
}
@Override
public String getSummaryName() {
return getBaseDir().getAbsolutePath();
}
public File getBaseDir() {
return basedir;
}
public void prepareForMasterUse() {
if (doneFirstContentiousWrite.get())
return;
synchronized (this) {
if (doneFirstContentiousWrite.get())
return;
try {
if (deferredBackupNeeded) {
// defer backup and path creation until first write
// this way if node is standby or auto, the backup is not created superfluously
File backup = backupDirByCopying(basedir);
log.info("Persistence deferred backup, directory "+basedir+" backed up to "+backup.getAbsolutePath());
deferredBackupNeeded = false;
}
} catch (Exception e) {
throw Exceptions.propagate(e);
}
doneFirstContentiousWrite.getAndSet(true);
}
}
@Override
public void createSubPath(String subPath) {
if (!prepared) throw new IllegalStateException("Not yet prepared: "+this);
File dir = new File(getBaseDir(), subPath);
if (dir.mkdir()) {
try {
FileUtil.setFilePermissionsTo700(dir);
} catch (IOException e) {
log.warn("Unable to set sub-directory permissions to 700 (continuing): "+dir);
}
} else {
if (!dir.exists())
throw new IllegalStateException("Cannot create "+dir+"; call returned false");
}
checkPersistenceDirAccessible(dir);
}
@Override
public StoreObjectAccessor newAccessor(String path) {
if (!prepared) throw new IllegalStateException("Not yet prepared: "+this);
String tmpExt = ".tmp";
if (mgmt!=null && mgmt.getManagementNodeId()!=null) tmpExt = "."+mgmt.getManagementNodeId()+tmpExt;
return new FileBasedStoreObjectAccessor(new File(Os.mergePaths(getBaseDir().getAbsolutePath(), path)), tmpExt);
}
@Override
public List<String> listContentsWithSubPath(final String parentSubPath) {
if (!prepared) throw new IllegalStateException("Not yet prepared: "+this);
Preconditions.checkNotNull(parentSubPath);
File subPathDir = new File(basedir, parentSubPath);
FileFilter fileFilter = new FileFilter() {
@Override public boolean accept(File file) {
// An inclusion filter would be safer than exclusion
return !file.getName().endsWith(".tmp") && !file.getName().endsWith(".swp");
}
};
File[] subPathDirFiles = subPathDir.listFiles(fileFilter);
if (subPathDirFiles==null) return ImmutableList.<String>of();
return FluentIterable.from(Arrays.asList(subPathDirFiles))
.transform(new Function<File, String>() {
@Nullable
@Override
public String apply(@Nullable File input) {
return format("%s/%s", parentSubPath, input.getName());
}
}).toList();
}
@Override
public void close() {
executor.shutdown();
try {
executor.awaitTermination(SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw Exceptions.propagate(e);
}
}
@Override
public String toString() {
return Objects.toStringHelper(this).add("basedir", basedir).toString();
}
@Override
public void injectManagementContext(ManagementContext mgmt) {
if (this.mgmt!=null && !this.mgmt.equals(mgmt))
throw new IllegalStateException("Cannot change mgmt context of "+this);
this.mgmt = mgmt;
}
@Override
public void prepareForSharedUse(@Nullable PersistMode persistMode, HighAvailabilityMode haMode) {
if (mgmt==null) throw new NullPointerException("Must inject ManagementContext before preparing "+this);
if (persistMode==null || persistMode==PersistMode.DISABLED) {
// TODO is this check needed? shouldn't come here now without persistence on.
prepared = true;
return;
}
@SuppressWarnings("deprecation")
Boolean backups = mgmt.getConfig().getConfig(BrooklynServerConfig.PERSISTENCE_BACKUPS_REQUIRED);
if (Boolean.TRUE.equals(backups)) {
log.warn("Using legacy backup for "+this+"; functionality will be removed in future versions, in favor of promotion/demotion-specific backups to a configurable backup location.");
}
// default backups behaviour here changed to false, Nov 2014, because these backups are now legacy;
// we prefer the made when persistence is enabled, using routines in BrooklynPersistenceUtils
if (backups==null) backups = false;
File dir = getBaseDir();
try {
String persistencePath = dir.getAbsolutePath();
switch (persistMode) {
case CLEAN:
if (dir.exists()) {
checkPersistenceDirAccessible(dir);
try {
if (backups) {
File old = backupDirByMoving(dir);
log.info("Persistence mode CLEAN, directory "+persistencePath+" backed up to "+old.getAbsolutePath());
} else {
deleteCompletely();
log.info("Persistence mode CLEAN, directory "+persistencePath+" deleted");
}
} catch (IOException e) {
throw new FatalConfigurationRuntimeException("Error using existing persistence directory "+dir.getAbsolutePath(), e);
}
} else {
log.debug("Persistence mode CLEAN, directory "+persistencePath+", no previous state");
}
break;
case REBIND:
checkPersistenceDirAccessible(dir);
checkPersistenceDirNonEmpty(dir);
try {
if (backups) {
if (haMode==HighAvailabilityMode.MASTER) {
File backup = backupDirByCopying(dir);
log.info("Persistence mode REBIND, directory "+persistencePath+" backed up to "+backup.getAbsolutePath());
} else {
deferredBackupNeeded = true;
}
}
} catch (IOException e) {
throw new FatalConfigurationRuntimeException("Error backing up persistence directory "+dir.getAbsolutePath(), e);
}
break;
case AUTO:
if (dir.exists()) {
checkPersistenceDirAccessible(dir);
}
if (dir.exists() && !isMementoDirExistButEmpty(dir)) {
try {
if (backups) {
if (haMode==HighAvailabilityMode.MASTER) {
File backup = backupDirByCopying(dir);
log.info("Persistence mode REBIND, directory "+persistencePath+" backed up to "+backup.getAbsolutePath());
} else {
deferredBackupNeeded = true;
}
}
} catch (IOException e) {
throw new FatalConfigurationRuntimeException("Error backing up persistence directory "+dir.getAbsolutePath(), e);
}
} else {
log.debug("Persistence mode AUTO, directory "+persistencePath+", no previous state");
}
break;
default:
throw new FatalConfigurationRuntimeException("Unexpected persist mode "+persistMode+"; modified during initialization?!");
};
if (!dir.exists()) {
boolean success = dir.mkdirs();
if (success) {
FileUtil.setFilePermissionsTo700(dir);
} else {
throw new FatalConfigurationRuntimeException("Failed to create persistence directory "+dir);
}
}
} catch (Exception e) {
throw Exceptions.propagate(e);
}
prepared = true;
}
protected File checkPersistenceDirPlausible(File dir) {
checkNotNull(dir, "directory");
if (!dir.exists()) return dir;
if (dir.isFile()) throw new FatalConfigurationRuntimeException("Invalid persistence directory" + dir + ": must not be a file");
if (!(dir.canRead() && dir.canWrite())) throw new FatalConfigurationRuntimeException("Invalid persistence directory" + dir + ": " +
(!dir.canRead() ? "not readable" :
(!dir.canWrite() ? "not writable" : "unknown reason")));
return dir;
}
protected void checkPersistenceDirAccessible(File dir) {
if (!(dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite())) {
FatalConfigurationRuntimeException problem = new FatalConfigurationRuntimeException("Invalid persistence directory " + dir + ": " +
(!dir.exists() ? "does not exist" :
(!dir.isDirectory() ? "not a directory" :
(!dir.canRead() ? "not readable" :
(!dir.canWrite() ? "not writable" : "unknown reason")))));
log.debug("Invalid persistence directory "+dir+" (rethrowing): "+problem, problem);
} else {
log.debug("Created dir {} for {}", dir, this);
}
}
protected void checkPersistenceDirNonEmpty(File persistenceDir) {
FatalConfigurationRuntimeException problem;
if (!persistenceDir.exists()) {
problem = new FatalConfigurationRuntimeException("Invalid persistence directory "+persistenceDir+" because directory does not exist");
log.debug("Invalid persistence directory "+persistenceDir+" (rethrowing): "+problem, problem);
throw problem;
} if (isMementoDirExistButEmpty(persistenceDir)) {
problem = new FatalConfigurationRuntimeException("Invalid persistence directory "+persistenceDir+" because directory is empty");
log.debug("Invalid persistence directory "+persistenceDir+" (rethrowing): "+problem, problem);
throw problem;
}
}
protected File backupDirByCopying(File dir) throws IOException, InterruptedException {
File parentDir = dir.getParentFile();
String simpleName = dir.getName();
String timestamp = new SimpleDateFormat("yyyyMMdd-hhmmssSSS").format(new Date());
File backupDir = new File(parentDir, simpleName+"."+timestamp+".bak");
FileUtil.copyDir(dir, backupDir);
FileUtil.setFilePermissionsTo700(backupDir);
return backupDir;
}
protected File backupDirByMoving(File dir) throws InterruptedException, IOException {
File parentDir = dir.getParentFile();
String simpleName = dir.getName();
String timestamp = new SimpleDateFormat("yyyyMMdd-hhmmssSSS").format(new Date());
File newDir = new File(parentDir, simpleName+"."+timestamp+".bak");
FileUtil.moveDir(dir, newDir);
return newDir;
}
/**
* Attempts an fs level atomic move then fall back to pure java rename.
* Assumes files are on same mount point.
* Overwriting existing destFile
*/
static void moveFile(File srcFile, File destFile) throws IOException, InterruptedException {
if (destFile.isDirectory()) {
deleteCompletely(destFile);
}
try {
Files.move(srcFile.toPath(), destFile.toPath(), StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
if (!WARNED_ON_NON_ATOMIC_FILE_UPDATES) {
WARNED_ON_NON_ATOMIC_FILE_UPDATES = true;
log.warn("Unable to perform atomic file update ("+srcFile+" to "+destFile+"); file system not recommended for production HA/DR");
}
Files.move(srcFile.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
if (log.isTraceEnabled()) {
log.trace("Completly moved from {} to {} completed", new Object[] { srcFile, destFile });
}
}
/**
* True if directory exists, but is entirely empty, or only contains empty directories.
*/
static boolean isMementoDirExistButEmpty(String dir) {
return isMementoDirExistButEmpty(new File(dir));
}
static boolean isMementoDirExistButEmpty(File dir) {
if (!dir.exists()) return false;
File[] contents = dir.listFiles();
if (contents == null) return false;
for (File sub : contents) {
if (sub.isFile()) return false;
if (sub.isDirectory() && sub.listFiles().length > 0) return false;
}
return true;
}
@Override
public void deleteCompletely() {
deleteCompletely(getBaseDir());
}
public static void deleteCompletely(File d) {
DeletionResult result = Os.deleteRecursively(d);
if (!result.wasSuccessful())
log.warn("Unable to delete persistence dir "+d);
}
}