package com.hubspot.singularity.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.utils.ZKPaths;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.codahale.metrics.MetricRegistry;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.inject.Inject;
import com.hubspot.singularity.SingularityAction;
import com.hubspot.singularity.SingularityCreateResult;
import com.hubspot.singularity.SingularityDeleteResult;
import com.hubspot.singularity.SingularityDisabledAction;
import com.hubspot.singularity.SingularityDisaster;
import com.hubspot.singularity.SingularityDisasterDataPoints;
import com.hubspot.singularity.SingularityDisasterType;
import com.hubspot.singularity.SingularityDisastersData;
import com.hubspot.singularity.SingularityUser;
import com.hubspot.singularity.config.SingularityConfiguration;
import com.hubspot.singularity.data.transcoders.StringTranscoder;
import com.hubspot.singularity.data.transcoders.Transcoder;
public class DisasterManager extends CuratorAsyncManager {
private static final Logger LOG = LoggerFactory.getLogger(DisasterManager.class);
private static final String DISASTERS_ROOT = "/disasters";
private static final String DISABLED_ACTIONS_PATH = DISASTERS_ROOT + "/disabled-actions";
private static final String ACTIVE_DISASTERS_PATH = DISASTERS_ROOT + "/active";
private static final String DISASTER_STATS_PATH = DISASTERS_ROOT + "/statistics";
private static final String DISABLE_AUTOMATED_PATH = DISASTERS_ROOT + "/disabled";
private static final String TASK_CREDITS_PATH = DISASTERS_ROOT + "/task-credits";
private static final String TASK_CREDITS_ENABLED_PATH = TASK_CREDITS_PATH + "/enabled";
private static final String TASK_CREDITS_UPDATES_PATH = TASK_CREDITS_PATH + "/updates";
private static final String TASK_CREDIT_COUNT_PATH = TASK_CREDITS_PATH + "/count";
private static final String MESSAGE_FORMAT = "Cannot perform action %s: %s";
private static final String DEFAULT_MESSAGE = "Action is currently disabled";
private final Transcoder<SingularityDisabledAction> disabledActionTranscoder;
private final Transcoder<SingularityDisasterDataPoints> disasterStatsTranscoder;
@Inject
public DisasterManager(CuratorFramework curator, SingularityConfiguration configuration, MetricRegistry metricRegistry,
Transcoder<SingularityDisabledAction> disabledActionTranscoder, Transcoder<SingularityDisasterDataPoints> disasterStatsTranscoder) {
super(curator, configuration, metricRegistry);
this.disabledActionTranscoder = disabledActionTranscoder;
this.disasterStatsTranscoder = disasterStatsTranscoder;
}
private String getActionPath(SingularityAction action) {
return ZKPaths.makePath(DISABLED_ACTIONS_PATH, action.name());
}
public boolean isDisabled(SingularityAction action) {
return exists(getActionPath(action));
}
public SingularityDisabledAction getDisabledAction(SingularityAction action) {
Optional<SingularityDisabledAction> maybeDisabledAction = getData(getActionPath(action), disabledActionTranscoder);
return maybeDisabledAction.or(new SingularityDisabledAction(action, String.format(MESSAGE_FORMAT, action, DEFAULT_MESSAGE), Optional.<String>absent(), false, Optional.<Long>absent()));
}
public SingularityCreateResult disable(SingularityAction action, Optional<String> maybeMessage, Optional<SingularityUser> user, boolean systemGenerated, Optional<Long> expiresAt) {
if (!action.isCanDisable()) {
throw new IllegalArgumentException(String.format("Action %s cannot be disabled", action));
}
SingularityDisabledAction disabledAction = new SingularityDisabledAction(
action,
String.format(MESSAGE_FORMAT, action, maybeMessage.or(DEFAULT_MESSAGE)),
user.isPresent() ? Optional.of(user.get().getId()) : Optional.<String>absent(),
systemGenerated,
expiresAt);
return save(getActionPath(action), disabledAction, disabledActionTranscoder);
}
public SingularityDeleteResult enable(SingularityAction action) {
return delete(getActionPath(action));
}
public List<SingularityDisabledAction> getDisabledActions() {
List<String> paths = new ArrayList<>();
for (String path : getChildren(DISABLED_ACTIONS_PATH)) {
paths.add(ZKPaths.makePath(DISABLED_ACTIONS_PATH, path));
}
return getAsync("getDisabledActions", paths, disabledActionTranscoder);
}
public void addDisaster(SingularityDisasterType disaster) {
create(ZKPaths.makePath(ACTIVE_DISASTERS_PATH, disaster.name()));
}
public void removeDisaster(SingularityDisasterType disaster) {
delete(ZKPaths.makePath(ACTIVE_DISASTERS_PATH, disaster.name()));
if (getActiveDisasters().isEmpty()) {
clearSystemGeneratedDisabledActions();
}
}
public boolean isDisasterActive(SingularityDisasterType disaster) {
return exists(ZKPaths.makePath(ACTIVE_DISASTERS_PATH, disaster.name()));
}
public List<SingularityDisasterType> getActiveDisasters() {
List<String> disasterNames = getChildren(ACTIVE_DISASTERS_PATH);
List<SingularityDisasterType> disasters = new ArrayList<>();
for (String name : disasterNames) {
disasters.add(SingularityDisasterType.valueOf(name));
}
return disasters;
}
public List<SingularityDisaster> getAllDisasterStates() {
return getAllDisasterStates(getActiveDisasters());
}
public List<SingularityDisaster> getAllDisasterStates(List<SingularityDisasterType> activeDisasters) {
List<SingularityDisaster> disasters = new ArrayList<>();
for (SingularityDisasterType type : SingularityDisasterType.values()) {
disasters.add(new SingularityDisaster(type, activeDisasters.contains(type)));
}
return disasters;
}
public void saveDisasterStats(SingularityDisasterDataPoints stats) {
save(DISASTER_STATS_PATH, stats, disasterStatsTranscoder);
}
public SingularityDisasterDataPoints getDisasterStats() {
SingularityDisasterDataPoints stats = getData(DISASTER_STATS_PATH, disasterStatsTranscoder).or(SingularityDisasterDataPoints.empty());
Collections.sort(stats.getDataPoints());
return stats;
}
public SingularityDisastersData getDisastersData() {
return new SingularityDisastersData(getDisasterStats().getDataPoints(), getAllDisasterStates(), isAutomatedDisabledActionsDisabled());
}
public void updateActiveDisasters(List<SingularityDisasterType> previouslyActiveDisasters, List<SingularityDisasterType> newActiveDisasters) {
for (SingularityDisasterType disaster : previouslyActiveDisasters) {
if (!newActiveDisasters.contains(disaster)) {
removeDisaster(disaster);
}
}
for (SingularityDisasterType disaster : newActiveDisasters) {
if (!isDisasterActive(disaster)) {
addDisaster(disaster);
}
}
}
public void addDisabledActionsForDisasters(List<SingularityDisasterType> newActiveDisasters) {
boolean automaticallyClearable = true;
for (SingularityDisasterType disasterType : newActiveDisasters) {
if (!disasterType.isAutomaticallyClearable()) {
automaticallyClearable = false;
break;
}
}
String message = String.format("Active disasters detected: (%s)%s", newActiveDisasters, automaticallyClearable ? "" : ", action must be re-enabled by an admin user");
Optional<Long> expiresAt = Optional.absent();
if (automaticallyClearable) {
expiresAt = Optional.of(System.currentTimeMillis() + configuration.getDisasterDetection().getDefaultDisabledActionExpiration());
}
for (SingularityAction action : configuration.getDisasterDetection().getDisableActionsOnDisaster()) {
disable(action, Optional.of(message), Optional.<SingularityUser>absent(), automaticallyClearable, expiresAt);
}
}
public void clearSystemGeneratedDisabledActions() {
for (SingularityDisabledAction disabledAction : getDisabledActions()) {
if (disabledAction.isAutomaticallyClearable()) {
enable(disabledAction.getType());
}
}
}
public void disableAutomatedDisabledActions() {
create(DISABLE_AUTOMATED_PATH);
}
public void enableAutomatedDisabledActions() {
delete(DISABLE_AUTOMATED_PATH);
}
public boolean isAutomatedDisabledActionsDisabled() {
return exists(DISABLE_AUTOMATED_PATH);
}
public boolean isTaskCreditEnabled() {
return exists(TASK_CREDITS_ENABLED_PATH);
}
public void enableTaskCredits() {
create(TASK_CREDITS_ENABLED_PATH);
}
public void disableTaskCredits() {
delete(TASK_CREDITS_ENABLED_PATH);
}
public void enqueueCreditsChange(int credits) {
save(ZKPaths.makePath(TASK_CREDITS_UPDATES_PATH, UUID.randomUUID().toString()), Optional.of(Integer.toString(credits).getBytes(Charsets.UTF_8)));
}
public int getTaskCredits() {
Optional<String> data = getData(TASK_CREDIT_COUNT_PATH, StringTranscoder.INSTANCE);
try {
if (data.isPresent()) {
return Integer.parseInt(data.get());
} else {
return 0;
}
} catch (NumberFormatException nfe) {
LOG.error("Could not read integer from path {}", TASK_CREDIT_COUNT_PATH, nfe);
return 0;
}
}
public void saveTaskCreditCount(int remaining) {
save(TASK_CREDIT_COUNT_PATH, Optional.of(Integer.toString(remaining).getBytes(Charsets.UTF_8)));
}
public int getUpdatedCreditCount() {
int currentCredits = getTaskCredits();
List<String> updates = getChildren(TASK_CREDITS_UPDATES_PATH);
if (updates.isEmpty()) {
return currentCredits;
}
for (String update : updates) {
String path = ZKPaths.makePath(TASK_CREDITS_UPDATES_PATH, update);
try {
Optional<String> data = getData(path, StringTranscoder.INSTANCE);
if (data.isPresent()) {
currentCredits = currentCredits + Integer.parseInt(data.get());
}
} catch (NumberFormatException nfe) {
LOG.error("Could not read integer from path {}", path, nfe);
} finally {
delete(path);
}
}
currentCredits = Math.max(currentCredits, 0);
saveTaskCreditCount(currentCredits);
return currentCredits;
}
}