/**
* 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 org.apache.aurora.scheduler.storage.backup;
import java.io.File;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import javax.inject.Inject;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Files;
import com.google.common.util.concurrent.Atomics;
import org.apache.aurora.codec.ThriftBinaryCodec;
import org.apache.aurora.codec.ThriftBinaryCodec.CodingException;
import org.apache.aurora.common.base.Command;
import org.apache.aurora.gen.storage.Snapshot;
import org.apache.aurora.scheduler.base.Query;
import org.apache.aurora.scheduler.storage.DistributedSnapshotStore;
import org.apache.aurora.scheduler.storage.Storage;
import org.apache.aurora.scheduler.storage.Storage.MutateWork.NoResult;
import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
import static java.util.Objects.requireNonNull;
/**
* A recovery mechanism that works with {@link StorageBackup} to provide a two-step storage
* recovery process.
*/
public interface Recovery {
/**
* List backups available for recovery.
*
* @return Available backup IDs.
*/
Set<String> listBackups();
/**
* Loads a backup in 'staging' so that it may be queried and modified prior to committing.
*
* @param backupName Name of the backup to load.
* @throws RecoveryException If the backup could not be found or loaded.
*/
void stage(String backupName) throws RecoveryException;
/**
* Queries a staged backup.
*
* @param query Builder of query to perform.
* @return Tasks matching the query.
* @throws RecoveryException If a backup is not staged, or could not be queried.
*/
Iterable<IScheduledTask> query(Query.Builder query) throws RecoveryException;
/**
* Deletes tasks from a staged backup.
*
* @param query Query builder for tasks to delete.
* @throws RecoveryException If a backup is not staged, or tasks could not be deleted.
*/
void deleteTasks(Query.Builder query) throws RecoveryException;
/**
* Unloads a staged backup.
*/
void unload();
/**
* Commits a staged backup the main storage system.
*
* @throws RecoveryException If a backup is not staged, or the commit failed.
*/
void commit() throws RecoveryException;
/**
* Thrown when a recovery operation could not be completed due to internal errors or improper
* invocation order.
*/
class RecoveryException extends RuntimeException {
public RecoveryException(String message) {
super(message);
}
public RecoveryException(String message, Throwable cause) {
super(message, cause);
}
}
class RecoveryImpl implements Recovery {
private final File backupDir;
private final Function<Snapshot, TemporaryStorage> tempStorageFactory;
private final AtomicReference<PendingRecovery> recovery;
private final Storage primaryStorage;
private final DistributedSnapshotStore distributedStore;
private final Command shutDownNow;
@Inject
RecoveryImpl(
File backupDir,
Function<Snapshot, TemporaryStorage> tempStorageFactory,
Storage primaryStorage,
DistributedSnapshotStore distributedStore,
Command shutDownNow) {
this.backupDir = requireNonNull(backupDir);
this.tempStorageFactory = requireNonNull(tempStorageFactory);
this.recovery = Atomics.newReference();
this.primaryStorage = requireNonNull(primaryStorage);
this.distributedStore = requireNonNull(distributedStore);
this.shutDownNow = requireNonNull(shutDownNow);
}
@Override
public Set<String> listBackups() {
return ImmutableSet.<String>builder().add(backupDir.list()).build();
}
@Override
public void stage(String backupName) throws RecoveryException {
File backupFile = new File(backupDir, backupName);
if (!backupFile.exists()) {
throw new RecoveryException("Backup " + backupName + " does not exist.");
}
Snapshot snapshot;
try {
snapshot = ThriftBinaryCodec.decode(Snapshot.class, Files.toByteArray(backupFile));
} catch (CodingException e) {
throw new RecoveryException("Failed to decode backup " + e, e);
} catch (IOException e) {
throw new RecoveryException("Failed to read backup " + e, e);
}
boolean applied =
recovery.compareAndSet(null, new PendingRecovery(tempStorageFactory.apply(snapshot)));
if (!applied) {
throw new RecoveryException("Another backup is already loaded.");
}
}
private PendingRecovery getLoadedRecovery() throws RecoveryException {
@Nullable PendingRecovery loaded = this.recovery.get();
if (loaded == null) {
throw new RecoveryException("No backup loaded.");
}
return loaded;
}
@Override
public Iterable<IScheduledTask> query(Query.Builder query) throws RecoveryException {
return getLoadedRecovery().query(query);
}
@Override
public void deleteTasks(Query.Builder query) throws RecoveryException {
getLoadedRecovery().delete(query);
}
@Override
public void unload() {
recovery.set(null);
}
@Override
public void commit() throws RecoveryException {
getLoadedRecovery().commit();
}
private class PendingRecovery {
private final TemporaryStorage tempStorage;
PendingRecovery(TemporaryStorage tempStorage) {
this.tempStorage = tempStorage;
}
void commit() {
primaryStorage.write((NoResult.Quiet) storeProvider -> {
try {
distributedStore.persist(tempStorage.toSnapshot());
shutDownNow.execute();
} catch (CodingException e) {
throw new IllegalStateException("Failed to encode snapshot.", e);
}
});
}
Iterable<IScheduledTask> query(final Query.Builder query) {
return tempStorage.fetchTasks(query);
}
void delete(final Query.Builder query) {
tempStorage.deleteTasks(query);
}
}
}
}