/*******************************************************************************
* Copyright (c) 2008, 2010 VMware Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware Inc. - initial contribution
*******************************************************************************/
package org.eclipse.virgo.kernel.deployer.core.internal.recovery;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.virgo.nano.deployer.api.core.DeploymentOptions;
import org.eclipse.virgo.nano.deployer.api.core.FatalDeploymentException;
import org.eclipse.virgo.util.io.PathReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link DeployerRecoveryLog} maintains the deployer's recoverable state across restarts.
* <p />
*
* <strong>Concurrent Semantics</strong><br />
*
* This class is thread safe.
*
*/
final class DeployerRecoveryLog {
private static final String REDEPLOY_FILE_NAME = "deployed";
private static final String REDEPLOY_COMPRESSION_FILE_NAME = "deployed.compress";
private static final int INITIAL_REDEPLOY_DATA_SIZE = 32 * 1024;
private static final int COMPRESSION_THRESHOLD = 10;
private static final int COMMAND_LENGTH = 3;
private static final String UNDEPLOY_URI_COMMAND = "---";
private static final String URI_SEPARATOR = ";";
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final PathReference redeployDataset;
private final PathReference redeployCompressionDataset;
private final long redeployFileLastModified;
DeployerRecoveryLog(PathReference workArea) {
PathReference recoveryArea = workArea.newChild("recovery");
recoveryArea.createDirectory();
this.redeployDataset = recoveryArea.newChild(REDEPLOY_FILE_NAME);
this.redeployFileLastModified = this.redeployDataset.toFile().lastModified();
this.redeployCompressionDataset = recoveryArea.newChild(REDEPLOY_COMPRESSION_FILE_NAME);
// Recover from a crash during compression
if (!this.redeployDataset.exists() && this.redeployCompressionDataset.exists()) {
this.redeployCompressionDataset.copy(this.redeployDataset);
if (!this.redeployCompressionDataset.delete()) {
logger.warn("Could not delete '%s' in recovery after compression failure.", this.redeployCompressionDataset);
}
}
}
/**
* Get the URIs that need to be recovered along with their deployment options.
*
* @return a map of URI to deployment options
*/
public Map<URI, DeploymentOptions> getRecoveryState() {
Map<URI, DeploymentOptions> redeploySet = new LinkedHashMap<URI, DeploymentOptions>(20);
String redeployData = readRedployData();
int recordCount = 0;
int undeployCount = 0;
for (String uriCommandString : redeployData.split(URI_SEPARATOR)) {
recordCount++;
// Skip short command strings as there will typically be one
// at the end of the dataset.
if (uriCommandString.length() >= COMMAND_LENGTH) {
String uriCommand = uriCommandString.substring(0, COMMAND_LENGTH);
String uriString = uriCommandString.substring(COMMAND_LENGTH);
try {
URI uri = new URI(uriString);
if (UNDEPLOY_URI_COMMAND.equals(uriCommand)) {
undeployCount++;
redeploySet.remove(uri);
} else {
char[] commands = uriCommand.toCharArray();
DeploymentOptions options = new DeploymentOptions(fromCommandOption(commands[0]), fromCommandOption(commands[1]),
fromCommandOption(commands[2]));
redeploySet.put(uri, options);
}
} catch (URISyntaxException e) {
logger.error("Invalid URI in command string '%s' read from redeploy dataset", e, uriCommandString);
// skip and carry on
}
}
}
// If there is a significant amount of wasted space in the redeploy
// dataset, rewrite it.
if (COMPRESSION_THRESHOLD * undeployCount > recordCount) {
rewriteRedeploySet(redeploySet);
}
return redeploySet;
}
private String readRedployData() {
StringBuffer redeployData = new StringBuffer(INITIAL_REDEPLOY_DATA_SIZE);
Reader redeployDataReader = null;
try {
redeployDataReader = new BufferedReader(new InputStreamReader(new FileInputStream(redeployDataset.toFile()), UTF_8));
try {
char[] chars = new char[INITIAL_REDEPLOY_DATA_SIZE];
int numRead;
while (-1 != (numRead = redeployDataReader.read(chars))) {
redeployData.append(String.valueOf(chars, 0, numRead));
}
} catch (IOException e) {
logger.error("Problem reading redeploy dataset", e);
} finally {
try {
redeployDataReader.close();
} catch (IOException e) {
logger.error("Problem closing redeploy dataset", e);
}
}
} catch (FileNotFoundException e) {
// Ignore - this is acceptable if there are no deployed applications
}
return redeployData.toString();
}
/**
* Write the given set of URIs to the redeploy dataset. To avoid corruption if a crash occurs, write to a redeploy
* compression file and then switch this for the redeploy dataset.
*
* @param redeploySet the URIs to be written
*/
private void rewriteRedeploySet(Map<URI, DeploymentOptions> redeploySet) {
this.redeployCompressionDataset.delete();
try (Writer redeployDataWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(this.redeployCompressionDataset.toFile()),
UTF_8))) {
for (Entry<URI, DeploymentOptions> redeployEntry : redeploySet.entrySet()) {
recordUriCommand(redeployDataWriter, redeployEntry.getKey(), getCommandString(redeployEntry.getValue()));
}
redeployDataWriter.close();
} catch (IOException e) {
logger.warn("Problem while rewriting redeploy dataset", e);
// Return without replacing the redeploy dataset.
}
// Now switch the files
this.redeployDataset.delete();
this.redeployCompressionDataset.moveTo(this.redeployDataset);
}
/**
* Add the given location and deployment options to the recovery state.
*
* @param location
* @param deploymentOptions
*/
void add(URI location, DeploymentOptions deploymentOptions) {
recordUriCommand(location, getCommandString(deploymentOptions));
}
/**
* Return the command string for tagging the type of a log record based on the given deployment options.
*
* @param deploymentOptions
* @return the command string
*/
private String getCommandString(DeploymentOptions deploymentOptions) {
// boolean recoverable, boolean deployerOwned, boolean synchronous
StringBuilder command = new StringBuilder().append(toCommandOption(deploymentOptions.getRecoverable())).append(
toCommandOption(deploymentOptions.getDeployerOwned())).append(toCommandOption(deploymentOptions.getSynchronous()));
return command.toString();
}
/**
* Remove the given location and associated deployment options from the recovery state.
*
* @param location
*/
void remove(URI location) {
recordUriCommand(location, UNDEPLOY_URI_COMMAND);
}
private void recordUriCommand(URI uri, String command) {
try (Writer writer = new FileWriter(this.redeployDataset.toFile(), true)) {
recordUriCommand(writer, uri, command);
writer.close();
} catch (IOException e) {
throw new FatalDeploymentException("Failed to record (un)deployment", e);
}
}
private static void recordUriCommand(Writer writer, URI uri, String command) throws IOException {
writer.write(command);
writer.write(uri.toString());
writer.write(URI_SEPARATOR);
}
/**
* Converts boolean deployment option flag to a string representation for the logged command option
*
* @param deploymentOption
* @return
*/
private static char toCommandOption(boolean deploymentOption) {
return deploymentOption ? 'Y' : 'N';
}
/**
* Converts from a String command option to a boolean deployment option flag
*
* @param commandOption
* @return
*/
private static boolean fromCommandOption(char commandOption) {
return 'Y' == commandOption ? true : false;
}
/**
* Get the last modified time of the deployer's recovery file. Any applications in the pickup directory with a later
* last modified time will need to be redeployed.
*
* @return the last modified time of the deployer's recovery file
*/
public long getRedeployFileLastModified() {
return redeployFileLastModified;
}
/**
* Update the last modified time of the deployer's recovery file.
*
* @return <code>true</code> iff the operation succeeded
*/
// TODO Make package private
public boolean setRedeployFileLastModified() {
return this.redeployDataset.touch();
}
}