
org.elasticsearch.client.transport.TransportClientNodesService Maven / Gradle / Ivy
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.client.transport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.admin.cluster.node.liveness.LivenessRequest;
import org.elasticsearch.action.admin.cluster.node.liveness.LivenessResponse;
import org.elasticsearch.action.admin.cluster.node.liveness.TransportLivenessAction;
import org.elasticsearch.action.admin.cluster.state.ClusterStateAction;
import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse;
import org.elasticsearch.client.Requests;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.Randomness;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.common.util.concurrent.AbstractRunnable;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.core.internal.io.IOUtils;
import org.elasticsearch.threadpool.Scheduler;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.ConnectTransportException;
import org.elasticsearch.transport.ConnectionProfile;
import org.elasticsearch.transport.FutureTransportResponseHandler;
import org.elasticsearch.transport.NodeDisconnectedException;
import org.elasticsearch.transport.NodeNotConnectedException;
import org.elasticsearch.transport.PlainTransportFuture;
import org.elasticsearch.transport.Transport;
import org.elasticsearch.transport.TransportException;
import org.elasticsearch.transport.TransportRequestOptions;
import org.elasticsearch.transport.TransportResponseHandler;
import org.elasticsearch.transport.TransportService;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
final class TransportClientNodesService implements Closeable {
private static final Logger logger = LogManager.getLogger(TransportClientNodesService.class);
private final TimeValue nodesSamplerInterval;
private final TimeValue pingTimeout;
private final ClusterName clusterName;
private final TransportService transportService;
private final ThreadPool threadPool;
private final Version minCompatibilityVersion;
// nodes that are added to be discovered
private volatile List listedNodes = Collections.emptyList();
private final Object mutex = new Object();
private volatile List nodes = Collections.emptyList();
// Filtered nodes are nodes whose cluster name does not match the configured cluster name
private volatile List filteredNodes = Collections.emptyList();
private final AtomicInteger tempNodeIdGenerator = new AtomicInteger();
private final NodeSampler nodesSampler;
private volatile Scheduler.Cancellable nodesSamplerCancellable;
private final AtomicInteger randomNodeGenerator = new AtomicInteger(Randomness.get().nextInt());
private final boolean ignoreClusterName;
private volatile boolean closed;
private final TransportClient.HostFailureListener hostFailureListener;
// TODO: migrate this to use low level connections and single type channels
/** {@link ConnectionProfile} to use when to connecting to the listed nodes and doing a liveness check */
private static final ConnectionProfile LISTED_NODES_PROFILE;
static {
ConnectionProfile.Builder builder = new ConnectionProfile.Builder();
builder.addConnections(
1,
TransportRequestOptions.Type.BULK,
TransportRequestOptions.Type.PING,
TransportRequestOptions.Type.RECOVERY,
TransportRequestOptions.Type.REG,
TransportRequestOptions.Type.STATE
);
LISTED_NODES_PROFILE = builder.build();
}
TransportClientNodesService(
Settings settings,
TransportService transportService,
ThreadPool threadPool,
TransportClient.HostFailureListener hostFailureListener
) {
this.clusterName = ClusterName.CLUSTER_NAME_SETTING.get(settings);
this.transportService = transportService;
this.threadPool = threadPool;
this.minCompatibilityVersion = Version.CURRENT.minimumCompatibilityVersion();
this.nodesSamplerInterval = TransportClient.CLIENT_TRANSPORT_NODES_SAMPLER_INTERVAL.get(settings);
this.pingTimeout = TransportClient.CLIENT_TRANSPORT_PING_TIMEOUT.get(settings);
this.ignoreClusterName = TransportClient.CLIENT_TRANSPORT_IGNORE_CLUSTER_NAME.get(settings);
if (logger.isDebugEnabled()) {
logger.debug("node_sampler_interval[{}]", nodesSamplerInterval);
}
if (TransportClient.CLIENT_TRANSPORT_SNIFF.get(settings)) {
this.nodesSampler = new SniffNodesSampler();
} else {
this.nodesSampler = new SimpleNodeSampler();
}
this.hostFailureListener = hostFailureListener;
this.nodesSamplerCancellable = threadPool.schedule(new ScheduledNodeSampler(), nodesSamplerInterval, ThreadPool.Names.GENERIC);
}
public List transportAddresses() {
List lstBuilder = new ArrayList<>();
for (DiscoveryNode listedNode : listedNodes) {
lstBuilder.add(listedNode.getAddress());
}
return Collections.unmodifiableList(lstBuilder);
}
public List connectedNodes() {
return this.nodes;
}
public List filteredNodes() {
return this.filteredNodes;
}
public List listedNodes() {
return this.listedNodes;
}
public TransportClientNodesService addTransportAddresses(TransportAddress... transportAddresses) {
synchronized (mutex) {
if (closed) {
throw new IllegalStateException("transport client is closed, can't add an address");
}
List filtered = new ArrayList<>(transportAddresses.length);
for (TransportAddress transportAddress : transportAddresses) {
boolean found = false;
for (DiscoveryNode otherNode : listedNodes) {
if (otherNode.getAddress().equals(transportAddress)) {
found = true;
logger.debug("address [{}] already exists with [{}], ignoring...", transportAddress, otherNode);
break;
}
}
if (found == false) {
filtered.add(transportAddress);
}
}
if (filtered.isEmpty()) {
return this;
}
List builder = new ArrayList<>(listedNodes);
for (TransportAddress transportAddress : filtered) {
DiscoveryNode node = new DiscoveryNode(
"#transport#-" + tempNodeIdGenerator.incrementAndGet(),
transportAddress,
Collections.emptyMap(),
Collections.emptySet(),
minCompatibilityVersion
);
logger.debug("adding address [{}]", node);
builder.add(node);
}
listedNodes = Collections.unmodifiableList(builder);
nodesSampler.sample();
}
return this;
}
public TransportClientNodesService removeTransportAddress(TransportAddress transportAddress) {
synchronized (mutex) {
if (closed) {
throw new IllegalStateException("transport client is closed, can't remove an address");
}
List listNodesBuilder = new ArrayList<>();
for (DiscoveryNode otherNode : listedNodes) {
if (otherNode.getAddress().equals(transportAddress) == false) {
listNodesBuilder.add(otherNode);
} else {
logger.debug("removing address [{}] from listed nodes", otherNode);
}
}
listedNodes = Collections.unmodifiableList(listNodesBuilder);
List nodesBuilder = new ArrayList<>();
for (DiscoveryNode otherNode : nodes) {
if (otherNode.getAddress().equals(transportAddress) == false) {
nodesBuilder.add(otherNode);
} else {
logger.debug("disconnecting from node with address [{}]", otherNode);
transportService.disconnectFromNode(otherNode);
}
}
nodes = Collections.unmodifiableList(nodesBuilder);
nodesSampler.sample();
}
return this;
}
public void execute(NodeListenerCallback callback, ActionListener listener) {
// we first read nodes before checking the closed state; this
// is because otherwise we could be subject to a race where we
// read the state as not being closed, and then the client is
// closed and the nodes list is cleared, and then a
// NoNodeAvailableException is thrown
// it is important that the order of first setting the state of
// closed and then clearing the list of nodes is maintained in
// the close method
final List nodes = this.nodes;
if (closed) {
listener.onFailure(new IllegalStateException("transport client is closed"));
return;
}
if (nodes.isEmpty()) {
String message = String.format(Locale.ROOT, "None of the configured nodes are available: %s", this.listedNodes);
listener.onFailure(new NoNodeAvailableException(message));
return;
}
int index = getNodeNumber();
RetryListener retryListener = new RetryListener<>(callback, listener, nodes, index, hostFailureListener);
DiscoveryNode node = retryListener.getNode(0);
try {
callback.doWithNode(node, retryListener);
} catch (Exception e) {
try {
// this exception can't come from the TransportService as it doesn't throw exception at all
listener.onFailure(e);
} finally {
retryListener.maybeNodeFailed(node, e);
}
}
}
public static class RetryListener implements ActionListener {
private final NodeListenerCallback callback;
private final ActionListener listener;
private final List nodes;
private final int index;
private final TransportClient.HostFailureListener hostFailureListener;
private volatile int i;
RetryListener(
NodeListenerCallback callback,
ActionListener listener,
List nodes,
int index,
TransportClient.HostFailureListener hostFailureListener
) {
this.callback = callback;
this.listener = listener;
this.nodes = nodes;
this.index = index;
this.hostFailureListener = hostFailureListener;
}
@Override
public void onResponse(Response response) {
listener.onResponse(response);
}
@Override
public void onFailure(Exception e) {
Throwable throwable = ExceptionsHelper.unwrapCause(e);
if (throwable instanceof ConnectTransportException) {
maybeNodeFailed(getNode(this.i), (ConnectTransportException) throwable);
int i = ++this.i;
if (i >= nodes.size()) {
listener.onFailure(new NoNodeAvailableException("None of the configured nodes were available: " + nodes, e));
} else {
try {
callback.doWithNode(getNode(i), this);
} catch (final Exception inner) {
inner.addSuppressed(e);
// this exception can't come from the TransportService as it doesn't throw exceptions at all
listener.onFailure(inner);
}
}
} else {
listener.onFailure(e);
}
}
final DiscoveryNode getNode(int i) {
return nodes.get((index + i) % nodes.size());
}
final void maybeNodeFailed(DiscoveryNode node, Exception ex) {
if (ex instanceof NodeDisconnectedException || ex instanceof NodeNotConnectedException) {
hostFailureListener.onNodeDisconnected(node, ex);
}
}
}
@Override
public void close() {
synchronized (mutex) {
if (closed) {
return;
}
closed = true;
if (nodesSamplerCancellable != null) {
nodesSamplerCancellable.cancel();
}
for (DiscoveryNode node : nodes) {
transportService.disconnectFromNode(node);
}
for (DiscoveryNode listedNode : listedNodes) {
transportService.disconnectFromNode(listedNode);
}
nodes = Collections.emptyList();
}
}
private int getNodeNumber() {
int index = randomNodeGenerator.incrementAndGet();
if (index < 0) {
index = 0;
randomNodeGenerator.set(0);
}
return index;
}
abstract class NodeSampler {
public void sample() {
synchronized (mutex) {
if (closed) {
return;
}
doSample();
}
}
protected abstract void doSample();
/**
* Establishes the node connections. If validateInHandshake is set to true, the connection will fail if
* node returned in the handshake response is different than the discovery node.
*/
List establishNodeConnections(Set nodes) {
for (Iterator it = nodes.iterator(); it.hasNext();) {
DiscoveryNode node = it.next();
if (transportService.nodeConnected(node) == false) {
try {
logger.trace("connecting to node [{}]", node);
transportService.connectToNode(node);
} catch (Exception e) {
it.remove();
logger.debug(() -> new ParameterizedMessage("failed to connect to discovered node [{}]", node), e);
}
}
}
return Collections.unmodifiableList(new ArrayList<>(nodes));
}
}
class ScheduledNodeSampler implements Runnable {
@Override
public void run() {
try {
nodesSampler.sample();
if (closed == false) {
nodesSamplerCancellable = threadPool.schedule(this, nodesSamplerInterval, ThreadPool.Names.GENERIC);
}
} catch (Exception e) {
logger.warn("failed to sample", e);
}
}
}
class SimpleNodeSampler extends NodeSampler {
@Override
protected void doSample() {
HashSet newNodes = new HashSet<>();
ArrayList newFilteredNodes = new ArrayList<>();
for (DiscoveryNode listedNode : listedNodes) {
try (Transport.Connection connection = transportService.openConnection(listedNode, LISTED_NODES_PROFILE)) {
final PlainTransportFuture handler = new PlainTransportFuture<>(
new FutureTransportResponseHandler() {
@Override
public LivenessResponse read(StreamInput in) throws IOException {
return new LivenessResponse(in);
}
}
);
transportService.sendRequest(
connection,
TransportLivenessAction.NAME,
new LivenessRequest(),
TransportRequestOptions.of(pingTimeout, TransportRequestOptions.Type.STATE),
handler
);
final LivenessResponse livenessResponse = handler.txGet();
if (ignoreClusterName == false && clusterName.equals(livenessResponse.getClusterName()) == false) {
logger.warn("node {} not part of the cluster {}, ignoring...", listedNode, clusterName);
newFilteredNodes.add(listedNode);
} else {
// use discovered information but do keep the original transport address,
// so people can control which address is exactly used.
DiscoveryNode nodeWithInfo = livenessResponse.getDiscoveryNode();
newNodes.add(
new DiscoveryNode(
nodeWithInfo.getName(),
nodeWithInfo.getId(),
nodeWithInfo.getEphemeralId(),
nodeWithInfo.getHostName(),
nodeWithInfo.getHostAddress(),
listedNode.getAddress(),
nodeWithInfo.getAttributes(),
nodeWithInfo.getRoles(),
nodeWithInfo.getVersion()
)
);
}
} catch (ConnectTransportException e) {
logger.debug(() -> new ParameterizedMessage("failed to connect to node [{}], ignoring...", listedNode), e);
hostFailureListener.onNodeDisconnected(listedNode, e);
} catch (Exception e) {
logger.info(() -> new ParameterizedMessage("failed to get node info for {}, disconnecting...", listedNode), e);
}
}
nodes = establishNodeConnections(newNodes);
filteredNodes = Collections.unmodifiableList(newFilteredNodes);
}
}
class SniffNodesSampler extends NodeSampler {
@Override
protected void doSample() {
// the nodes we are going to ping include the core listed nodes that were added
// and the last round of discovered nodes
Set nodesToPing = new HashSet<>();
for (DiscoveryNode node : listedNodes) {
nodesToPing.add(node);
}
for (DiscoveryNode node : nodes) {
nodesToPing.add(node);
}
final CountDownLatch latch = new CountDownLatch(nodesToPing.size());
final ConcurrentMap clusterStateResponses = ConcurrentCollections.newConcurrentMap();
try {
for (final DiscoveryNode nodeToPing : nodesToPing) {
threadPool.executor(ThreadPool.Names.MANAGEMENT).execute(new AbstractRunnable() {
/**
* we try to reuse existing connections but if needed we will open a temporary connection
* that will be closed at the end of the execution.
*/
Transport.Connection connectionToClose = null;
void onDone() {
try {
IOUtils.closeWhileHandlingException(connectionToClose);
} finally {
latch.countDown();
}
}
@Override
public void onFailure(Exception e) {
onDone();
if (e instanceof ConnectTransportException) {
logger.debug(() -> new ParameterizedMessage("failed to connect to node [{}], ignoring...", nodeToPing), e);
hostFailureListener.onNodeDisconnected(nodeToPing, e);
} else {
logger.info(
() -> new ParameterizedMessage(
"failed to get local cluster state info for {}, disconnecting...",
nodeToPing
),
e
);
}
}
@Override
protected void doRun() throws Exception {
Transport.Connection pingConnection = null;
if (nodes.contains(nodeToPing)) {
try {
pingConnection = transportService.getConnection(nodeToPing);
} catch (NodeNotConnectedException e) {
// will use a temp connection
}
}
if (pingConnection == null) {
logger.trace("connecting to cluster node [{}]", nodeToPing);
connectionToClose = transportService.openConnection(nodeToPing, LISTED_NODES_PROFILE);
pingConnection = connectionToClose;
}
transportService.sendRequest(
pingConnection,
ClusterStateAction.NAME,
Requests.clusterStateRequest().clear().nodes(true).local(true),
TransportRequestOptions.of(pingTimeout, TransportRequestOptions.Type.STATE),
new TransportResponseHandler() {
@Override
public ClusterStateResponse read(StreamInput in) throws IOException {
return new ClusterStateResponse(in);
}
@Override
public String executor() {
return ThreadPool.Names.SAME;
}
@Override
public void handleResponse(ClusterStateResponse response) {
clusterStateResponses.put(nodeToPing, response);
onDone();
}
@Override
public void handleException(TransportException e) {
logger.info(
() -> new ParameterizedMessage(
"failed to get local cluster state for {}, disconnecting...",
nodeToPing
),
e
);
try {
hostFailureListener.onNodeDisconnected(nodeToPing, e);
} finally {
onDone();
}
}
}
);
}
});
}
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
HashSet newNodes = new HashSet<>();
HashSet newFilteredNodes = new HashSet<>();
for (Map.Entry entry : clusterStateResponses.entrySet()) {
if (ignoreClusterName == false && clusterName.equals(entry.getValue().getClusterName()) == false) {
logger.warn(
"node {} not part of the cluster {}, ignoring...",
entry.getValue().getState().nodes().getLocalNode(),
clusterName
);
newFilteredNodes.add(entry.getKey());
continue;
}
for (DiscoveryNode node : entry.getValue().getState().nodes().getDataNodes().values()) {
newNodes.add(node);
}
}
nodes = establishNodeConnections(newNodes);
filteredNodes = Collections.unmodifiableList(new ArrayList<>(newFilteredNodes));
}
}
public interface NodeListenerCallback {
void doWithNode(DiscoveryNode node, ActionListener listener);
}
// pkg private for testing
void doSample() {
nodesSampler.doSample();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy