package com.netflix.discovery; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.netflix.appinfo.AbstractEurekaIdentity; import com.netflix.appinfo.DataCenterInfo; import com.netflix.appinfo.EurekaClientIdentity; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.converters.XmlXStream; import com.netflix.discovery.shared.Application; import com.netflix.discovery.shared.Applications; import org.junit.Assert; import org.junit.rules.ExternalResource; import org.mortbay.jetty.Request; import org.mortbay.jetty.Server; import org.mortbay.jetty.handler.AbstractHandler; /** * @author Nitesh Kant */ public class MockRemoteEurekaServer extends ExternalResource { public static final String EUREKA_API_BASE_PATH = "/eureka/v2/"; private static Pattern HOSTNAME_PATTERN = Pattern.compile("\"hostName\"\\s?:\\s?\\\"([A-Za-z0-9\\.-]*)\\\""); private static Pattern STATUS_PATTERN = Pattern.compile("\"status\"\\s?:\\s?\\\"([A-Z_]*)\\\""); private int port; private final Map<String, Application> applicationMap = new HashMap<String, Application>(); private final Map<String, Application> remoteRegionApps = new HashMap<String, Application>(); private final Map<String, Application> remoteRegionAppsDelta = new HashMap<String, Application>(); private final Map<String, Application> applicationDeltaMap = new HashMap<String, Application>(); private Server server; private final AtomicBoolean sentDelta = new AtomicBoolean(); private final AtomicBoolean sentRegistry = new AtomicBoolean(); public final List<String> registrationStatuses = new ArrayList<String>(); public final AtomicLong registerCount = new AtomicLong(0); public final AtomicLong heartbeatCount = new AtomicLong(0); public final AtomicLong getFullRegistryCount = new AtomicLong(0); public final AtomicLong getSingleVipCount = new AtomicLong(0); public final AtomicLong getDeltaCount = new AtomicLong(0); @Override protected void before() throws Throwable { start(); } @Override protected void after() { try { stop(); } catch (Exception e) { Assert.fail(e.getMessage()); } } public void start() throws Exception { server = new Server(port); server.setHandler(new AppsResourceHandler()); server.start(); port = server.getConnectors()[0].getLocalPort(); } public int getPort() { return port; } public void stop() throws Exception { server.stop(); server = null; port = 0; registrationStatuses.clear(); applicationMap.clear(); remoteRegionApps.clear(); remoteRegionAppsDelta.clear(); applicationDeltaMap.clear(); } public boolean isSentDelta() { return sentDelta.get(); } public boolean isSentRegistry() { return sentRegistry.get(); } public void addRemoteRegionApps(String appName, Application app) { remoteRegionApps.put(appName, app); } public void addRemoteRegionAppsDelta(String appName, Application app) { remoteRegionAppsDelta.put(appName, app); } public void addLocalRegionApps(String appName, Application app) { applicationMap.put(appName, app); } public void addLocalRegionAppsDelta(String appName, Application app) { applicationDeltaMap.put(appName, app); } public void waitForDeltaToBeRetrieved(int refreshRate) throws InterruptedException { int count = 0; while (count++ < 3 && !isSentDelta()) { System.out.println("Sleeping for " + refreshRate + " seconds to let the remote registry fetch delta. Attempt: " + count); Thread.sleep(3 * refreshRate * 1000); System.out.println("Done sleeping for 10 seconds to let the remote registry fetch delta. Delta fetched: " + isSentDelta()); } System.out.println("Sleeping for extra " + refreshRate + " seconds for the client to update delta in memory."); } // // A base default resource handler for the mock server // private class AppsResourceHandler extends AbstractHandler { @Override public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException, ServletException { String authName = request.getHeader(AbstractEurekaIdentity.AUTH_NAME_HEADER_KEY); String authVersion = request.getHeader(AbstractEurekaIdentity.AUTH_VERSION_HEADER_KEY); String authId = request.getHeader(AbstractEurekaIdentity.AUTH_ID_HEADER_KEY); Assert.assertEquals(EurekaClientIdentity.DEFAULT_CLIENT_NAME, authName); Assert.assertNotNull(authVersion); Assert.assertNotNull(authId); String pathInfo = request.getPathInfo(); System.out.println("Eureka port: " + port + ". " + System.currentTimeMillis() + ". Eureka resource mock, received request on path: " + pathInfo + ". HTTP method: |" + request.getMethod() + '|' + ", query string: " + request.getQueryString()); boolean handled = false; if (null != pathInfo && pathInfo.startsWith("")) { pathInfo = pathInfo.substring(EUREKA_API_BASE_PATH.length()); boolean includeRemote = isRemoteRequest(request); if (pathInfo.startsWith("apps/delta")) { getDeltaCount.getAndIncrement(); Applications apps = new Applications(); apps.setVersion(100L); if (sentDelta.compareAndSet(false, true)) { addDeltaApps(includeRemote, apps); } else { System.out.println("Eureka port: " + port + ". " + System.currentTimeMillis() + ". Not including delta as it has already been sent."); } apps.setAppsHashCode(getDeltaAppsHashCode(includeRemote)); sendOkResponseWithContent((Request) request, response, apps); handled = true; } else if (pathInfo.equals("apps/")) { getFullRegistryCount.getAndIncrement(); Applications apps = new Applications(); apps.setVersion(100L); for (Application application : applicationMap.values()) { apps.addApplication(application); } if (includeRemote) { for (Application application : remoteRegionApps.values()) { apps.addApplication(application); } } if (sentDelta.get()) { addDeltaApps(includeRemote, apps); } else { System.out.println("Eureka port: " + port + ". " + System.currentTimeMillis() + ". Not including delta apps in /apps response, as delta has not been sent."); } apps.setAppsHashCode(apps.getReconcileHashCode()); sendOkResponseWithContent((Request) request, response, apps); sentRegistry.set(true); handled = true; } else if (pathInfo.startsWith("vips/")) { getSingleVipCount.getAndIncrement(); String vipAddress = pathInfo.substring("vips/".length()); Applications apps = new Applications(); apps.setVersion(-1l); for (Application application : applicationMap.values()) { Application retApp = new Application(application.getName()); for (InstanceInfo instance : application.getInstances()) { if (vipAddress.equals(instance.getVIPAddress())) { retApp.addInstance(instance); } } if (retApp.getInstances().size() > 0) { apps.addApplication(retApp); } } apps.setAppsHashCode(apps.getReconcileHashCode()); sendOkResponseWithContent((Request) request, response, apps); handled = true; } else if (pathInfo.startsWith("apps")) { // assume this is the renewal heartbeat if (request.getMethod().equals("PUT")) { // this is the renewal heartbeat heartbeatCount.getAndIncrement(); } else if (request.getMethod().equals("POST")) { // this is a register request registerCount.getAndIncrement(); String statusStr = null; String hostname = null; String line; BufferedReader reader = request.getReader(); while ((line = reader.readLine()) != null) { Matcher hostNameMatcher = HOSTNAME_PATTERN.matcher(line); if (hostname == null && hostNameMatcher.find()) { hostname = hostNameMatcher.group(1); // don't break here as we want to read the full buffer for a clean connection close } Matcher statusMatcher = STATUS_PATTERN.matcher(line); if (statusStr == null && statusMatcher.find()) { statusStr = statusMatcher.group(1); // don't break here as we want to read the full buffer for a clean connection close } } System.out.println("Matched status to: " + statusStr); registrationStatuses.add(statusStr); String appName = pathInfo.substring(5); if (!applicationMap.containsKey(appName)) { Application app = new Application(appName); InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() .setAppName(appName) .setIPAddr("1.1.1.1") .setHostName(hostname) .setStatus(InstanceInfo.InstanceStatus.toEnum(statusStr)) .setDataCenterInfo(new DataCenterInfo() { @Override public Name getName() { return Name.MyOwn; } }) .build(); app.addInstance(instanceInfo); applicationMap.put(appName, app); } } Applications apps = new Applications(); apps.setAppsHashCode(""); sendOkResponseWithContent((Request) request, response, apps); handled = true; } else { System.out.println("Not handling request: " + pathInfo); } } if (!handled) { response.sendError(HttpServletResponse.SC_NOT_FOUND, "Request path: " + pathInfo + " not supported by eureka resource mock."); } } protected void addDeltaApps(boolean includeRemote, Applications apps) { for (Application application : applicationDeltaMap.values()) { apps.addApplication(application); } if (includeRemote) { for (Application application : remoteRegionAppsDelta.values()) { apps.addApplication(application); } } } protected String getDeltaAppsHashCode(boolean includeRemote) { Applications allApps = new Applications(); for (Application application : applicationMap.values()) { allApps.addApplication(application); } if (includeRemote) { for (Application application : remoteRegionApps.values()) { allApps.addApplication(application); } } addDeltaApps(includeRemote, allApps); return allApps.getReconcileHashCode(); } protected boolean isRemoteRequest(HttpServletRequest request) { String queryString = request.getQueryString(); if (queryString == null) { return false; } return queryString.contains("regions="); } protected void sendOkResponseWithContent(Request request, HttpServletResponse response, Applications apps) throws IOException { String content = XmlXStream.getInstance().toXML(apps); response.setContentType("application/xml"); response.setStatus(HttpServletResponse.SC_OK); response.getWriter().println(content); response.getWriter().flush(); request.setHandled(true); System.out.println("Eureka port: " + port + ". " + System.currentTimeMillis() + ". Eureka resource mock, sent response for request path: " + request.getPathInfo() + ", apps count: " + apps.getRegisteredApplications().size()); } protected void sleep(int seconds) { try { Thread.sleep(seconds); } catch (InterruptedException e) { System.out.println("Interrupted: " + e); } } } }