package com.hubspot.baragon.service.worker;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
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.BaragonDataModule;
import com.hubspot.baragon.models.AgentRequestType;
import com.hubspot.baragon.models.AgentRequestsStatus;
import com.hubspot.baragon.models.AgentResponse;
import com.hubspot.baragon.models.BaragonRequest;
import com.hubspot.baragon.models.BaragonService;
import com.hubspot.baragon.models.InternalRequestStates;
import com.hubspot.baragon.models.InternalStatesMap;
import com.hubspot.baragon.models.QueuedRequestId;
import com.hubspot.baragon.models.QueuedRequestWithState;
import com.hubspot.baragon.models.RequestAction;
import com.hubspot.baragon.service.config.BaragonConfiguration;
import com.hubspot.baragon.service.exceptions.BaragonExceptionNotifier;
import com.hubspot.baragon.service.managers.AgentManager;
import com.hubspot.baragon.service.managers.RequestManager;
import com.hubspot.baragon.utils.JavaUtils;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class BaragonRequestWorker implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(BaragonRequestWorker.class);
private final AgentManager agentManager;
private final RequestManager requestManager;
private final AtomicLong workerLastStartAt;
private final BaragonExceptionNotifier exceptionNotifier;
private final BaragonConfiguration configuration;
@Inject
public BaragonRequestWorker(AgentManager agentManager,
RequestManager requestManager,
BaragonExceptionNotifier exceptionNotifier,
BaragonConfiguration configuration,
@Named(BaragonDataModule.BARAGON_SERVICE_WORKER_LAST_START) AtomicLong workerLastStartAt) {
this.agentManager = agentManager;
this.requestManager = requestManager;
this.workerLastStartAt = workerLastStartAt;
this.exceptionNotifier = exceptionNotifier;
this.configuration = configuration;
}
private String buildResponseString(Map<String, Collection<AgentResponse>> agentResponses, AgentRequestType requestType) {
if (agentResponses.containsKey(requestType.name()) && !agentResponses.get(requestType.name()).isEmpty()) {
Set<String> messages = new HashSet<>();
for (AgentResponse response : agentResponses.get(requestType.name())) {
if (response.toRequestStatus() == AgentRequestsStatus.FAILURE || response.toRequestStatus() == AgentRequestsStatus.INVALID_REQUEST_NOOP) {
messages.add(String.format("(%s) - %s", response.getStatusCode().or(0), response.getContent().or(response.getException()).or("")));
} else {
messages.add(String.format("%s - %s", response.getUrl(), response.toRequestStatus().name()));
}
}
return JavaUtils.COMMA_JOINER.join(messages);
} else {
return "No agent responses";
}
}
private InternalRequestStates handleCheckRevertResponse(BaragonRequest request, InternalRequestStates currentState) {
final Map<String, Collection<AgentResponse>> agentResponses;
switch (agentManager.getRequestsStatus(request, InternalStatesMap.getRequestType(currentState))) {
case FAILURE:
agentResponses = agentManager.getAgentResponses(request.getLoadBalancerRequestId());
requestManager.setRequestMessage(request.getLoadBalancerRequestId(), String.format("Apply failed {%s}, %s failed {%s}", buildResponseString(agentResponses, AgentRequestType.APPLY), InternalStatesMap.getRequestType(currentState).name(), buildResponseString(agentResponses, InternalStatesMap.getRequestType(currentState))));
return InternalStatesMap.getFailureState(currentState);
case SUCCESS:
agentResponses = agentManager.getAgentResponses(request.getLoadBalancerRequestId());
requestManager.setRequestMessage(request.getLoadBalancerRequestId(), String.format("Apply failed {%s}, %s OK.", buildResponseString(agentResponses, AgentRequestType.APPLY), InternalStatesMap.getRequestType(currentState).name()));
requestManager.revertBasePath(request);
return InternalStatesMap.getSuccessState(currentState);
case RETRY:
return InternalStatesMap.getRetryState(currentState);
default:
return InternalStatesMap.getWaitingState(currentState);
}
}
private InternalRequestStates handleState(InternalRequestStates currentState, BaragonRequest request) {
switch (currentState) {
case PENDING:
final Map<String, String> conflicts = requestManager.getBasePathConflicts(request);
if (!conflicts.isEmpty() && !(request.getAction().or(RequestAction.UPDATE) == RequestAction.DELETE)) {
requestManager.setRequestMessage(request.getLoadBalancerRequestId(), getBasePathConflictMessage(conflicts));
return InternalRequestStates.INVALID_REQUEST_NOOP;
}
final Set<String> missingGroups = requestManager.getMissingLoadBalancerGroups(request);
if (!missingGroups.isEmpty()) {
requestManager.setRequestMessage(request.getLoadBalancerRequestId(), String.format("Invalid request due to non-existent load balancer groups: %s", missingGroups));
return InternalRequestStates.INVALID_REQUEST_NOOP;
}
for (String loadBalancerGroup : request.getLoadBalancerService().getLoadBalancerGroups()) {
if (agentManager.invalidAgentCount(loadBalancerGroup)) {
requestManager.setRequestMessage(request.getLoadBalancerRequestId(), String.format("Invalid request due to not enough agents present for group: %s", loadBalancerGroup));
return InternalRequestStates.FAILED_REVERTED;
}
}
if (!request.getLoadBalancerService().getDomains().isEmpty()) {
List<String> domainsNotServed = getDomainsNotServed(request.getLoadBalancerService());
if (!domainsNotServed.isEmpty()) {
requestManager.setRequestMessage(request.getLoadBalancerRequestId(), String.format("No groups present that serve domains: %s", domainsNotServed));
return InternalRequestStates.INVALID_REQUEST_NOOP;
}
}
if (!(request.getAction().or(RequestAction.UPDATE) == RequestAction.DELETE)) {
requestManager.lockBasePaths(request);
}
return InternalRequestStates.SEND_APPLY_REQUESTS;
case CHECK_APPLY_RESPONSES:
switch (agentManager.getRequestsStatus(request, InternalStatesMap.getRequestType(currentState))) {
case FAILURE:
final Map<String, Collection<AgentResponse>> agentResponses = agentManager.getAgentResponses(request.getLoadBalancerRequestId());
requestManager.setRequestMessage(request.getLoadBalancerRequestId(), String.format("Apply failed (%s), reverting...", buildResponseString(agentResponses, InternalStatesMap.getRequestType(currentState))));
return InternalRequestStates.FAILED_SEND_REVERT_REQUESTS;
case SUCCESS:
try {
requestManager.setRequestMessage(request.getLoadBalancerRequestId(), String.format("%s request succeeded! Added upstreams: %s, Removed upstreams: %s", request.getAction().or(RequestAction.UPDATE), request.getAddUpstreams(), request.getRemoveUpstreams()));
requestManager.commitRequest(request);
return InternalRequestStates.COMPLETED;
} catch (KeeperException ke) {
String message = String.format("Caught zookeeper error for path %s.", ke.getPath());
LOG.error(message, ke);
requestManager.setRequestMessage(request.getLoadBalancerRequestId(), message + ke.getMessage());
return InternalRequestStates.FAILED_SEND_REVERT_REQUESTS;
} catch (Exception e) {
LOG.warn(String.format("Request %s was successful, but failed to commit!", request.getLoadBalancerRequestId()), e);
exceptionNotifier.notify(e, ImmutableMap.of("requestId", request.getLoadBalancerRequestId(), "serviceId", request.getLoadBalancerService().getServiceId()));
return InternalRequestStates.FAILED_SEND_REVERT_REQUESTS;
}
case RETRY:
return InternalRequestStates.SEND_APPLY_REQUESTS;
case INVALID_REQUEST_NOOP:
requestManager.revertBasePath(request);
return InternalRequestStates.INVALID_REQUEST_NOOP;
default:
return InternalRequestStates.CHECK_APPLY_RESPONSES;
}
case SEND_APPLY_REQUESTS:
case FAILED_SEND_REVERT_REQUESTS:
case CANCELLED_SEND_REVERT_REQUESTS:
throw new RuntimeException(String.format("Requests in state %s must be handled by batch request sender", currentState));
case FAILED_CHECK_REVERT_RESPONSES:
case CANCELLED_CHECK_REVERT_RESPONSES:
return handleCheckRevertResponse(request, currentState);
default:
return currentState;
}
}
private List<String> getDomainsNotServed(BaragonService service) {
List<String> notServed = new ArrayList<>(service.getDomains());
for (String group : service.getLoadBalancerGroups()) {
Set<String> domains = agentManager.getAllDomainsForGroup(group);
for (String domain : domains) {
notServed.remove(domain);
}
}
return notServed;
}
private String getBasePathConflictMessage(Map<String, String> conflicts) {
String message = "Invalid request due to base path conflicts: [";
for (Map.Entry<String, String> entry : conflicts.entrySet()) {
message = String.format("%s %s on group %s,", message, entry.getValue(), entry.getKey());
}
return message.substring(0, message.length() -1) + " ]";
}
public Map<QueuedRequestWithState, InternalRequestStates> handleQueuedRequests(Set<QueuedRequestWithState> queuedRequestsWithState) {
Map<QueuedRequestWithState, InternalRequestStates> results = new HashMap<>();
Set<QueuedRequestWithState> toApply = Sets.newHashSet();
for (QueuedRequestWithState queuedRequestWithState : queuedRequestsWithState) {
if (!queuedRequestWithState.getCurrentState().isRequireAgentRequest()) {
try {
results.put(queuedRequestWithState, handleState(queuedRequestWithState.getCurrentState(), queuedRequestWithState.getRequest()));
} catch (Exception e) {
LOG.error("Error processing request {}", queuedRequestWithState.getRequest().getLoadBalancerRequestId(), e);
}
} else {
if (toApply.size() < configuration.getWorkerConfiguration().getMaxBatchSize()) {
toApply.add(queuedRequestWithState);
}
}
}
results.putAll(agentManager.sendRequests(toApply));
return results;
}
@Override
public void run() {
workerLastStartAt.set(System.currentTimeMillis());
try {
final List<QueuedRequestId> queuedRequestIds = requestManager.getQueuedRequestIds();
if (!queuedRequestIds.isEmpty()) {
final Set<String> handledServices = Sets.newHashSet();
final Set<QueuedRequestWithState> queuedRequestsWithState = Sets.newHashSet();
for (QueuedRequestId queuedRequestId : queuedRequestIds) {
if (!handledServices.contains(queuedRequestId.getServiceId())) {
final String requestId = queuedRequestId.getRequestId();
final Optional<InternalRequestStates> maybeState = requestManager.getRequestState(requestId);
if (!maybeState.isPresent()) {
LOG.warn(String.format("%s does not have a request status!", requestId));
continue;
}
final Optional<BaragonRequest> maybeRequest = requestManager.getRequest(requestId);
if (!maybeRequest.isPresent()) {
LOG.warn(String.format("%s does not have a request object!", requestId));
continue;
}
queuedRequestsWithState.add(new QueuedRequestWithState(queuedRequestId, maybeRequest.get(), maybeState.get()));
handledServices.add(queuedRequestId.getServiceId());
}
}
Map<QueuedRequestWithState, InternalRequestStates> results = handleQueuedRequests(queuedRequestsWithState);
for (Map.Entry<QueuedRequestWithState, InternalRequestStates> result : results.entrySet()) {
if (result.getValue() != result.getKey().getCurrentState()) {
LOG.info(String.format("%s: %s --> %s", result.getKey().getQueuedRequestId().getRequestId(), result.getKey().getCurrentState(), result.getValue()));
requestManager.setRequestState(result.getKey().getQueuedRequestId().getRequestId(), result.getValue());
}
if (InternalStatesMap.isRemovable(result.getValue())) {
requestManager.removeQueuedRequest(result.getKey().getQueuedRequestId());
requestManager.saveResponseToHistory(result.getKey().getRequest(), result.getValue());
requestManager.deleteRequest(result.getKey().getQueuedRequestId().getRequestId());
}
}
}
} catch (Exception e) {
LOG.warn("Caught exception", e);
exceptionNotifier.notify(e, Collections.<String, String>emptyMap());
}
}
}