io.undertow.server.handlers.proxy.mod_cluster.ModClusterContainer Maven / Gradle / Ivy
Go to download
This artifact provides a single jar that contains all classes required to use remote Jakarta Enterprise Beans and Jakarta Messaging, including
all dependencies. It is intended for use by those not using maven, maven users should just import the Jakarta Enterprise Beans and
Jakarta Messaging BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up
with different versions on classes on the class path).
/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.server.handlers.proxy.mod_cluster;
import io.undertow.UndertowLogger;
import io.undertow.client.UndertowClient;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import io.undertow.server.handlers.cache.LRUCache;
import io.undertow.server.handlers.proxy.RouteIteratorFactory;
import io.undertow.server.handlers.proxy.ProxyClient;
import io.undertow.util.CopyOnWriteMap;
import io.undertow.util.Headers;
import io.undertow.util.PathMatcher;
import io.undertow.connector.ByteBufferPool;
import org.xnio.OptionMap;
import org.xnio.XnioExecutor;
import org.xnio.XnioIoThread;
import org.xnio.ssl.XnioSsl;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
/**
* @author Stuart Douglas
* @author Emanuel Muckenhuber
* @author Radoslav Husar
* @author Richard Opalka
*/
class ModClusterContainer implements ModClusterController {
// The configured balancers
private final ConcurrentMap balancers = new CopyOnWriteMap<>();
// The available nodes
private final ConcurrentMap nodes = new CopyOnWriteMap<>();
// virtual-host > per context balancing table
private final ConcurrentMap hosts = new CopyOnWriteMap<>();
// Map of removed jvmRoutes to failover domain
private final LRUCache failoverDomains = new LRUCache<>(100, 5 * 60 * 1000);
// The health check tasks
private final ConcurrentMap healthChecks = new CopyOnWriteMap<>();
private final UpdateLoadTask updateLoadTask = new UpdateLoadTask();
private final XnioSsl xnioSsl;
private final UndertowClient client;
private final ProxyClient proxyClient;
private final ModCluster modCluster;
private final NodeHealthChecker healthChecker;
private final long removeBrokenNodesThreshold;
private RouteIteratorFactory routeIteratorFactory;
private final OptionMap clientOptions;
ModClusterContainer(final ModCluster modCluster, final XnioSsl xnioSsl, final UndertowClient client, OptionMap clientOptions) {
this.xnioSsl = xnioSsl;
this.client = client;
this.modCluster = modCluster;
this.clientOptions = clientOptions;
this.healthChecker = modCluster.getHealthChecker();
this.proxyClient = new ModClusterProxyClient(null, this);
this.removeBrokenNodesThreshold = removeThreshold(modCluster.getHealthCheckInterval(), modCluster.getRemoveBrokenNodes());
this.routeIteratorFactory = new RouteIteratorFactory(modCluster.routeParsingStrategy(), RouteIteratorFactory.ParsingCompatibility.MOD_CLUSTER, modCluster.rankedAffinityDelimiter());
}
String getServerID() {
return modCluster.getServerID();
}
UndertowClient getClient() {
return client;
}
XnioSsl getXnioSsl() {
return xnioSsl;
}
/**
* Get the proxy client.
*
* @return the proxy client
*/
public ProxyClient getProxyClient() {
return proxyClient;
}
Collection getBalancers() {
return Collections.unmodifiableCollection(balancers.values());
}
Collection getNodes() {
return Collections.unmodifiableCollection(nodes.values());
}
Node getNode(final String jvmRoute) {
return nodes.get(jvmRoute);
}
/**
* Get the mod_cluster proxy target.
*
* @param exchange the http exchange
* @return proxy target
*/
public ModClusterProxyTarget findTarget(final HttpServerExchange exchange) {
// There is an option to disable the virtual host check, probably a default virtual host
final PathMatcher.PathMatch entry = mapVirtualHost(exchange);
if (entry == null) {
return null;
}
for (final Balancer balancer : balancers.values()) {
if (balancer.isStickySession()) {
for (Cookie cookie : exchange.requestCookies()) {
if (balancer.getStickySessionCookie().equals(cookie.getName())) {
String sessionId = cookie.getValue();
Iterator routes = parseRoutes(sessionId);
if (routes.hasNext()) {
return new ModClusterProxyTarget.ExistingSessionTarget(sessionId, routes, entry.getValue(), this, balancer.isStickySessionForce());
}
}
}
if (exchange.getPathParameters().containsKey(balancer.getStickySessionPath())) {
String sessionId = exchange.getPathParameters().get(balancer.getStickySessionPath()).getFirst();
Iterator jvmRoute = parseRoutes(sessionId);
if (jvmRoute.hasNext()) {
return new ModClusterProxyTarget.ExistingSessionTarget(sessionId, jvmRoute, entry.getValue(), this, balancer.isStickySessionForce());
}
}
}
}
return new ModClusterProxyTarget.BasicTarget(entry.getValue(), this);
}
/**
* Register a new node.
*
* @param config the node configuration
* @param balancerConfig the balancer configuration
* @param ioThread the associated I/O thread
* @param bufferPool the buffer pool
* @return whether the node could be created or not
*/
public synchronized boolean addNode(final NodeConfig config, final Balancer.BalancerBuilder balancerConfig, final XnioIoThread ioThread, final ByteBufferPool bufferPool) {
final String jvmRoute = config.getJvmRoute();
final Node existing = nodes.get(jvmRoute);
if (existing != null) {
if (config.getConnectionURI().equals(existing.getNodeConfig().getConnectionURI())) {
// TODO better check if they are the same
existing.resetState();
return true;
} else {
existing.markRemoved();
removeNode(existing);
if (!existing.isInErrorState()) {
return false; // replies with MNODERM error
}
}
}
final String balancerRef = config.getBalancer();
Balancer balancer = balancers.get(balancerRef);
if (balancer != null) {
UndertowLogger.ROOT_LOGGER.debugf("Balancer %s already exists, replacing", balancerRef);
}
balancer = balancerConfig.build();
balancers.put(balancerRef, balancer);
final Node node = new Node(config, balancer, ioThread, bufferPool, this);
nodes.put(jvmRoute, node);
// Schedule the health check
scheduleHealthCheck(node, ioThread);
// Reset the load factor periodically
if (updateLoadTask.cancelKey == null) {
updateLoadTask.cancelKey = ioThread.executeAtInterval(updateLoadTask, modCluster.getHealthCheckInterval(), TimeUnit.MILLISECONDS);
}
// Remove from the failover groups
failoverDomains.remove(node.getJvmRoute());
UndertowLogger.ROOT_LOGGER.registeringNode(jvmRoute, config.getConnectionURI());
return true;
}
/**
* Management command enabling all contexts on the given node.
*
* @param jvmRoute the jvmRoute
* @return whether the given node was enabled
*/
public synchronized boolean enableNode(final String jvmRoute) {
final Node node = nodes.get(jvmRoute);
if (node != null) {
for (final Context context : node.getContexts()) {
context.enable();
}
return true;
}
return false;
}
/**
* Management command disabling all contexts on the given node.
*
* @param jvmRoute the jvmRoute
* @return whether the given node was disabled
*/
public synchronized boolean disableNode(final String jvmRoute) {
final Node node = nodes.get(jvmRoute);
if (node != null) {
for (final Context context : node.getContexts()) {
context.disable();
}
return true;
}
return false;
}
/**
* Management command stopping all contexts on the given node.
*
* @param jvmRoute the jvmRoute
* @return whether the given node was stopped
*/
public synchronized boolean stopNode(final String jvmRoute) {
final Node node = nodes.get(jvmRoute);
if (node != null) {
for (final Context context : node.getContexts()) {
context.stop();
}
return true;
}
return false;
}
/**
* Remove a node.
*
* @param jvmRoute the jvmRoute
* @return the removed node
*/
public synchronized Node removeNode(final String jvmRoute) {
final Node node = nodes.get(jvmRoute);
if (node != null) {
removeNode(node);
}
return node;
}
protected void removeNode(final Node node) {
removeNode(node, false);
}
protected synchronized void removeNode(final Node node, boolean onlyInError) {
if (onlyInError && !node.isInErrorState()) {
return;
}
final String jvmRoute = node.getJvmRoute();
node.markRemoved();
if (nodes.remove(jvmRoute, node)) {
UndertowLogger.ROOT_LOGGER.removingNode(jvmRoute);
node.markRemoved();
// Remove the health check
removeHealthCheck(node, node.getIoThread());
// Remove the contexts, if any
for (final Context context : node.getContexts()) {
removeContext(context.getPath(), node, context.getVirtualHosts());
}
final String domain = node.getNodeConfig().getDomain();
if (domain != null) {
failoverDomains.add(node.getJvmRoute(), domain);
}
final String balancerName = node.getBalancer().getName();
for (final Node other : nodes.values()) {
if (other.getBalancer().getName().equals(balancerName)) {
return;
}
}
balancers.remove(balancerName);
}
if (nodes.size() == 0) {
// In case there are no nodes registered unschedule the task
updateLoadTask.cancelKey.remove();
updateLoadTask.cancelKey = null;
}
}
/**
* Register a web context. If the web context already exists, just enable it.
*
* @param contextPath the context path
* @param jvmRoute the jvmRoute
* @param aliases the virtual host aliases
*/
public synchronized boolean enableContext(final String contextPath, final String jvmRoute, final List aliases) {
final Node node = nodes.get(jvmRoute);
if (node != null) {
Context context = getOrRegisterContext(contextPath, jvmRoute, aliases, node);
context.enable();
return true;
}
return false;
}
public synchronized boolean disableContext(final String contextPath, final String jvmRoute, List aliases) {
final Node node = nodes.get(jvmRoute);
if (node != null) {
Context context = getOrRegisterContext(contextPath, jvmRoute, aliases, node);
context.disable();
return true;
}
return false;
}
synchronized int stopContext(final String contextPath, final String jvmRoute, List aliases) {
final Node node = nodes.get(jvmRoute);
if (node != null) {
Context context = getOrRegisterContext(contextPath, jvmRoute, aliases, node);
context.stop();
return context.getActiveRequests();
}
return -1;
}
synchronized boolean removeContext(final String contextPath, final String jvmRoute, List aliases) {
final Node node = nodes.get(jvmRoute);
if (node != null) {
return removeContext(contextPath, node, aliases);
}
return false;
}
public synchronized boolean removeContext(final String contextPath, final Node node, List aliases) {
if (node == null) {
return false;
}
final String jvmRoute = node.getJvmRoute();
UndertowLogger.ROOT_LOGGER.unregisteringContext(contextPath, jvmRoute);
final Context context = node.removeContext(contextPath, aliases);
if (context == null) {
return false;
}
context.stop();
for (final String alias : context.getVirtualHosts()) {
final VirtualHost virtualHost = hosts.get(alias);
if (virtualHost != null) {
virtualHost.removeContext(contextPath, jvmRoute, context);
if (virtualHost.isEmpty()) {
hosts.remove(alias);
}
}
}
return true;
}
private Context getOrRegisterContext(String contextPath, String jvmRoute, List aliases, Node node) {
Context context = node.getContext(contextPath, aliases);
if (context == null) {
context = node.registerContext(contextPath, aliases);
UndertowLogger.ROOT_LOGGER.registeringContext(contextPath, jvmRoute);
UndertowLogger.ROOT_LOGGER.registeringContext(contextPath, jvmRoute, aliases);
for (final String alias : aliases) {
VirtualHost virtualHost = hosts.get(alias);
if (virtualHost == null) {
virtualHost = new VirtualHost();
hosts.put(alias, virtualHost);
}
virtualHost.registerContext(contextPath, jvmRoute, context);
}
}
return context;
}
/**
* Find a new node handling this request.
*
* @param entry the resolved virtual host entry
* @return the context, {@code null} if not found
*/
Context findNewNode(final VirtualHost.HostEntry entry) {
return electNode(entry.getContexts(), false, null);
}
/**
* Try to find a failover node within the same load balancing group.
*
* @param entry the resolved virtual host entry
* @param domain the load balancing domain, if known
* @param session the actual value of JSESSIONID/jsessionid cookie/parameter
* @param jvmRoute the original jvmRoute; in case of multiple routes, the first one
* @param forceStickySession whether sticky sessions are forced
* @return the context, {@code null} if not found
*/
Context findFailoverNode(final VirtualHost.HostEntry entry, final String domain, final String session, final String jvmRoute, final boolean forceStickySession) {
// If configured, deterministically choose the failover target by calculating hash of the session ID modulo number of electable nodes
if (modCluster.isDeterministicFailover()) {
List candidates = new ArrayList<>(entry.getNodes().size());
for (String route : entry.getNodes()) {
Node node = nodes.get(route);
if (node != null && !node.isInErrorState() && !node.isHotStandby()) {
candidates.add(route);
}
}
// If there are no available regular nodes, all hot standby nodes become candidates
if (candidates.isEmpty()) {
for (String route : entry.getNodes()) {
Node node = nodes.get(route);
if (node != null && !node.isInErrorState() && node.isHotStandby()) {
candidates.add(route);
}
}
}
if (candidates.isEmpty()) {
return null;
}
String sessionId = session.substring(0, session.indexOf('.'));
int index = (int) (Math.abs((long) sessionId.hashCode()) % candidates.size());
Collections.sort(candidates);
String electedRoute = candidates.get(index);
UndertowLogger.ROOT_LOGGER.debugf("Using deterministic failover target: %s", electedRoute);
return entry.getContextForNode(electedRoute);
}
String failOverDomain = null;
if (domain == null) {
final Node node = nodes.get(jvmRoute);
if (node != null) {
failOverDomain = node.getNodeConfig().getDomain();
}
if (failOverDomain == null) {
failOverDomain = failoverDomains.get(jvmRoute);
}
} else {
failOverDomain = domain;
}
final Collection contexts = entry.getContexts();
if (failOverDomain != null) {
final Context context = electNode(contexts, true, failOverDomain);
if (context != null) {
return context;
}
}
if (forceStickySession) {
return null;
} else {
return electNode(contexts, false, null);
}
}
/**
* Map a request to virtual host.
*
* @param exchange the http exchange
*/
private PathMatcher.PathMatch mapVirtualHost(final HttpServerExchange exchange) {
final String context = exchange.getRelativePath();
if(modCluster.isUseAlias()) {
final String hostName = exchange.getRequestHeaders().getFirst(Headers.HOST);
if (hostName != null) {
// Remove the port from the host
int i = hostName.indexOf(":");
VirtualHost host;
if (i > 0) {
host = hosts.get(hostName.substring(0, i));
if (host == null) {
host = hosts.get(hostName);
}
} else {
host = hosts.get(hostName);
}
if (host == null) {
return null;
}
PathMatcher.PathMatch result = host.match(context);
if (result.getValue() == null) {
return null;
}
return result;
}
} else {
for(Map.Entry host : hosts.entrySet()) {
PathMatcher.PathMatch result = host.getValue().match(context);
if (result.getValue() != null) {
return result;
}
}
}
return null;
}
OptionMap getClientOptions() {
return clientOptions;
}
private Iterator parseRoutes(String sessionId) {
return routeIteratorFactory.iterator(sessionId);
}
static Context electNode(final Iterable contexts, final boolean existingSession, final String domain) {
Context elected = null;
Node candidate = null;
boolean candidateHotStandby = false;
for (Context context : contexts) {
// Skip disabled contexts
if (context.checkAvailable(existingSession)) {
final Node node = context.getNode();
final boolean hotStandby = node.isHotStandby();
// Check that we only failover in the domain
if (domain != null && !domain.equals(node.getNodeConfig().getDomain())) {
continue;
}
if (candidate != null) {
// Check if the nodes are in hot-standby
if (candidateHotStandby) {
if (hotStandby) {
if (candidate.getElectedDiff() > node.getElectedDiff()) {
candidate = node;
elected = context;
}
} else {
candidate = node;
elected = context;
candidateHotStandby = hotStandby;
}
} else if (hotStandby) {
continue;
} else {
// Normal election process
final int lbStatus1 = candidate.getLoadStatus();
final int lbStatus2 = node.getLoadStatus();
if (lbStatus1 > lbStatus2) {
candidate = node;
elected = context;
candidateHotStandby = false;
}
}
} else {
candidate = node;
elected = context;
candidateHotStandby = hotStandby;
}
}
}
if (candidate != null) {
candidate.elected(); // We have a winner!
}
return elected;
}
void scheduleHealthCheck(final Node node, XnioIoThread ioThread) {
assert Thread.holdsLock(this);
HealthCheckTask task = healthChecks.get(ioThread);
if (task == null) {
task = new HealthCheckTask(removeBrokenNodesThreshold, healthChecker);
healthChecks.put(ioThread, task);
task.cancelKey = ioThread.executeAtInterval(task, modCluster.getHealthCheckInterval(), TimeUnit.MILLISECONDS);
}
task.nodes.add(node);
}
void removeHealthCheck(final Node node, XnioIoThread ioThread) {
assert Thread.holdsLock(this);
final HealthCheckTask task = healthChecks.get(ioThread);
if (task == null) {
return;
}
task.nodes.remove(node);
if (task.nodes.size() == 0) {
healthChecks.remove(ioThread);
task.cancelKey.remove();
}
}
static long removeThreshold(final long healthChecks, final long removeBrokenNodes) {
if (healthChecks > 0 && removeBrokenNodes > 0) {
final long threshold = removeBrokenNodes / healthChecks;
if (threshold > 1000) {
return 1000;
} else if (threshold < 1) {
return 1;
} else {
return threshold;
}
} else {
return -1;
}
}
static class HealthCheckTask implements Runnable {
private final long threshold;
private final NodeHealthChecker healthChecker;
private final ArrayList nodes = new ArrayList<>();
private volatile XnioExecutor.Key cancelKey;
HealthCheckTask(long threshold, NodeHealthChecker healthChecker) {
this.threshold = threshold;
this.healthChecker = healthChecker;
}
@Override
public void run() {
for (final Node node : nodes) {
node.checkHealth(threshold, healthChecker);
}
}
}
class UpdateLoadTask implements Runnable {
private volatile XnioExecutor.Key cancelKey;
@Override
public void run() {
for (final Node node : nodes.values()) {
node.resetLbStatus();
}
}
}
@Override
public ModClusterStatus getStatus() {
List balancers = new ArrayList<>();
for(Map.Entry bentry : this.balancers.entrySet()) {
List nodes = new ArrayList<>();
for(Node node : this.getNodes()) {
if(node.getBalancer().getName().equals(bentry.getKey())) {
List contexts = new ArrayList<>();
for(Context i : node.getContexts()) {
contexts.add(new ContextImpl(i));
}
nodes.add(new NodeImpl(node, contexts));
}
}
balancers.add(new BalancerImpl(bentry.getValue(), nodes));
}
return new ModClusterStatusImpl(balancers);
}
private static class ModClusterStatusImpl implements ModClusterStatus {
private final List balancers;
private ModClusterStatusImpl(List balancers) {
this.balancers = balancers;
}
@Override
public List getLoadBalancers() {
return balancers;
}
@Override
public LoadBalancer getLoadBalancer(String name) {
for (LoadBalancer b : balancers) {
if (b.getName().equals(name)) {
return b;
}
}
return null;
}
}
private static class BalancerImpl implements ModClusterStatus.LoadBalancer {
private final Balancer balancer;
private final List nodes;
private BalancerImpl(Balancer balancer, List nodes) {
this.balancer = balancer;
this.nodes = nodes;
}
@Override
public String getName() {
return balancer.getName();
}
@Override
public List getNodes() {
return nodes;
}
@Override
public ModClusterStatus.Node getNode(String name) {
for (ModClusterStatus.Node i : nodes) {
if(i.getName().equals(name)) {
return i;
}
}
return null;
}
@Override
public boolean isStickySession() {
return balancer.isStickySession();
}
@Override
public String getStickySessionCookie() {
return balancer.getStickySessionCookie();
}
@Override
public String getStickySessionPath() {
return null;
}
@Override
public boolean isStickySessionRemove() {
return balancer.isStickySessionRemove();
}
@Override
public boolean isStickySessionForce() {
return balancer.isStickySessionForce();
}
@Override
public int getWaitWorker() {
return balancer.getWaitWorker();
}
@Override
public int getMaxRetries() {
return balancer.getMaxRetries();
}
@Override
@Deprecated
public int getMaxAttempts() {
return balancer.getMaxRetries();
}
}
private static class NodeImpl implements ModClusterStatus.Node {
private final Node node;
private final List contexts;
private NodeImpl(Node node, List contexts) {
this.node = node;
this.contexts = contexts;
}
@Override
public String getName() {
return node.getJvmRoute();
}
@Override
public URI getUri() {
return node.getConnectionPool().getUri();
}
@Override
public List getContexts() {
return Collections.unmodifiableList(contexts);
}
@Override
public ModClusterStatus.Context getContext(String name) {
for (ModClusterStatus.Context i : contexts) {
if(i.getName().equals(name)) {
return i;
}
}
return null;
}
@Override
public int getLoad() {
return node.getLoad();
}
@Override
public NodeStatus getStatus() {
return node.getStatus();
}
@Override
public int getOpenConnections() {
return node.getConnectionPool().getOpenConnections();
}
@Override
public long getTransferred() {
return node.getConnectionPool().getClientStatistics().getWritten();
}
@Override
public long getRead() {
return node.getConnectionPool().getClientStatistics().getRead();
}
@Override
public int getElected() {
return node.getElected();
}
@Override
public int getCacheConnections() {
return node.getNodeConfig().getCacheConnections();
}
@Override
public String getJvmRoute() {
return node.getNodeConfig().getJvmRoute();
}
@Override
public String getDomain() {
return node.getNodeConfig().getDomain();
}
@Override
public int getFlushWait() {
return node.getNodeConfig().getFlushwait();
}
@Override
public int getMaxConnections() {
return node.getNodeConfig().getMaxConnections();
}
@Override
public int getPing() {
return node.getNodeConfig().getPing();
}
@Override
public int getRequestQueueSize() {
return node.getNodeConfig().getRequestQueueSize();
}
@Override
public int getTimeout() {
return node.getNodeConfig().getTimeout();
}
@Override
public long getTtl() {
return node.getNodeConfig().getTtl();
}
@Override
public boolean isFlushPackets() {
return node.getNodeConfig().isFlushPackets();
}
@Override
public boolean isQueueNewRequests() {
return node.getNodeConfig().isQueueNewRequests();
}
@Override
public List getAliases() {
List ret = new ArrayList<>();
for(Node.VHostMapping host : node.getVHosts()) {
ret.addAll(host.getAliases());
}
return ret;
}
@Override
public void resetStatistics() {
node.getConnectionPool().getClientStatistics().reset();
}
}
private static class ContextImpl implements ModClusterStatus.Context {
private final Context context;
private ContextImpl(Context context) {
this.context = context;
}
@Override
public String getName() {
return context.getPath();
}
@Override
public boolean isEnabled() {
return context.isEnabled();
}
@Override
public boolean isStopped() {
return context.isStopped();
}
@Override
public int getRequests() {
return context.getActiveRequests();
}
@Override
public void enable() {
context.enable();
}
@Override
public void disable() {
context.disable();
}
@Override
public void stop() {
context.stop();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy