/*
* 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.
*/
package com.addthis.hydra.job.backup;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import com.addthis.basis.util.JitterClock;
import com.addthis.basis.util.Parameter;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class represents types of backups that happen on time schedules (daily, weekly, etc.) and also gold backups.
* Its methods construct names for newly-created backups and decide whether to make new backups.
*/
public abstract class ScheduledBackupType {
private static final Date backupsValidAfterDate = new Date(1325419200000L); // Jan 1, 2012
private static final Map<String, ScheduledBackupType> backupTypesByDesc = ImmutableMap.of(new GoldBackup().getDescription(), new GoldBackup(), new HourlyBackup().getDescription(), new HourlyBackup(), new DailyBackup().getDescription(), new DailyBackup(), new WeeklyBackup().getDescription(), new WeeklyBackup(), new MonthlyBackup().getDescription(), new MonthlyBackup());
private static final long goldLifeTime = Parameter.longValue("backup.gold.life.time", 90 * 60 * 1000L);
private static final Map<ScheduledBackupType, Long> protectedBackupTypes = ImmutableMap.of((ScheduledBackupType) new GoldBackup(), goldLifeTime);
private static final Logger log = LoggerFactory.getLogger(ScheduledBackupType.class);
private static final Comparator<String> backupNameSorter = (o1, o2) -> {
// Sort backups going from most recent to earliest
return Long.compare(getBackupCreationTimeMillis(o2), getBackupCreationTimeMillis(o1));
};
protected static final String backupPrefix = "b-";
/**
* Generates the name that should be given to a backup being made "right now".
*
* @param includeSuffix Whether to include the time-sensitive suffix
* @return String name
*/
public String generateCurrentName(boolean includeSuffix) {
return generateNameForTime(JitterClock.globalTime(), includeSuffix);
}
/**
* Generates the name that would be given to a backup made at the given time, primarily for testing.
*
* @param time native time, in milliseconds
* @param includeSuffix Whether to include the time-sensitive suffix
* @return String name
*/
public String generateNameForTime(long time, boolean includeSuffix) {
return getPrefix() + getFormattedDateString(time) + (includeSuffix ? getSuffix(time) : "");
}
/**
* Turn the given time into a formatted string (e.g., YYMMDD)
*
* @param timeMillis native time, in milliseconds
* @return String formatted date
*/
protected abstract String getFormattedDateString(long timeMillis);
/**
* Parse the date string according to the backup type's formatter
*
* @param name A string representing a formatted date (e.g., 120415)
* @return Date the corresponding date
* @throws IllegalArgumentException if the input is not valid
*/
public abstract Date parseDateFromName(String name) throws IllegalArgumentException;
/**
* A static prefix that indicates the type of backup
*
* @return String prefix
*/
public String getPrefix() {
return backupPrefix;
}
public static String getBackupPrefix() {
return backupPrefix;
}
/**
* A dynamic suffix that should depend sensitively on the current time to avoid overwriting files
*
* @param time (native) time in millis
* @return String suffix
*/
public String getSuffix(long time) {
return "-" + Long.toHexString(time);
}
/**
* Should we create a symlink after completing the backup / what should it be called?
* Only used for gold backups at the moment.
*
* @return String a non-null name if the symlink should be created; null otherwise.
*/
public String getSymlinkName() {
return null;
}
/**
* Is this directory name a valid name for this kind of backup?
*
* @param name name to test
* @return True if valid
*/
public boolean isValidName(String name) {
String currentDailyBackupName = generateCurrentName(true);
if (name == null || !name.startsWith(getPrefix()) || name.length() != currentDailyBackupName.length()) {
return false;
}
try {
Date backupDate = parseDateFromName(name);
return backupDate.after(backupsValidAfterDate);
} catch (NumberFormatException nfe) {
return false;
} catch (IllegalArgumentException pe) {
return false;
} catch (ArrayIndexOutOfBoundsException aie) {
return false;
}
}
/**
* Strip off the prefix and suffix, leaving only the formatted-date part
*
* @param name input
* @return String stripped output
*/
public String stripSuffixAndPrefix(String name) {
return name.substring(getPrefix().length(), name.length() - getSuffix(JitterClock.globalTime()).length());
}
/**
* Should we make a new backup of this type, given that we have these backups already?
*
* @param existingBackups A list of directory names
* @return True if we should backup
*/
public boolean shouldMakeNewBackup(String[] existingBackups) {
List<String> backupsOfThisType = getSortedListBackupsOfThisType(existingBackups);
if (backupsOfThisType.isEmpty()) {
return true;
} else {
String lastBackup = backupsOfThisType.get(backupsOfThisType.size() - 1);
return !lastBackup.startsWith(generateCurrentName(false));
}
}
/**
* Given that certain backups exist, which backups from this type should be deleted, if any?
*
* @param allBackups A list of directory names
* @param completeBackups A list of directories that have a backup.valid file
* @param max The max number of backups of this type that should exist
* @return A (possibly empty) list of backups that should be deleted.
*/
public List<String> oldBackupsToDelete(String[] allBackups, String[] completeBackups, int max) {
List<String> validBackupsOfThisType = getSortedListBackupsOfThisType(completeBackups);
List<String> allBackupsOfThisType = getSortedListBackupsOfThisType(allBackups);
List<String> rv = new ArrayList<>();
if (max < 0) // Indicator that we're not sure how many backups should be created
{
return rv;
}
for (String backup : allBackupsOfThisType) {
if (!validBackupsOfThisType.contains(backup)) {
rv.add(backup); // Delete invalid backups
}
}
int excess = validBackupsOfThisType.size() - max;
if (excess <= 0 || validBackupsOfThisType.isEmpty()) {
return rv;
}
if (protectedBackupTypes.containsKey(this)) {
String latestValidBackup = validBackupsOfThisType.get(validBackupsOfThisType.size() - 1);
try {
rv.addAll(validBackupsOfThisType.subList(0, excess));
} catch (IllegalArgumentException e) {
log.warn("Failed to parse date from backup with name " + latestValidBackup);
}
} else {
rv.addAll(validBackupsOfThisType.subList(0, excess));
}
return rv;
}
/**
* Given an array of arbitrary directory names, get the ones that are valid names of this type and sort them
*
* @param existingBackups input array
* @return List all valid names
*/
protected List<String> getSortedListBackupsOfThisType(String[] existingBackups) {
List<String> backupsBelongingToThisType = new ArrayList<>(existingBackups.length);
for (String str : existingBackups) {
if (isValidName(str)) {
backupsBelongingToThisType.add(str);
}
}
Collections.sort(backupsBelongingToThisType);
return backupsBelongingToThisType;
}
/**
* Backup types returning the same getDescription are considered equal.
*/
@Override
public boolean equals(Object o) {
return o instanceof ScheduledBackupType && ((ScheduledBackupType) o).getDescription().equals(getDescription());
}
@Override
public int hashCode() {
return getDescription().hashCode();
}
/**
* A description of what the type should be. Primarily used to distinguish between types.
*
* @return String description (e.g. "daily")
*/
public abstract String getDescription();
/**
* Get an array of the registered backup types. Used to maintain parallelism between minion behavior and testing.
*
* @return ScheduledBackupType[] array, one of each type that is used.
*/
public static Map<String, ScheduledBackupType> getBackupTypes() {
return backupTypesByDesc;
}
/**
* Get the backup types that need to be protected (not deleted) for a given time period
*
* @return The map from backup type to time period in milliseconds
*/
public static Map<ScheduledBackupType, Long> getProtectedBackupTypes() {
return protectedBackupTypes;
}
protected static long getBackupCreationTimeMillis(String name) {
String millis = name.substring(name.lastIndexOf("-") + 1);
try {
return Long.parseLong(millis, 16);
} catch (NumberFormatException nfe) {
return -1L;
}
}
@Override
public String toString() {
return getDescription();
}
public static void sortBackupsByTime(List<String> backups) {
Collections.sort(backups, backupNameSorter);
}
}