package voldemort.store.readonly;
import com.google.common.collect.Maps;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import voldemort.server.VoldemortConfig;
import voldemort.store.PersistenceFailureException;
import voldemort.store.readonly.swapper.FailedFetchLock;
import voldemort.utils.Props;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Map;
/**
* This class helps manage stores that have multiple versions of their data set.
*
* Currently, the class only supports the following functionality:
* 1. Enabling/disabling specific store versions,
* 2. Keeping track of which version is the current one.
*
* Note that 1 and 2 are orthogonal: the current version can be enabled or disabled.
*
* TODO: Port atomic swap functionality here.
* TODO: Port delete backups functionality here.
*
* Eventually, this can allow us to generically manage stores with multiple data set
* versions (not just ReadOnly).
*/
public class StoreVersionManager {
private static final Logger logger = Logger.getLogger(StoreVersionManager.class);
private static final String DISABLED_MARKER_NAME = ".disabled";
private final File rootDir;
private final String storeName;
private final Map<Long, Boolean> versionToEnabledMap = Maps.newConcurrentMap();
private final VoldemortConfig config;
private long currentVersion;
/**
* This constructor inspects the rootDir of the store and finds out which
* versions exist and which one is active.
*
* @param rootDir of the store to be managed.
*/
public StoreVersionManager(File rootDir, VoldemortConfig config) {
this.rootDir = rootDir;
this.config = config;
this.storeName = rootDir.getName();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getSimpleName());
sb.append(" { currentVersion: ");
sb.append(currentVersion);
sb.append(", versionToEnabledMap: {");
boolean firstItem = true;
for (Map.Entry<Long, Boolean> entry: versionToEnabledMap.entrySet()) {
if (firstItem) {
firstItem = false;
} else {
sb.append(", ");
}
sb.append(entry.getKey());
sb.append(": ");
sb.append(entry.getValue());
}
sb.append("}, rootDir: ");
sb.append(rootDir);
sb.append(" }");
return sb.toString();
}
/**
* Compares the StoreVersionManager's internal state with the content on the file-system
* of the rootDir provided at construction time.
*
* TODO: If the StoreVersionManager supports non-RO stores in the future,
* we should move some of the ReadOnlyUtils functions below to another Utils class.
*/
public void syncInternalStateFromFileSystem(boolean alsoSyncRemoteState) {
// Make sure versions missing from the file-system are cleaned up from the internal state
for (Long version: versionToEnabledMap.keySet()) {
File[] existingVersionDirs = ReadOnlyUtils.getVersionDirs(rootDir, version, version);
if (existingVersionDirs.length == 0) {
removeVersion(version, alsoSyncRemoteState);
}
}
// Make sure we have all versions on the file-system in the internal state
File[] versionDirs = ReadOnlyUtils.getVersionDirs(rootDir);
if (versionDirs != null) {
for (File versionDir: versionDirs) {
long versionNumber = ReadOnlyUtils.getVersionId(versionDir);
boolean versionEnabled = isVersionEnabled(versionDir);
versionToEnabledMap.put(versionNumber, versionEnabled);
}
}
// Identify the current version (based on a symlink in the file-system)
File currentVersionDir = ReadOnlyUtils.getCurrentVersion(rootDir);
if (currentVersionDir != null) {
currentVersion = ReadOnlyUtils.getVersionId(currentVersionDir);
} else {
currentVersion = -1; // Should we throw instead?
}
logger.info("Successfully synced internal state from local file-system: " + this.toString());
}
/**
* Enables a specific store/version, so that it stops failing requests.
*
* @param version to be enabled
*/
public void enableStoreVersion(long version) throws PersistenceFailureException {
versionToEnabledMap.put(version, true);
persistEnabledVersion(version);
}
/**
* Disables a specific store/version. When disabled, a store should
* fail all subsequent requests to it.
*
* @param version to be disabled
*/
public void disableStoreVersion(long version) throws PersistenceFailureException {
versionToEnabledMap.put(version, false);
persistDisabledVersion(version);
}
/**
* Tells whether a version is enabled or disabled.
*
* @param version which we want to know the status of.
* @return true if the requested version is enabled,
* false if the requested version is disabled,
* null if the requested version does not exist
*/
public Boolean isStoreVersionEnabled(long version) {
return versionToEnabledMap.get(version);
}
/**
* Tells whether the current version is enabled or disabled.
*
* @return true if the requested version is enabled,
* false if the requested version is disabled,
* null if the requested version does not exist
*/
public Boolean isCurrentVersionEnabled() {
return isStoreVersionEnabled(currentVersion);
}
public long getCurrentVersion() {
return currentVersion;
}
public void setCurrentVersion(long currentVersion) {
this.currentVersion = currentVersion;
}
public boolean hasAnyDisabledVersion() {
for (Boolean enabled: versionToEnabledMap.values()) {
if (!enabled) return true;
}
return false;
}
// PRIVATE UTILITY FUNCTIONS
/**
* Inspects the specified versionDir to see if it has been marked as disabled
* (via a .disabled file in the directory). If the file is absent, the store is
* assumed to be enabled.
*
* @param versionDir to inspect
* @return true if the specified version is enabled, false otherwise
* @throws IllegalArgumentException if the version does not exist
*/
private boolean isVersionEnabled(File versionDir) throws IllegalArgumentException {
if (!versionDir.exists()) {
throw new IllegalArgumentException("The versionDir " + versionDir.getName() + " does not exist.");
}
File[] relevantFile = versionDir.listFiles(new FileFilter() {
public boolean accept(File pathName) {
return pathName.getName().equals(DISABLED_MARKER_NAME);
}
});
return relevantFile.length == 0;
}
/**
* Places a disabled marker file in the directory of the specified version.
*
* @param version to disable
* @throws PersistenceFailureException if the marker file could not be created (can happen if
* the storage system has become read-only or is otherwise
* inaccessible).
*/
private void persistDisabledVersion(long version) throws PersistenceFailureException {
File disabledMarker = getDisabledMarkerFile(version);
try {
disabledMarker.createNewFile();
} catch (IOException e) {
throw new PersistenceFailureException("Failed to create the disabled marker at path: " +
disabledMarker.getAbsolutePath() + "\nThe store/version " +
"will remain disabled only until the next restart.", e);
}
}
/**
* Deletes the disabled marker file in the directory of the specified version.
*
* @param version to enable
* @throws PersistenceFailureException if the marker file could not be deleted (can happen if
* the storage system has become read-only or is otherwise
* inaccessible).
*/
private void persistEnabledVersion(long version) throws PersistenceFailureException {
File disabledMarker = getDisabledMarkerFile(version);
if (disabledMarker.exists()) {
if (!disabledMarker.delete()) {
throw new PersistenceFailureException("Failed to create the disabled marker at path: " +
disabledMarker.getAbsolutePath() + "\nThe store/version " +
"will remain enabled only until the next restart.");
}
}
}
/**
* Gets the '.disabled' file for a given version of this store. That file may or may not
* exist.
*
* @param version of the store for which to get the '.disabled' file.
* @return an instance of {@link File} pointing to the '.disabled' file.
* @throws PersistenceFailureException if the requested version cannot be found.
*/
private File getDisabledMarkerFile(long version) throws PersistenceFailureException {
File[] versionDirArray = ReadOnlyUtils.getVersionDirs(rootDir, version, version);
if (versionDirArray.length == 0) {
throw new PersistenceFailureException("getDisabledMarkerFile did not find the requested version directory" +
" on disk. Version: " + version + ", rootDir: " + rootDir);
}
File disabledMarkerFile = new File(versionDirArray[0], DISABLED_MARKER_NAME);
return disabledMarkerFile;
}
private void removeVersion(long version, boolean alsoSyncRemoteState) {
if (currentVersion == version) {
currentVersion = -1; // Should we throw instead?
}
versionToEnabledMap.remove(version);
if (alsoSyncRemoteState && config != null && config.getHighAvailabilityStateAutoCleanUp()) {
FailedFetchLock failedFetchLock = null;
try {
failedFetchLock = FailedFetchLock.getLock(config, new Props());
removeRemoteObsoleteState(failedFetchLock);
} catch (Exception e) {
logger.error("Failed to execute failedFetchLock.removeObsoleteStateForStore() for store " + storeName, e);
} finally {
IOUtils.closeQuietly(failedFetchLock);
}
}
}
public void removeRemoteObsoleteState(FailedFetchLock failedFetchLock) {
if (config != null && config.getHighAvailabilityStateAutoCleanUp()) {
try {
failedFetchLock.removeObsoleteStateForStore(config.getNodeId(), storeName, versionToEnabledMap);
} catch (Exception e) {
logger.error("Failed to execute failedFetchLock.removeObsoleteStateForStore() for store " + storeName, e);
}
logger.info("Successfully synced internal state with remote FailedFetchLock state for store " + storeName);
} else {
logger.debug("Will not attempt removeRemoteObsoleteState() because it is disabled. " +
"This can be enabled with " + VoldemortConfig.PUSH_HA_STATE_AUTO_CLEANUP + "=true");
}
}
}