package com.hubspot.baragon.agent.managers;
import com.google.common.base.Optional;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.hubspot.baragon.agent.BaragonAgentServiceModule;
import com.hubspot.baragon.agent.config.LoadBalancerConfiguration;
import com.hubspot.baragon.agent.config.TestingConfiguration;
import com.hubspot.baragon.agent.lbs.FilesystemConfigHelper;
import com.hubspot.baragon.data.BaragonRequestDatastore;
import com.hubspot.baragon.data.BaragonStateDatastore;
import com.hubspot.baragon.exceptions.LockTimeoutException;
import com.hubspot.baragon.exceptions.MissingTemplateException;
import com.hubspot.baragon.models.AgentBatchResponseItem;
import com.hubspot.baragon.models.BaragonAgentState;
import com.hubspot.baragon.models.BaragonRequest;
import com.hubspot.baragon.models.BaragonRequestBatchItem;
import com.hubspot.baragon.models.BaragonService;
import com.hubspot.baragon.models.RequestAction;
import com.hubspot.baragon.models.ServiceContext;
import com.hubspot.baragon.models.UpstreamInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import javax.ws.rs.core.Response;
@Singleton
public class AgentRequestManager {
private static final Logger LOG = LoggerFactory.getLogger(AgentRequestManager.class);
private final FilesystemConfigHelper configHelper;
private final BaragonStateDatastore stateDatastore;
private final BaragonRequestDatastore requestDatastore;
private final AtomicReference<String> mostRecentRequestId;
private final Optional<TestingConfiguration> maybeTestingConfiguration;
private final Random random;
private final AtomicReference<BaragonAgentState> agentState;
private final LoadBalancerConfiguration loadBalancerConfiguration;
private final long agentLockTimeoutMs;
@Inject
public AgentRequestManager(BaragonStateDatastore stateDatastore,
BaragonRequestDatastore requestDatastore,
FilesystemConfigHelper configHelper,
Optional<TestingConfiguration> maybeTestingConfiguration,
LoadBalancerConfiguration loadBalancerConfiguration,
Random random,
AtomicReference<BaragonAgentState> agentState,
@Named(BaragonAgentServiceModule.AGENT_MOST_RECENT_REQUEST_ID) AtomicReference<String> mostRecentRequestId,
@Named(BaragonAgentServiceModule.AGENT_LOCK_TIMEOUT_MS) long agentLockTimeoutMs) {
this.stateDatastore = stateDatastore;
this.configHelper = configHelper;
this.maybeTestingConfiguration = maybeTestingConfiguration;
this.requestDatastore = requestDatastore;
this.mostRecentRequestId = mostRecentRequestId;
this.random = random;
this.agentState = agentState;
this.loadBalancerConfiguration = loadBalancerConfiguration;
this.agentLockTimeoutMs = agentLockTimeoutMs;
}
public Set<AgentBatchResponseItem> processRequests(Set<BaragonRequestBatchItem> batch) throws InterruptedException {
Set<AgentBatchResponseItem> responses = Sets.newHashSet();
int i = 0;
for (BaragonRequestBatchItem item : batch) {
boolean isLast = i == batch.size() - 1;
responses.add(getResponseItem(processRequest(item.getRequestId(), actionForBatchItem(item), !isLast, Optional.of(i)), item));
i++;
}
return responses;
}
private AgentBatchResponseItem getResponseItem(Response httpResponse, BaragonRequestBatchItem item) {
Optional<String> maybeMessage = httpResponse.getEntity() != null ? Optional.of(httpResponse.getEntity().toString()) : Optional.<String>absent();
return new AgentBatchResponseItem(item.getRequestId(), httpResponse.getStatus(), maybeMessage, item.getRequestType());
}
private Optional<RequestAction> actionForBatchItem(BaragonRequestBatchItem item) {
switch (item.getRequestType()) {
case REVERT:
case CANCEL:
return Optional.of(RequestAction.REVERT);
case APPLY:
default:
if (item.getRequestAction().isPresent()) {
return item.getRequestAction();
} else {
return Optional.of(RequestAction.UPDATE);
}
}
}
public Response processRequest(String requestId, Optional<RequestAction> maybeAction, boolean delayReload, Optional<Integer> batchItemNumber) throws InterruptedException {
final Optional<BaragonRequest> maybeRequest = requestDatastore.getRequest(requestId);
if (!maybeRequest.isPresent()) {
return Response.status(Response.Status.NOT_FOUND).entity(String.format("Request %s does not exist", requestId)).build();
}
final BaragonRequest request = maybeRequest.get();
RequestAction action = maybeAction.or(request.getAction().or(RequestAction.UPDATE));
Optional<BaragonService> maybeOldService = getOldService(request);
try {
agentState.set(BaragonAgentState.APPLYING);
LOG.info(String.format("Received request to %s with id %s", action, requestId));
switch (action) {
case DELETE:
return delete(request, maybeOldService, delayReload);
case RELOAD:
return reload(request, delayReload);
case REVERT:
return revert(request, maybeOldService, delayReload, batchItemNumber);
default:
return apply(request, maybeOldService, delayReload, batchItemNumber);
}
} catch (LockTimeoutException e) {
LOG.warn(String.format("Couldn't acquire agent lock for %s in %s ms", requestId, agentLockTimeoutMs), e);
return Response.status(Response.Status.CONFLICT).entity(String.format("Couldn't acquire agent lock for %s in %s ms. Lock Info: %s", requestId, agentLockTimeoutMs, e.getLockInfo())).build();
} catch (Exception e) {
LOG.error(String.format("Caught exception while %sING for request %s", action, requestId), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(String.format("Caught exception while %sING for request %s: %s", action, requestId, e.getMessage())).build();
} finally {
LOG.info(String.format("Done processing %s request: %s", action, requestId));
agentState.set(BaragonAgentState.ACCEPTING);
}
}
private Response reload(BaragonRequest request, boolean delayReload) throws Exception {
if (!delayReload) {
configHelper.checkAndReload();
}
mostRecentRequestId.set(request.getLoadBalancerRequestId());
return Response.ok().build();
}
private Response delete(BaragonRequest request, Optional<BaragonService> maybeOldService, boolean delayReload) throws Exception {
try {
configHelper.delete(request.getLoadBalancerService(), maybeOldService, request.isNoReload(), request.isNoValidate(), delayReload);
mostRecentRequestId.set(request.getLoadBalancerRequestId());
return Response.ok().build();
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
}
}
private Response apply(BaragonRequest request, Optional<BaragonService> maybeOldService, boolean delayReload, Optional<Integer> batchItemNumber) throws Exception {
final ServiceContext update = getApplyContext(request);
triggerTesting();
try {
configHelper.apply(update, maybeOldService, true, request.isNoReload(), request.isNoValidate(), delayReload, batchItemNumber);
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
}
mostRecentRequestId.set(request.getLoadBalancerRequestId());
return Response.ok().build();
}
private Response revert(BaragonRequest request, Optional<BaragonService> maybeOldService, boolean delayReload, Optional<Integer> batchItemNumber) throws Exception {
final ServiceContext update;
if (movedOffLoadBalancer(maybeOldService)) {
update = new ServiceContext(request.getLoadBalancerService(), Collections.<UpstreamInfo>emptyList(), System.currentTimeMillis(), false);
} else {
update = new ServiceContext(maybeOldService.get(), stateDatastore.getUpstreams(maybeOldService.get().getServiceId()), System.currentTimeMillis(), true);
}
triggerTesting();
LOG.info(String.format("Reverting to %s", update));
try {
configHelper.apply(update, Optional.<BaragonService>absent(), false, request.isNoReload(), request.isNoValidate(), delayReload, batchItemNumber);
} catch (MissingTemplateException e) {
if (serviceDidNotPreviouslyExist(maybeOldService)) {
return Response.ok().build();
} else {
throw e;
}
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
}
return Response.ok().build();
}
private ServiceContext getApplyContext(BaragonRequest request) throws Exception {
if (movedOffLoadBalancer(request)) {
return new ServiceContext(request.getLoadBalancerService(), Collections.<UpstreamInfo>emptyList(), System.currentTimeMillis(), false);
} else if (!request.getReplaceUpstreams().isEmpty()) {
return new ServiceContext(request.getLoadBalancerService(), request.getReplaceUpstreams(), System.currentTimeMillis(), true);
} else {
List<UpstreamInfo> upstreams = new ArrayList<>();
upstreams.addAll(request.getAddUpstreams());
for (UpstreamInfo existingUpstream : stateDatastore.getUpstreams(request.getLoadBalancerService().getServiceId())) {
boolean present = false;
boolean toRemove = false;
for (UpstreamInfo currentUpstream : upstreams) {
if (UpstreamInfo.upstreamAndGroupMatches(currentUpstream, existingUpstream)) {
present = true;
break;
}
}
for (UpstreamInfo upstreamToRemove : request.getRemoveUpstreams()) {
if (UpstreamInfo.upstreamAndGroupMatches(upstreamToRemove, existingUpstream)) {
toRemove = true;
break;
}
}
if (!present && !toRemove) {
upstreams.add(existingUpstream);
}
}
return new ServiceContext(request.getLoadBalancerService(), upstreams, System.currentTimeMillis(), true);
}
}
private boolean movedOffLoadBalancer(Optional<BaragonService> maybeOldService) {
return (!maybeOldService.isPresent() || !maybeOldService.get().getLoadBalancerGroups().contains(loadBalancerConfiguration.getName()));
}
private boolean movedOffLoadBalancer(BaragonRequest request) {
return (!request.getLoadBalancerService().getLoadBalancerGroups().contains(loadBalancerConfiguration.getName()));
}
private boolean serviceDidNotPreviouslyExist(Optional<BaragonService> maybeOldService) {
return (!maybeOldService.isPresent() || !maybeOldService.get().getLoadBalancerGroups().contains(loadBalancerConfiguration.getName()));
}
private Optional<BaragonService> getOldService(BaragonRequest request) {
Optional<BaragonService> service = Optional.absent();
if (request.getReplaceServiceId().isPresent()) {
service = stateDatastore.getService(request.getReplaceServiceId().get());
}
if (service.isPresent()) {
return service;
} else {
return stateDatastore.getService(request.getLoadBalancerService().getServiceId());
}
}
private void triggerTesting() throws Exception {
if (maybeTestingConfiguration.isPresent() && maybeTestingConfiguration.get().isEnabled() && maybeTestingConfiguration.get().getApplyDelayMs() > 0) {
Thread.sleep(maybeTestingConfiguration.get().getApplyDelayMs());
}
if (maybeTestingConfiguration.isPresent() && maybeTestingConfiguration.get().isEnabled()) {
if (random.nextFloat() <= maybeTestingConfiguration.get().getApplyFailRate()) {
throw new Exception("Random testing failure");
}
}
}
}