/* * Licensed to ElasticSearch and Shay Banon under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. ElasticSearch licenses this * file to you 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 org.elasticsearch.client.transport; import org.elasticsearch.transport.TransportService; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.elasticsearch.ElasticSearchException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoAction; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; import org.elasticsearch.client.AdminRequests; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.*; import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicInteger; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; /** * */ public class TransportClientNodesService extends AbstractComponent { private final TimeValue nodesSamplerInterval; private final long pingTimeout; private final ClusterName clusterName; private final TransportService transportService; private final ThreadPool threadPool; // nodes that are added to be discovered private volatile ImmutableList<DiscoveryNode> listedNodes = ImmutableList.of(); private final Object transportMutex = new Object(); private volatile ImmutableList<DiscoveryNode> nodes = ImmutableList.of(); private final AtomicInteger tempNodeIdGenerator = new AtomicInteger(); private final NodeSampler nodesSampler; private volatile ScheduledFuture nodesSamplerFuture; private final AtomicInteger randomNodeGenerator = new AtomicInteger(); private final boolean ignoreClusterName; private volatile boolean closed; @Inject public TransportClientNodesService(Settings settings, ClusterName clusterName, TransportService transportService, ThreadPool threadPool) { super(settings); this.clusterName = clusterName; this.transportService = transportService; this.threadPool = threadPool; this.nodesSamplerInterval = componentSettings.getAsTime("nodes_sampler_interval", timeValueSeconds(5)); this.pingTimeout = componentSettings.getAsTime("ping_timeout", timeValueSeconds(5)).millis(); this.ignoreClusterName = componentSettings.getAsBoolean("ignore_cluster_name", false); if (logger.isDebugEnabled()) { logger.debug("node_sampler_interval[" + nodesSamplerInterval + "]"); } if (componentSettings.getAsBoolean("sniff", false)) { this.nodesSampler = new SniffNodesSampler(); } else { this.nodesSampler = new SimpleNodeSampler(); } this.nodesSamplerFuture = threadPool.schedule(nodesSamplerInterval, ThreadPool.Names.GENERIC, new ScheduledNodeSampler()); // we want the transport service to throw connect exceptions, so we can retry transportService.throwConnectException(true); } public ImmutableList<TransportAddress> transportAddresses() { ImmutableList.Builder<TransportAddress> lstBuilder = ImmutableList.builder(); for (DiscoveryNode listedNode : listedNodes) { lstBuilder.add(listedNode.address()); } return lstBuilder.build(); } public ImmutableList<DiscoveryNode> connectedNodes() { return this.nodes; } public ImmutableList<DiscoveryNode> listedNodes() { return this.listedNodes; } public TransportClientNodesService addTransportAddresses(TransportAddress... transportAddresses) { synchronized (transportMutex) { List<TransportAddress> filtered = Lists.newArrayListWithExpectedSize(transportAddresses.length); for (TransportAddress transportAddress : transportAddresses) { boolean found = false; for (DiscoveryNode otherNode : listedNodes) { if (otherNode.address().equals(transportAddress)) { found = true; logger.debug("address [{}] already exists with [{}], ignoring...", transportAddress, otherNode); break; } } if (!found) { filtered.add(transportAddress); } } if (filtered.isEmpty()) { return this; } ImmutableList.Builder<DiscoveryNode> builder = ImmutableList.builder(); builder.addAll(listedNodes()); for (TransportAddress transportAddress : filtered) { DiscoveryNode node = new DiscoveryNode("#transport#-" + tempNodeIdGenerator.incrementAndGet(), transportAddress); logger.debug("adding address [{}]", node); builder.add(node); } listedNodes = builder.build(); } nodesSampler.sample(); return this; } public TransportClientNodesService removeTransportAddress(TransportAddress transportAddress) { synchronized (transportMutex) { ImmutableList.Builder<DiscoveryNode> builder = ImmutableList.builder(); for (DiscoveryNode otherNode : listedNodes) { if (!otherNode.address().equals(transportAddress)) { builder.add(otherNode); } else { logger.debug("removing address [{}]", otherNode); } } listedNodes = builder.build(); } nodesSampler.sample(); return this; } public <T> T execute(NodeCallback<T> callback) throws ElasticSearchException { ImmutableList<DiscoveryNode> nodes = this.nodes; if (nodes.isEmpty()) { throw new NoNodeAvailableException(); } int index = randomNodeGenerator.incrementAndGet(); if (index < 0) { index = 0; randomNodeGenerator.set(0); } for (int i = 0; i < nodes.size(); i++) { DiscoveryNode node = nodes.get((index + i) % nodes.size()); try { return callback.doWithNode(node); } catch (ElasticSearchException e) { if (!(e.unwrapCause() instanceof ConnectTransportException)) { throw e; } } } throw new NoNodeAvailableException(); } public <Response> void execute(NodeListenerCallback<Response> callback, ActionListener<Response> listener) throws ElasticSearchException { ImmutableList<DiscoveryNode> nodes = this.nodes; if (nodes.isEmpty()) { throw new NoNodeAvailableException(); } int index = randomNodeGenerator.incrementAndGet(); if (index < 0) { index = 0; randomNodeGenerator.set(0); } RetryListener<Response> retryListener = new RetryListener<Response>(callback, listener, nodes, index); try { callback.doWithNode(nodes.get((index) % nodes.size()), retryListener); } catch (ElasticSearchException e) { if (e.unwrapCause() instanceof ConnectTransportException) { retryListener.onFailure(e); } else { throw e; } } } public static class RetryListener<Response> implements ActionListener<Response> { private final NodeListenerCallback<Response> callback; private final ActionListener<Response> listener; private final ImmutableList<DiscoveryNode> nodes; private final int index; private volatile int i; public RetryListener(NodeListenerCallback<Response> callback, ActionListener<Response> listener, ImmutableList<DiscoveryNode> nodes, int index) { this.callback = callback; this.listener = listener; this.nodes = nodes; this.index = index; } @Override public void onResponse(Response response) { listener.onResponse(response); } @Override public void onFailure(Throwable e) { if (ExceptionsHelper.unwrapCause(e) instanceof ConnectTransportException) { int i = ++this.i; if (i == nodes.size()) { listener.onFailure(new NoNodeAvailableException()); } else { try { callback.doWithNode(nodes.get((index + i) % nodes.size()), this); } catch (Exception e1) { // retry the next one... onFailure(e); } } } else { listener.onFailure(e); } } } public void close() { closed = true; nodesSamplerFuture.cancel(true); for (DiscoveryNode node : nodes) { transportService.disconnectFromNode(node); } for (DiscoveryNode listedNode : listedNodes) { transportService.disconnectFromNode(listedNode); } nodes = ImmutableList.of(); } interface NodeSampler { void sample(); } class ScheduledNodeSampler implements Runnable { @Override public void run() { try { nodesSampler.sample(); if (!closed) { nodesSamplerFuture = threadPool.schedule(nodesSamplerInterval, ThreadPool.Names.GENERIC, this); } } catch (Exception e) { logger.warn("failed to sample", e); } } } class SimpleNodeSampler implements NodeSampler { @Override public synchronized void sample() { if (closed) { return; } HashSet<DiscoveryNode> newNodes = new HashSet<DiscoveryNode>(); for (DiscoveryNode node : listedNodes) { if (!transportService.nodeConnected(node)) { try { transportService.connectToNode(node); } catch (Exception e) { logger.debug("failed to connect to node [{}], removed from nodes list", e, node); continue; } } try { NodesInfoResponse nodeInfo = transportService.submitRequest(node, NodesInfoAction.NAME, AdminRequests.nodesInfoRequest("_local"), TransportRequestOptions.options().withHighType().withTimeout(pingTimeout), new FutureTransportResponseHandler<NodesInfoResponse>() { @Override public NodesInfoResponse newInstance() { return new NodesInfoResponse(); } }).txGet(); if (!ignoreClusterName && !clusterName.equals(nodeInfo.clusterName())) { logger.warn("node {} not part of the cluster {}, ignoring...", node, clusterName); } else { newNodes.add(node); } } catch (Exception e) { logger.info("failed to get node info for {}, disconnecting...", e, node); transportService.disconnectFromNode(node); } } nodes = new ImmutableList.Builder<DiscoveryNode>().addAll(newNodes).build(); } } class SniffNodesSampler implements NodeSampler { @Override public synchronized void sample() { if (closed) { return; } // the nodes we are going to ping include the core listed nodes that were added // and the last round of discovered nodes Set<DiscoveryNode> nodesToPing = Sets.newHashSet(); for (DiscoveryNode node : listedNodes) { nodesToPing.add(node); } for (DiscoveryNode node : nodes) { nodesToPing.add(node); } final CountDownLatch latch = new CountDownLatch(nodesToPing.size()); final Queue<ClusterStateResponse> clusterStateResponses = ConcurrentCollections.newQueue(); for (final DiscoveryNode listedNode : nodesToPing) { threadPool.executor(ThreadPool.Names.MANAGEMENT).execute(new Runnable() { @Override public void run() { try { if (!transportService.nodeConnected(listedNode)) { try { // if its one of hte actual nodes we will talk to, not to listed nodes, fully connect if (nodes.contains(listedNode)) { logger.trace("connecting to cluster node [{}]", listedNode); transportService.connectToNode(listedNode); } else { // its a listed node, light connect to it... logger.trace("connecting to listed node (light) [{}]", listedNode); transportService.connectToNodeLight(listedNode); } } catch (Exception e) { logger.debug("failed to connect to node [{}], ignoring...", e, listedNode); latch.countDown(); return; } } transportService.sendRequest(listedNode, ClusterStateAction.NAME, AdminRequests.clusterStateRequest() .filterAll().filterNodes(false).local(true), TransportRequestOptions.options().withHighType().withTimeout(pingTimeout), new BaseTransportResponseHandler<ClusterStateResponse>() { @Override public ClusterStateResponse newInstance() { return new ClusterStateResponse(); } @Override public String executor() { return ThreadPool.Names.SAME; } @Override public void handleResponse(ClusterStateResponse response) { clusterStateResponses.add(response); latch.countDown(); } @Override public void handleException(TransportException e) { logger.info("failed to get local cluster state for {}, disconnecting...", e, listedNode); transportService.disconnectFromNode(listedNode); latch.countDown(); } }); } catch (Exception e) { logger.info("failed to get local cluster state info for {}, disconnecting...", e, listedNode); transportService.disconnectFromNode(listedNode); latch.countDown(); } } }); } try { latch.await(); } catch (InterruptedException e) { return; } HashSet<DiscoveryNode> newNodes = new HashSet<DiscoveryNode>(); for (ClusterStateResponse clusterStateResponse : clusterStateResponses) { if (!ignoreClusterName && !clusterName.equals(clusterStateResponse.clusterName())) { logger.warn("node {} not part of the cluster {}, ignoring...", clusterStateResponse.state().nodes().localNode(), clusterName); } for (DiscoveryNode node : clusterStateResponse.state().nodes().dataNodes().values()) { newNodes.add(node); } } // now, make sure we are connected to all the updated nodes for (Iterator<DiscoveryNode> it = newNodes.iterator(); it.hasNext(); ) { DiscoveryNode node = it.next(); if (!transportService.nodeConnected(node)) { try { logger.trace("connecting to node [{}]", node); transportService.connectToNode(node); } catch (Exception e) { it.remove(); logger.debug("failed to connect to discovered node [" + node + "]", e); } } } nodes = new ImmutableList.Builder<DiscoveryNode>().addAll(newNodes).build(); } } public static interface NodeCallback<T> { T doWithNode(DiscoveryNode node) throws ElasticSearchException; } public static interface NodeListenerCallback<Response> { void doWithNode(DiscoveryNode node, ActionListener<Response> listener) throws ElasticSearchException; } }