/** * 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.http; import java.io.Closeable; import java.io.IOException; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.net.HostAndPort; import org.apache.aurora.common.thrift.Endpoint; import org.apache.aurora.common.thrift.ServiceInstance; import org.apache.aurora.scheduler.app.ServiceGroupMonitor; import org.apache.aurora.scheduler.app.ServiceGroupMonitor.MonitorException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.Objects.requireNonNull; /** * Redirect logic for finding the leading scheduler in the event that this process is not the * leader. */ class LeaderRedirect implements Closeable { enum LeaderStatus { /** * This instance is currently the leading scheduler. */ LEADING, /** * There is not currently an elected leading scheduler. */ NO_LEADER, /** * This instance is not currently the leading scheduler. */ NOT_LEADING, } private static final Logger LOG = LoggerFactory.getLogger(LeaderRedirect.class); private final HttpService httpService; private final ServiceGroupMonitor serviceGroupMonitor; @Inject LeaderRedirect(HttpService httpService, ServiceGroupMonitor serviceGroupMonitor) { this.httpService = requireNonNull(httpService); this.serviceGroupMonitor = requireNonNull(serviceGroupMonitor); } /** * Initiates the monitor that will watch the scheduler host set. * * @throws MonitorException If monitoring failed to initialize. */ public void monitor() throws MonitorException { serviceGroupMonitor.start(); } @Override public void close() throws IOException { serviceGroupMonitor.close(); } private Optional<HostAndPort> getLeaderHttp() { Optional<ServiceInstance> leadingScheduler = getLeader(); if (leadingScheduler.isPresent() && leadingScheduler.get().isSetServiceEndpoint()) { Endpoint leaderHttp = leadingScheduler.get().getServiceEndpoint(); if (leaderHttp != null && leaderHttp.isSetHost() && leaderHttp.isSetPort()) { return Optional.of(HostAndPort.fromParts(leaderHttp.getHost(), leaderHttp.getPort())); } } LOG.warn("Leader service instance seems to be incomplete: " + leadingScheduler); return Optional.absent(); } private Optional<HostAndPort> getLocalHttp() { HostAndPort localHttp = httpService.getAddress(); return (localHttp == null) ? Optional.absent() : Optional.of(HostAndPort.fromParts(localHttp.getHost(), localHttp.getPort())); } /** * Gets the optional HTTP endpoint that should be redirected to in the event that this * scheduler is not the leader. * * @return Optional redirect target. */ @VisibleForTesting Optional<HostAndPort> getRedirect() { Optional<HostAndPort> leaderHttp = getLeaderHttp(); Optional<HostAndPort> localHttp = getLocalHttp(); if (leaderHttp.isPresent()) { if (leaderHttp.equals(localHttp)) { return Optional.absent(); } else { return leaderHttp; } } else { LOG.info("No leader found, not redirecting."); return Optional.absent(); } } /** * Gets the current status of the elected leader. * * @return a {@code LeaderStatus} indicating whether there is an elected leader (and if so, if * this instance is the leader). */ LeaderStatus getLeaderStatus() { Optional<ServiceInstance> leadingScheduler = getLeader(); if (!leadingScheduler.isPresent()) { return LeaderStatus.NO_LEADER; } if (!leadingScheduler.get().isSetServiceEndpoint()) { LOG.warn("Leader service instance seems to be incomplete: " + leadingScheduler); return LeaderStatus.NO_LEADER; } Optional<HostAndPort> leaderHttp = getLeaderHttp(); Optional<HostAndPort> localHttp = getLocalHttp(); if (leaderHttp.isPresent() && leaderHttp.equals(localHttp)) { return LeaderStatus.LEADING; } return LeaderStatus.NOT_LEADING; } /** * Gets the optional redirect URI target in the event that this process is not the leading * scheduler. * * @param req HTTP request. * @return An optional redirect destination to route the request to the leading scheduler. */ Optional<String> getRedirectTarget(HttpServletRequest req) { Optional<HostAndPort> redirectTarget = getRedirect(); if (redirectTarget.isPresent()) { HostAndPort target = redirectTarget.get(); StringBuilder redirect = new StringBuilder() .append(req.getScheme()) .append("://") .append(target.getHost()) .append(':') .append(target.getPort()) .append( // If Jetty rewrote the path, we want to be sure to redirect to the original path // rather than the rewritten path to be sure it's a route the UI code recognizes. Optional.fromNullable( req.getAttribute(JettyServerModule.ORIGINAL_PATH_ATTRIBUTE_NAME)) .or(req.getRequestURI())); String queryString = req.getQueryString(); if (queryString != null) { redirect.append('?').append(queryString); } return Optional.of(redirect.toString()); } else { return Optional.absent(); } } private Optional<ServiceInstance> getLeader() { ImmutableSet<ServiceInstance> hostSet = serviceGroupMonitor.get(); switch (hostSet.size()) { case 0: LOG.warn("No serviceGroupMonitor in host set, will not redirect despite not being leader."); return Optional.absent(); case 1: LOG.debug("Found leader scheduler at {}", hostSet); return Optional.of(Iterables.getOnlyElement(hostSet)); default: LOG.error("Multiple serviceGroupMonitor detected, will not redirect: {}", hostSet); return Optional.absent(); } } }