package com.hubspot.baragon.service.managers; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Optional; import com.google.common.base.Strings; 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.data.BaragonAgentResponseDatastore; import com.hubspot.baragon.data.BaragonLoadBalancerDatastore; import com.hubspot.baragon.data.BaragonStateDatastore; import com.hubspot.baragon.models.AgentBatchResponseItem; import com.hubspot.baragon.models.AgentRequestType; import com.hubspot.baragon.models.AgentRequestsStatus; import com.hubspot.baragon.models.AgentResponse; import com.hubspot.baragon.models.AgentResponseId; import com.hubspot.baragon.models.BaragonAgentMetadata; import com.hubspot.baragon.models.BaragonGroup; import com.hubspot.baragon.models.BaragonRequest; import com.hubspot.baragon.models.BaragonRequestBatchItem; import com.hubspot.baragon.models.BaragonService; import com.hubspot.baragon.models.InternalRequestStates; import com.hubspot.baragon.models.InternalStatesMap; import com.hubspot.baragon.models.QueuedRequestWithState; import com.hubspot.baragon.models.RequestAction; import com.hubspot.baragon.service.BaragonServiceModule; import com.hubspot.baragon.service.config.BaragonConfiguration; import com.ning.http.client.AsyncCompletionHandler; import com.ning.http.client.AsyncHttpClient; import com.ning.http.client.AsyncHttpClient.BoundRequestBuilder; import com.ning.http.client.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Singleton public class AgentManager { private static final Logger LOG = LoggerFactory.getLogger(AgentManager.class); private final BaragonLoadBalancerDatastore loadBalancerDatastore; private final BaragonStateDatastore stateDatastore; private final BaragonAgentResponseDatastore agentResponseDatastore; private final AsyncHttpClient asyncHttpClient; private final String baragonAgentRequestUriFormat; private final String baragonAgentBatchRequestUriFormat; private final Integer baragonAgentMaxAttempts; private final Optional<String> baragonAuthKey; private final Long baragonAgentRequestTimeout; private final BaragonConfiguration configuration; private final ObjectMapper objectMapper; @Inject public AgentManager(BaragonLoadBalancerDatastore loadBalancerDatastore, BaragonStateDatastore stateDatastore, BaragonAgentResponseDatastore agentResponseDatastore, BaragonConfiguration configuration, ObjectMapper objectMapper, @Named(BaragonServiceModule.BARAGON_SERVICE_HTTP_CLIENT) AsyncHttpClient asyncHttpClient, @Named(BaragonDataModule.BARAGON_AGENT_REQUEST_URI_FORMAT) String baragonAgentRequestUriFormat, @Named(BaragonDataModule.BARAGON_AGENT_BATCH_REQUEST_URI_FORMAT) String baragonAgentBatchRequestUriFormat, @Named(BaragonDataModule.BARAGON_AGENT_MAX_ATTEMPTS) Integer baragonAgentMaxAttempts, @Named(BaragonDataModule.BARAGON_AUTH_KEY) Optional<String> baragonAuthKey, @Named(BaragonDataModule.BARAGON_AGENT_REQUEST_TIMEOUT_MS) Long baragonAgentRequestTimeout) { this.loadBalancerDatastore = loadBalancerDatastore; this.stateDatastore = stateDatastore; this.agentResponseDatastore = agentResponseDatastore; this.configuration = configuration; this.objectMapper = objectMapper; this.asyncHttpClient = asyncHttpClient; this.baragonAgentRequestUriFormat = baragonAgentRequestUriFormat; this.baragonAgentBatchRequestUriFormat = baragonAgentBatchRequestUriFormat; this.baragonAgentMaxAttempts = baragonAgentMaxAttempts; this.baragonAuthKey = baragonAuthKey; this.baragonAgentRequestTimeout = baragonAgentRequestTimeout; } private AsyncHttpClient.BoundRequestBuilder buildAgentRequest(String url, AgentRequestType requestType) { final BoundRequestBuilder builder; switch (requestType) { case APPLY: builder = asyncHttpClient.preparePost(url); break; case REVERT: case CANCEL: builder = asyncHttpClient.prepareDelete(url); break; default: throw new RuntimeException("Don't know how to send requests for " + requestType); } if (baragonAuthKey.isPresent()) { builder.addQueryParameter("authkey", baragonAuthKey.get()); } return builder; } private AsyncHttpClient.BoundRequestBuilder buildAgentBatchRequest(String url, Set<BaragonRequestBatchItem> batch) throws JsonProcessingException { final BoundRequestBuilder builder = asyncHttpClient.preparePost(url); if (baragonAuthKey.isPresent()) { builder.addQueryParameter("authkey", baragonAuthKey.get()); } builder.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); builder.setBody(objectMapper.writeValueAsBytes(batch)); return builder; } public Map<QueuedRequestWithState, InternalRequestStates> sendRequests(final Set<QueuedRequestWithState> queuedRequestsWithState) { Map<QueuedRequestWithState, InternalRequestStates> results = new HashMap<>(); Map<String, Set<BaragonRequestBatchItem>> requestsByGroup = new HashMap<>(); for (QueuedRequestWithState queuedRequestWithState : queuedRequestsWithState) { final BaragonRequest request = queuedRequestWithState.getRequest(); final Optional<BaragonService> maybeOriginalService = stateDatastore.getService(request.getLoadBalancerService().getServiceId()); final Set<String> loadBalancerGroupsToUpdate = Sets.newHashSet(request.getLoadBalancerService().getLoadBalancerGroups()); if (maybeOriginalService.isPresent()) { loadBalancerGroupsToUpdate.addAll(maybeOriginalService.get().getLoadBalancerGroups()); } for (String group : loadBalancerGroupsToUpdate) { if (requestsByGroup.containsKey(group)) { requestsByGroup.get(group).add(new BaragonRequestBatchItem(request.getLoadBalancerRequestId(), request.getAction(), InternalStatesMap.getRequestType(queuedRequestWithState.getCurrentState()))); } else { requestsByGroup.put(group, Sets.newHashSet(new BaragonRequestBatchItem(request.getLoadBalancerRequestId(), request.getAction(), InternalStatesMap.getRequestType(queuedRequestWithState.getCurrentState())))); } } results.put(queuedRequestWithState, InternalStatesMap.getWaitingState(queuedRequestWithState.getCurrentState())); } for (Map.Entry<String, Set<BaragonRequestBatchItem>> entry : requestsByGroup.entrySet()) { for (final BaragonAgentMetadata agentMetadata : loadBalancerDatastore.getAgentMetadata(entry.getKey())) { final String baseUrl = agentMetadata.getBaseAgentUri(); if (agentMetadata.isBatchEnabled()) { sendBatchRequest(baseUrl, entry.getValue()); } else { for (BaragonRequestBatchItem batchItem : entry.getValue()) { sendIndividualRequest(baseUrl, batchItem.getRequestId(), batchItem.getRequestType()); } } } } return results; } private void sendBatchRequest(final String baseUrl, final Set<BaragonRequestBatchItem> originalBatch) { final Set<BaragonRequestBatchItem> batch = Sets.newHashSet(originalBatch); Set<BaragonRequestBatchItem> doNotSend = Sets.newHashSet(); for (BaragonRequestBatchItem item : batch) { if (!shouldSendRequest(baseUrl, item.getRequestId(), item.getRequestType())) { doNotSend.add(item); } else { agentResponseDatastore.setPendingRequestStatus(item.getRequestId(), baseUrl, true); } } batch.removeAll(doNotSend); final String url = String.format(baragonAgentBatchRequestUriFormat, baseUrl); final Set<String> handledRequestIds = Sets.newHashSet(); try { buildAgentBatchRequest(url, batch).execute(new AsyncCompletionHandler<Void>() { @Override public Void onCompleted(Response response) throws Exception { LOG.info("Got HTTP {} from {} for batch request {}", response.getStatusCode(), baseUrl, batch); if (response.getStatusCode() >= 300) { LOG.error("Received invalid response from agent (status: {}, response: {})", response.getStatusCode(), response.getResponseBody()); for (BaragonRequestBatchItem item : batch) { agentResponseDatastore.addAgentResponse(item.getRequestId(), item.getRequestType(), baseUrl, url, Optional.<Integer> absent(), Optional.<String> absent(), Optional.of(String.format("Caught exception processing agent response %s", response))); agentResponseDatastore.setPendingRequestStatus(item.getRequestId(), baseUrl, false); handledRequestIds.add(item.getRequestId()); } return null; } Set<AgentBatchResponseItem> responses = objectMapper.readValue(response.getResponseBody(), new TypeReference<Set<AgentBatchResponseItem>>(){}); for (AgentBatchResponseItem agentResponse : responses) { agentResponseDatastore.addAgentResponse(agentResponse.getRequestId(), agentResponse.getRequestType(), baseUrl, url, Optional.of(agentResponse.getStatusCode()), agentResponse.getMessage(), Optional.<String>absent()); agentResponseDatastore.setPendingRequestStatus(agentResponse.getRequestId(), baseUrl, false); handledRequestIds.add(agentResponse.getRequestId()); } for (BaragonRequestBatchItem item : batch) { if (!handledRequestIds.contains(item.getRequestId())) { agentResponseDatastore.addAgentResponse(item.getRequestId(), item.getRequestType(), baseUrl, url, Optional.<Integer> absent(), Optional.<String> absent(), Optional.of(String.format("No response in batch for request %s", item.getRequestId()))); agentResponseDatastore.setPendingRequestStatus(item.getRequestId(), baseUrl, false); } } return null; } @Override public void onThrowable(Throwable t) { LOG.error("Got exception when hitting {} with batch request {}", baseUrl, batch, t); for (BaragonRequestBatchItem item : batch) { if (!handledRequestIds.contains(item.getRequestId())) { agentResponseDatastore.addAgentResponse(item.getRequestId(), item.getRequestType(), baseUrl, url, Optional.<Integer> absent(), Optional.<String> absent(), Optional.of(t.getMessage())); agentResponseDatastore.setPendingRequestStatus(item.getRequestId(), baseUrl, false); } } } }); } catch (Exception e) { LOG.info("Got exception {} when hitting {} with batch reqeust {}", e, baseUrl, batch); for (BaragonRequestBatchItem item : batch) { if (!handledRequestIds.contains(item.getRequestId())) { agentResponseDatastore.addAgentResponse(item.getRequestId(), item.getRequestType(), baseUrl, url, Optional.<Integer> absent(), Optional.<String> absent(), Optional.of(e.getMessage())); agentResponseDatastore.setPendingRequestStatus(item.getRequestId(), baseUrl, false); } } } } private boolean shouldSendRequest(String baseUrl, String requestId, AgentRequestType requestType) { Optional<Long> maybePendingRequest = agentResponseDatastore.getPendingRequest(requestId, baseUrl); if (maybePendingRequest.isPresent() && !((System.currentTimeMillis() - maybePendingRequest.get()) > baragonAgentRequestTimeout)) { LOG.info(String.format("Request has been processing for %s ms", (System.currentTimeMillis() - maybePendingRequest.get()))); return false; } final Optional<AgentResponseId> maybeLastResponseId = agentResponseDatastore.getLastAgentResponseId(requestId, requestType, baseUrl); // don't retry request if we've hit the max attempts, or the request was successful if (maybeLastResponseId.isPresent() && (maybeLastResponseId.get().getAttempt() > baragonAgentMaxAttempts || maybeLastResponseId.get().isSuccess())) { return false; } return true; } private void sendIndividualRequest(final String baseUrl, final String requestId, final AgentRequestType requestType) { if (!shouldSendRequest(baseUrl, requestId, requestType)) { return; } agentResponseDatastore.setPendingRequestStatus(requestId, baseUrl, true); final String url = String.format(baragonAgentRequestUriFormat, baseUrl, requestId); try { buildAgentRequest(url, requestType).execute(new AsyncCompletionHandler<Void>() { @Override public Void onCompleted(Response response) throws Exception { LOG.info(String.format("Got HTTP %d from %s for %s", response.getStatusCode(), baseUrl, requestId)); final Optional<String> content = Strings.isNullOrEmpty(response.getResponseBody()) ? Optional.<String>absent() : Optional.of(response.getResponseBody()); agentResponseDatastore.addAgentResponse(requestId, requestType, baseUrl, url, Optional.of(response.getStatusCode()), content, Optional.<String>absent()); agentResponseDatastore.setPendingRequestStatus(requestId, baseUrl, false); return null; } @Override public void onThrowable(Throwable t) { LOG.info(String.format("Got exception %s when hitting %s for %s", t, baseUrl, requestId)); agentResponseDatastore.addAgentResponse(requestId, requestType, baseUrl, url, Optional.<Integer>absent(), Optional.<String>absent(), Optional.of(t.getMessage())); agentResponseDatastore.setPendingRequestStatus(requestId, baseUrl, false); } }); } catch (Exception e) { LOG.info(String.format("Got exception %s when hitting %s for %s", e, baseUrl, requestId)); agentResponseDatastore.addAgentResponse(requestId, requestType, baseUrl, url, Optional.<Integer>absent(), Optional.<String>absent(), Optional.of(e.getMessage())); agentResponseDatastore.setPendingRequestStatus(requestId, baseUrl, false); } } public AgentRequestsStatus getRequestsStatus(BaragonRequest request, AgentRequestType requestType) { boolean success = true; RequestAction action = request.getAction().or(RequestAction.UPDATE); List<Boolean> missingTemplateExceptions = new ArrayList<>(); for (BaragonAgentMetadata agentMetadata : getAgents(request.getLoadBalancerService().getLoadBalancerGroups())) { final String baseUrl = agentMetadata.getBaseAgentUri(); Optional<Long> maybePendingRequestTime = agentResponseDatastore.getPendingRequest(request.getLoadBalancerRequestId(), baseUrl); if (maybePendingRequestTime.isPresent()) { if ((System.currentTimeMillis() - maybePendingRequestTime.get()) > baragonAgentRequestTimeout) { LOG.info("Request {} reached maximum pending request time", request.getLoadBalancerRequestId()); agentResponseDatastore.setPendingRequestStatus(request.getLoadBalancerRequestId(), baseUrl, false); return AgentRequestsStatus.FAILURE; } else { return AgentRequestsStatus.WAITING; } } final Optional<AgentResponseId> maybeAgentResponseId = agentResponseDatastore.getLastAgentResponseId(request.getLoadBalancerRequestId(), requestType, baseUrl); if (!maybeAgentResponseId.isPresent()) { return AgentRequestsStatus.RETRY; } Optional<AgentResponse> maybeLastResponse = agentResponseDatastore.getAgentResponse(request.getLoadBalancerRequestId(), requestType, maybeAgentResponseId.get(), baseUrl); boolean missingTemplate = hasMissingTemplate(maybeLastResponse); missingTemplateExceptions.add(missingTemplate); if (!missingTemplate) { final AgentResponseId agentResponseId = maybeAgentResponseId.get(); if ((agentResponseId.getAttempt() < baragonAgentMaxAttempts - 1) && !agentResponseId.isSuccess()) { return AgentRequestsStatus.RETRY; } else { success = success && agentResponseId.isSuccess(); } } } if (!missingTemplateExceptions.isEmpty() && allTrue(missingTemplateExceptions)) { return AgentRequestsStatus.INVALID_REQUEST_NOOP; } else if (success) { return AgentRequestsStatus.SUCCESS; } else { return action.equals(RequestAction.RELOAD) ? AgentRequestsStatus.INVALID_REQUEST_NOOP : AgentRequestsStatus.FAILURE; } } public Map<String, Collection<AgentResponse>> getAgentResponses(String requestId) { return agentResponseDatastore.getLastResponses(requestId); } public Collection<BaragonAgentMetadata> getAgents(Set<String> loadBalancerGroups) { return loadBalancerDatastore.getAgentMetadata(loadBalancerGroups); } public boolean invalidAgentCount(String loadBalancerGroup) { int agentCount = loadBalancerDatastore.getAgentMetadata(loadBalancerGroup).size(); int targetCount = loadBalancerDatastore.getTargetCount(loadBalancerGroup).or(configuration.getDefaultTargetAgentCount()); return (agentCount == 0 || (configuration.isEnforceTargetAgentCount() && agentCount < targetCount)); } public boolean hasMissingTemplate(Optional<AgentResponse> maybeLastResponse) { return maybeLastResponse.isPresent() && maybeLastResponse.get().getContent().isPresent() && maybeLastResponse.get().getContent().get().contains("MissingTemplateException"); } public boolean allTrue(List<Boolean> array) { for (boolean b : array) { if (!b) { return false; } } return true; } public Set<String> getAllDomainsForGroup(String group) { Optional<BaragonGroup> maybeGroup = loadBalancerDatastore.getLoadBalancerGroup(group); Set<String> domains = new HashSet<>(); if (maybeGroup.isPresent()) { domains.addAll(maybeGroup.get().getDomains()); if (maybeGroup.get().getDefaultDomain().isPresent()) { domains.add(maybeGroup.get().getDefaultDomain().get()); } } return domains; } }