/* Copyright 2015 predic8 GmbH, www.predic8.com
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.predic8.membrane.core.cloud.etcd;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicBoolean;
import com.predic8.membrane.annot.MCChildElement;
import com.predic8.membrane.core.config.security.SSLParser;
import com.predic8.membrane.core.resolver.ResolverMap;
import com.predic8.membrane.core.transport.ssl.StaticSSLContext;
import com.predic8.membrane.core.transport.ssl.SSLContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.Lifecycle;
import com.predic8.membrane.annot.MCAttribute;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.core.Router;
import com.predic8.membrane.core.interceptor.balancer.Balancer;
import com.predic8.membrane.core.interceptor.balancer.LoadBalancingInterceptor;
import com.predic8.membrane.core.interceptor.balancer.Node;
import com.predic8.membrane.core.rules.ServiceProxy;
import com.predic8.membrane.core.rules.ServiceProxyKey;
@MCElement(name = "etcdBasedConfigurator")
public class EtcdBasedConfigurator implements ApplicationContextAware, Lifecycle, DisposableBean {
private static final Logger log = LoggerFactory.getLogger(EtcdBasedConfigurator.class.getName());
private ApplicationContext context;
private int port = 8080;
private String baseUrl;
private String baseKey;
private Router router;
private HashMap<String, ServiceProxy> runningServiceProxyForModule = new HashMap<String, ServiceProxy>();
private HashMap<String, HashSet<EtcdNodeInformation>> runningNodesForModule = new HashMap<String, HashSet<EtcdNodeInformation>>();
private int waitTimeUntilPollAgain = 1000;
private SSLParser ssl = null;
private SSLContext sslCtx = null;
private AtomicBoolean updateThreadRunning = new AtomicBoolean(false);
private Thread nodeRefreshThread = new Thread(new Runnable() {
@Override
public void run() {
updateThreadRunning.compareAndSet(false,true);
while (updateThreadRunning.get()) {
try {
setUpServiceProxies(getConfigFromEtcd());
Thread.sleep(waitTimeUntilPollAgain);
}catch (Exception ignored) {
}
if(Thread.interrupted()){
return;
}
}
}
});
public int getPort() {
return port;
}
/**
* @description port
* @default 8080
*/
@MCAttribute
public void setPort(int port) {
this.port = port;
}
public String getBaseUrl() {
return baseUrl;
}
/**
* @description URL for etcd
* @default "http://localhost:4001"
*/
@MCAttribute
public void setBaseUrl(String baseURL) {
this.baseUrl = baseURL;
}
public String getBaseKey() {
return baseKey;
}
/**
* @description Key/Directory
* @default "/asa/lb"
*/
@MCAttribute
public void setBaseKey(String baseKey) {
this.baseKey = baseKey;
}
@Override
public boolean isRunning() {
return false;
}
@Override
public void start() {
if (router == null) {
if (context == null)
throw new IllegalStateException(
"EtcdBasedConfigurator requires a Router. Option 1 is to call setRouter(). Option 2 is setApplicationContext() and the EBC will try to use the only Router available.");
router = context.getBean(Router.class);
}
try {
} catch (Exception ignored) {
}
if (ssl != null)
sslCtx = new StaticSSLContext(ssl, new ResolverMap(), null);
if (!nodeRefreshThread.isAlive()) {
nodeRefreshThread.start();
}
}
private void setUpServiceProxies(ArrayList<EtcdNodeInformation> nodes) throws Exception {
HashSet<EtcdNodeInformation> newRunningNodes = new HashSet<EtcdNodeInformation>();
if (nodes.size() > 0) {
for (EtcdNodeInformation node : nodes) {
String currentModule = node.getModule();
if (!runningServiceProxyForModule.containsKey(currentModule)) {
setUpModuleServiceProxy(currentModule + " cluster", port, currentModule);
runningNodesForModule.put(currentModule, new HashSet<EtcdNodeInformation>());
}
if (!runningNodesForModule.get(currentModule).contains(node)) {
setUpClusterNode(node);
}
newRunningNodes.add(node);
}
}
cleanUpNotRunningNodes(newRunningNodes);
}
private void setUpClusterNode(EtcdNodeInformation node) {
log.info("Creating " + node);
ServiceProxy sp = runningServiceProxyForModule.get(node.getModule());
LoadBalancingInterceptor lbi = (LoadBalancingInterceptor) sp.getInterceptors().get(0);
lbi.getClusterManager().getClusters().get(0)
.nodeUp(new Node(node.getTargetHost(), Integer.parseInt(node.getTargetPort())));
runningNodesForModule.get(node.getModule()).add(node);
}
private ServiceProxy setUpModuleServiceProxy(String name, int port, String path) {
log.info("Creating serviceProxy for module: " + path);
ServiceProxyKey key = new ServiceProxyKey("*", "*", path, port);
key.setUsePathPattern(true);
key.setPathRegExp(false);
ServiceProxy sp = new ServiceProxy(key, null, 0);
sp.getInterceptors().add(new LoadBalancingInterceptor());
try {
sp.init(router);
router.add(sp);
runningServiceProxyForModule.put(path, sp);
} catch (Exception ignored) {
}
return sp;
}
private void cleanUpNotRunningNodes(HashSet<EtcdNodeInformation> newRunningNodes) {
HashSet<EtcdNodeInformation> currentlyRunningNodes = new HashSet<EtcdNodeInformation>();
for (String module : runningNodesForModule.keySet()) {
currentlyRunningNodes.addAll(runningNodesForModule.get(module));
}
for (EtcdNodeInformation node : newRunningNodes) {
currentlyRunningNodes.remove(node);
}
for (EtcdNodeInformation node : currentlyRunningNodes) {
shutdownRunningClusterNode(node);
}
HashSet<String> modules = new HashSet<String>();
for (String module : runningNodesForModule.keySet()) {
modules.add(module);
}
for (String module : modules) {
if (runningNodesForModule.get(module).size() == 0) {
runningNodesForModule.remove(module);
shutDownRunningModuleServiceProxy(module);
}
}
}
private void shutDownRunningModuleServiceProxy(String module) {
log.info("Destroying serviceProxy for module: " + module);
ServiceProxy sp = runningServiceProxyForModule.get(module);
router.getRuleManager().removeRule(sp);
runningServiceProxyForModule.remove(module);
}
private void shutdownRunningClusterNode(EtcdNodeInformation node) {
log.info("Destroying " + node);
ServiceProxy sp = runningServiceProxyForModule.get(node.getModule());
LoadBalancingInterceptor lbi = (LoadBalancingInterceptor) sp.getInterceptors().get(0);
lbi.getClusterManager().removeNode(Balancer.DEFAULT_NAME, baseUrl, port);
runningNodesForModule.get(node.getModule()).remove(node);
}
private EtcdRequest createRequest(String module){
if(sslCtx != null)
return EtcdRequest.create(sslCtx, baseUrl, baseKey, module);
else
return EtcdRequest.create(baseUrl, baseKey, module);
}
private ArrayList<EtcdNodeInformation> getConfigFromEtcd() {
ArrayList<EtcdNodeInformation> nodes = new ArrayList<EtcdNodeInformation>();
try {
EtcdResponse respAvailableModules = createRequest("").sendRequest();
if (!respAvailableModules.is2XX()) {
return nodes;
}
ArrayList<String> availableModules = respAvailableModules.getDirectories();
for (String module : availableModules) {
EtcdResponse respAvailableServicesForModule = createRequest(module).sendRequest();
if (!respAvailableServicesForModule.is2XX()) {
return nodes;
}
ArrayList<String> availableUUIDs = respAvailableServicesForModule.getDirectories();
for (String uuid : availableUUIDs) {
EtcdResponse respName = createRequest(module).uuid(uuid)
.getValue("name").sendRequest();
if (!respName.is2XX()) {
return nodes;
}
String targetName = respName.getValue();
EtcdResponse respPort = createRequest(module).uuid(uuid)
.getValue("port").sendRequest();
if (!respPort.is2XX()) {
return nodes;
}
String targetPort = respPort.getValue();
EtcdResponse respHost = createRequest(module).uuid(uuid)
.getValue("host").sendRequest();
if (!respHost.is2XX()) {
return nodes;
}
String targetHost = respHost.getValue();
EtcdNodeInformation node = new EtcdNodeInformation(module, uuid, targetHost, targetPort,
targetName);
if (node.isValid()) {
nodes.add(node);
}
}
}
} catch (Exception e) {
log.warn("Error retrieving base info from etcd.");
}
return nodes;
}
@Override
public void stop() {
updateThreadRunning.compareAndSet(true,false);
nodeRefreshThread.interrupt();
try {
nodeRefreshThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void setApplicationContext(ApplicationContext arg0) throws BeansException {
context = arg0;
}
public Router getRouter() {
return router;
}
public void setRouter(Router router) {
this.router = router;
}
public SSLParser getSsl() {
return ssl;
}
@MCChildElement
public void setSsl(SSLParser ssl) {
this.ssl = ssl;
}
@Override
public void destroy() throws Exception {
log.info("Destroying nodes");
sslCtx = null;
ssl = null;
updateThreadRunning.compareAndSet(true,false);
nodeRefreshThread.interrupt();
try {
nodeRefreshThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
nodeRefreshThread = null;
}
}