/**
* Copyright 2016-2017 Sixt GmbH & Co. Autovermietung KG
* 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 com.sixt.service.framework.registry.consul;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import com.sixt.service.framework.ServiceMethodHandler;
import com.sixt.service.framework.ServiceProperties;
import com.sixt.service.framework.util.Sleeper;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.Deflater;
import static com.sixt.service.framework.FeatureFlags.DEFAULT_HEALTH_CHECK_POLL_INTERVAL;
import static com.sixt.service.framework.util.ReflectionUtil.findSubClassParameterType;
//TODO: refactor this class so it can be shared across registry plugins
//TODO: make sure state change information is correctly logged
@Singleton
public class RegistrationManager implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(RegistrationManager.class);
private HttpClient httpClient;
private ServiceProperties serviceProps;
private Map<String, ServiceMethodHandler<? extends Message, ? extends Message>> registeredHandlers;
private AtomicBoolean isRegistered = new AtomicBoolean(false);
private AtomicBoolean isShutdownHookRegistered = new AtomicBoolean(false);
private String serviceId;
private String serviceName;
private String ipAddress;
private String unregisterString;
private JsonObject registrationJsonObject = null;
private ExecutorService executorService;
private Sleeper sleeper = new Sleeper();
@Inject
public RegistrationManager(ServiceProperties serviceProps,
HttpClient httpClient) {
this.serviceProps = serviceProps;
this.httpClient = httpClient;
serviceId = serviceProps.getServiceInstanceId();
serviceName = serviceProps.getServiceName();
}
public void setRegisteredHandlers(Map<String, ServiceMethodHandler<? extends Message,
? extends Message>> registeredHandlers) {
this.registeredHandlers = registeredHandlers;
}
public void register() {
executorService = Executors.newSingleThreadExecutor();
executorService.submit(this);
}
public boolean isRegistered() {
//TODO: verify actual registration with consul
return isRegistered.get();
}
@Override
public void run() {
long sleepDuration = 1000;
while (! isRegistered.get()) {
try {
attemptRegistration();
if (isRegistered.get()) {
break;
}
sleeper.sleepNoException(sleepDuration);
sleepDuration = (long) (sleepDuration * 1.5);
} catch (Exception ex) {
logger.warn("Caught exception attempting service registration", ex);
}
}
}
public void shutdown() {
logger.info("Shutting down {}", serviceName);
executorService.shutdown();
try {
executorService.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
logger.warn("Timed out waiting for worker thread termination");
}
unregisterService();
}
private void attemptRegistration() {
updateIpAddress();
JsonObject request = null;
try {
request = buildJsonRequest();
} catch (IOException e) {
logger.warn("Error building registration json", e);
}
logger.info("Attempting service registration of {}", serviceName);
boolean success = sendRegistration(request);
isRegistered.set(success);
if (success) {
logger.info("Registration of {} successful", serviceName);
registerShutdownHook();
}
}
private synchronized void registerShutdownHook() {
if (! isShutdownHookRegistered.get()) {
logger.info("Registering shutdown hook for {}", serviceName);
//low-level http to make fast as possible
unregisterString = "GET /v1/agent/service/deregister/" + serviceId + " HTTP/1.0\r\n\r\n";
Runtime.getRuntime().addShutdownHook(new Thread(this::unregisterService));
}
isShutdownHookRegistered.set(true);
}
protected void unregisterService() {
if (! isRegistered.get()) {
return;
}
try {
logger.info("Unregistering {}", serviceName);
String registryServer = serviceProps.getRegistryServer();
int colon = registryServer.indexOf(':');
String hostname = registryServer.substring(0, colon);
int port = Integer.parseInt(registryServer.substring(colon + 1));
Socket sock = new Socket(hostname, port);
OutputStream out = sock.getOutputStream();
out.write(unregisterString.getBytes());
out.flush();
sock.close();
if (isShutdownHookRegistered.get()) {
isShutdownHookRegistered.set(false);
}
} catch (Exception ex) {
logger.error("Error unregistering from consul", ex);
}
}
private boolean sendRegistration(JsonObject request) {
try {
ContentResponse httpResponse = httpClient.newRequest(getRegistrationUri()).
content(new StringContentProvider(request.toString())).
method(HttpMethod.PUT).header(HttpHeader.CONTENT_TYPE, "application/json").send();
if (httpResponse.getStatus() == 200) {
return true;
}
} catch (Exception ex) {
logger.warn("Caught exception sending registration {}", request.toString(), ex);
}
return false;
}
private String getRegistrationUri() {
return "http://" + serviceProps.getRegistryServer() + "/v1/agent/service/register";
}
protected JsonObject buildJsonRequest() throws IOException {
if (registrationJsonObject == null) {
JsonObject json = new JsonObject();
json.addProperty("ID", serviceId);
json.addProperty("Name", serviceName);
json.add("Tags", getRegistrationTags());
json.addProperty("Address", ipAddress);
json.addProperty("Port", serviceProps.getServicePort());
JsonObject ttlElement = new JsonObject();
ttlElement.addProperty("TTL", (DEFAULT_HEALTH_CHECK_POLL_INTERVAL + 2) + "s");
ttlElement.addProperty("DeregisterCriticalServiceAfter", "5m");
json.add("Check", ttlElement);
registrationJsonObject = json;
}
return registrationJsonObject;
}
private void updateIpAddress() {
try {
Enumeration<NetworkInterface> b = NetworkInterface.getNetworkInterfaces();
ipAddress = null;
while( b.hasMoreElements()){
NetworkInterface iface = b.nextElement();
if (iface.getName().startsWith("dock")) {
continue;
}
for ( InterfaceAddress f : iface.getInterfaceAddresses()) {
if (f.getAddress().isSiteLocalAddress()) {
ipAddress = f.getAddress().getHostAddress();
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}
}
private JsonArray getRegistrationTags() throws IOException {
JsonArray retval = new JsonArray();
String tag = "{\"type\":\"" + getShortServiceName() + "\"}";
retval.add(new JsonPrimitive("t-" + binaryEncode(tag)));
tag = "{\"transport\":\"http\"}";
retval.add(new JsonPrimitive("t-" + binaryEncode(tag)));
tag = "{\"broker\":\"http\"}";
retval.add(new JsonPrimitive("t-" + binaryEncode(tag)));
tag = "{\"server\":\"rpc\"}";
retval.add(new JsonPrimitive("t-" + binaryEncode(tag)));
tag = "{\"registry\":\"consul\"}";
retval.add(new JsonPrimitive("t-" + binaryEncode(tag)));
addEndpointsTags(retval);
tag = getServiceVersion();
retval.add(new JsonPrimitive("v-" + binaryEncode(tag)));
return retval;
}
private String binaryEncode(String tag) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Deflater deflater = new Deflater();
deflater.setInput(tag.getBytes());
deflater.finish();
byte[] buffer = new byte[1024];
while (!deflater.finished()) {
int count = deflater.deflate(buffer);
outputStream.write(buffer, 0, count);
}
outputStream.close();
byte compressed[] = outputStream.toByteArray();
return bytesToHex(compressed);
}
final protected static char[] hexArray = "0123456789abcdef".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
@SuppressWarnings("unchecked")
private void addEndpointsTags(JsonArray tagsArray) throws IOException {
if (registeredHandlers == null || registeredHandlers.isEmpty()) {
return;
}
for (String key : registeredHandlers.keySet()) {
StringBuilder sb = new StringBuilder();
sb.append("{\"name\":\"");
sb.append(key);
sb.append("\",\"request\":{\"name\":\"");
try {
ServiceMethodHandler handler = registeredHandlers.get(key);
Class<? extends Message> requestClass = (Class<? extends Message>)
findSubClassParameterType(handler, 0);
Class<? extends Message> responseClass = (Class<? extends Message>)
findSubClassParameterType(handler, 1);
sb.append(requestClass.getSimpleName());
sb.append("\",\"type\":\"");
sb.append(requestClass.getSimpleName());
sb.append("\",\"values\":[");
sb.append(getProtobufClassFieldDescriptions(requestClass));
sb.append("]},\"response\":{\"name\":\"");
sb.append(responseClass.getSimpleName());
sb.append("\",\"type\":\"");
sb.append(responseClass.getSimpleName());
sb.append("\",\"values\":[");
sb.append(getProtobufClassFieldDescriptions(responseClass));
sb.append("]},\"metadata\":{\"stream\":\"false\"}}");
} catch (Exception e) {
logger.error("Error inspecting handlers", e);
return;
}
String tag = sb.toString();
tagsArray.add(new JsonPrimitive("e-" + binaryEncode(tag)));
}
}
protected String getProtobufClassFieldDescriptions(Class<? extends Message> messageClass)
throws Exception {
StringBuilder sb = new StringBuilder();
Constructor<?> constructor = null;
try {
constructor = messageClass.getDeclaredConstructor();
} catch (NoSuchMethodException nsmex) {
//Issue #35
logger.info("Unsupported protobuf field: {}", messageClass.getName());
return sb.toString();
}
constructor.setAccessible(true);
Object instance = constructor.newInstance();
Message.Builder builder = ((Message)instance).newBuilderForType();
Message message = builder.build();
Descriptors.Descriptor requestDesc = message.getDescriptorForType();
List<Descriptors.FieldDescriptor> requestFields = requestDesc.getFields();
Iterator<Descriptors.FieldDescriptor> iter = requestFields.iterator();
while (iter.hasNext()) {
Descriptors.FieldDescriptor fd = iter.next();
//TODO: deal with repeated fields
sb.append("{\"name\":\"");
sb.append(fd.getName());
sb.append("\",\"type\":\"");
if (fd.getType().toString().equalsIgnoreCase("message")) {
sb.append(getLastComponent(fd.getMessageType().getFullName()));
sb.append("\",\"values\":[");
Descriptors.FieldDescriptor childDescriptor = requestDesc.findFieldByName(fd.getName());
Message.Builder subMessageBuilder = builder.newBuilderForField(childDescriptor);
Message subMessage = subMessageBuilder.build();
sb.append(getProtobufClassFieldDescriptions(subMessage.getClass()));
sb.append("]}");
} else {
sb.append(fd.getType().toString().toLowerCase());
sb.append("\",\"values\":null}");
}
if (iter.hasNext()) {
sb.append(",");
}
}
return sb.toString();
}
private String getLastComponent(String fullName) {
if (! StringUtils.contains(fullName, ".")) {
return fullName;
}
int index = fullName.lastIndexOf('.');
return fullName.substring(index + 1);
}
private String getShortServiceName() {
String name = serviceName;
if (StringUtils.isBlank(name)) {
return "unknown";
}
if (name.contains(".")) {
name = name.substring(name.lastIndexOf('.') + 1);
}
return name;
}
private String getServiceVersion() {
String version = serviceProps.getServiceVersion();
if (StringUtils.isBlank(version)) {
return "unknown";
}
return version;
}
}