package org.xbib.elasticsearch.helper.client; import com.google.common.collect.ImmutableMap; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionModule; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.GenericAction; import org.elasticsearch.action.TransportActionNodeProxy; 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.cache.recycler.PageCacheRecycler; import org.elasticsearch.client.support.AbstractClient; import org.elasticsearch.client.support.Headers; import org.elasticsearch.client.transport.ClientTransportModule; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterNameModule; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.component.LifecycleComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.Injector; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.inject.ModulesBuilder; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsModule; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.indices.breaker.CircuitBreakerModule; import org.elasticsearch.monitor.MonitorService; import org.elasticsearch.node.internal.InternalSettingsPreparer; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.PluginsModule; import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.search.SearchModule; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPoolModule; import org.elasticsearch.transport.ConnectTransportException; import org.elasticsearch.transport.FutureTransportResponseHandler; import org.elasticsearch.transport.TransportModule; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; import org.elasticsearch.client.transport.TransportClient.HostFailureListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.elasticsearch.common.settings.Settings.settingsBuilder; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; /** * Stripped-down transport client without node sampling. * Merged together: original TransportClient, TransportClientNodesServce, TransportClientProxy * Configurable ping interval setting added */ public class TransportClient extends AbstractClient { public static Builder builder() { return new Builder(); } public static class Builder { private Settings settings = Settings.EMPTY; private List<Class<? extends Plugin>> pluginClasses = new ArrayList<>(); private HostFailureListener hostFailedListener; public Builder settings(Settings.Builder settings) { return settings(settings.build()); } public Builder settings(Settings settings) { this.settings = settings; return this; } public Builder addPlugin(Class<? extends Plugin> pluginClass) { pluginClasses.add(pluginClass); return this; } public TransportClient build() { Settings settings = InternalSettingsPreparer.prepareSettings(this.settings); settings = settingsBuilder() .put("transport.ping.schedule", this.settings.get("ping.interval", "30s")) .put(settings) .put("network.server", false) .put("node.client", true) .put(CLIENT_TYPE_SETTING, CLIENT_TYPE) .build(); PluginsService pluginsService = new PluginsService(settings, null, null, pluginClasses); this.settings = pluginsService.updatedSettings(); Version version = Version.CURRENT; final ThreadPool threadPool = new ThreadPool(settings); final NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(); boolean success = false; try { ModulesBuilder modules = new ModulesBuilder(); modules.add(new Version.Module(version)); // plugin modules must be added here, before others or we can get crazy injection errors... for (Module pluginModule : pluginsService.nodeModules()) { modules.add(pluginModule); } modules.add(new PluginsModule(pluginsService)); modules.add(new SettingsModule(this.settings)); modules.add(new NetworkModule(namedWriteableRegistry)); modules.add(new ClusterNameModule(this.settings)); modules.add(new ThreadPoolModule(threadPool)); modules.add(new TransportModule(this.settings, namedWriteableRegistry)); modules.add(new SearchModule() { @Override protected void configure() { // noop } }); modules.add(new ActionModule(true)); modules.add(new ClientTransportModule(hostFailedListener)); modules.add(new CircuitBreakerModule(this.settings)); pluginsService.processModules(modules); Injector injector = modules.createInjector(); injector.getInstance(TransportService.class).start(); TransportClient transportClient = new TransportClient(injector); success = true; return transportClient; } finally { if (!success) { ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS); } } } public Builder setHostFailedListener(HostFailureListener hostFailedListener) { this.hostFailedListener = hostFailedListener; return this; } } public static final String CLIENT_TYPE = "transport"; private final Injector injector; private final ProxyActionMap proxyActionMap; private final long pingTimeout; private final ClusterName clusterName; private final TransportService transportService; private final Version minCompatibilityVersion; private final Headers headers; private final AtomicInteger tempNodeId = new AtomicInteger(); private final AtomicInteger nodeCounter = new AtomicInteger(); private final Object mutex = new Object(); private volatile List<DiscoveryNode> listedNodes = Collections.emptyList(); private volatile List<DiscoveryNode> nodes = Collections.emptyList(); private volatile List<DiscoveryNode> filteredNodes = Collections.emptyList(); private volatile boolean closed; private TransportClient(Injector injector) { super(injector.getInstance(Settings.class), injector.getInstance(ThreadPool.class), injector.getInstance(Headers.class)); this.injector = injector; this.clusterName = injector.getInstance(ClusterName.class); this.transportService = injector.getInstance(TransportService.class); this.minCompatibilityVersion = injector.getInstance(Version.class).minimumCompatibilityVersion(); this.headers = injector.getInstance(Headers.class); this.pingTimeout = this.settings.getAsTime("client.transport.ping_timeout", timeValueSeconds(5)).millis(); this.proxyActionMap = injector.getInstance(ProxyActionMap.class); } /** * Returns the current registered transport addresses to use. * @return list of transport addresess */ public List<TransportAddress> transportAddresses() { List<TransportAddress> lstBuilder = new ArrayList<>(); for (DiscoveryNode listedNode : listedNodes) { lstBuilder.add(listedNode.address()); } return Collections.unmodifiableList(lstBuilder); } /** * Returns the current connected transport nodes that this client will use. * The nodes include all the nodes that are currently alive based on the transport * addresses provided. * @return list of nodes */ public List<DiscoveryNode> connectedNodes() { return this.nodes; } /** * The list of filtered nodes that were not connected to, for example, due to * mismatch in cluster name. * @return list of nodes */ public List<DiscoveryNode> filteredNodes() { return this.filteredNodes; } /** * Returns the listed nodes in the transport client (ones added to it). * @return list of nodes */ public List<DiscoveryNode> listedNodes() { return this.listedNodes; } /** * Adds a list of transport addresses that will be used to connect to. * The Node this transport address represents will be used if its possible to connect to it. * If it is unavailable, it will be automatically connected to once it is up. * In order to get the list of all the current connected nodes, please see {@link #connectedNodes()}. * @param discoveryNodes nodes * @return this transport client */ public TransportClient addDiscoveryNodes(DiscoveryNodes discoveryNodes) { Collection<InetSocketTransportAddress> addresses = new ArrayList<>(); for (DiscoveryNode discoveryNode : discoveryNodes) { addresses.add((InetSocketTransportAddress) discoveryNode.address()); } addTransportAddresses(addresses); return this; } public TransportClient addTransportAddresses(Collection<InetSocketTransportAddress> transportAddresses) { synchronized (mutex) { if (closed) { throw new IllegalStateException("transport client is closed, can't add addresses"); } List<TransportAddress> filtered = new ArrayList<>(transportAddresses.size()); 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; } List<DiscoveryNode> discoveryNodeList = new ArrayList<>(); discoveryNodeList.addAll(listedNodes()); for (TransportAddress transportAddress : filtered) { DiscoveryNode node = new DiscoveryNode("#transport#-" + tempNodeId.incrementAndGet(), transportAddress, minCompatibilityVersion); logger.debug("adding address [{}]", node); discoveryNodeList.add(node); } listedNodes = Collections.unmodifiableList(discoveryNodeList); connect(); } return this; } /** * Removes a transport address from the list of transport addresses that are used to connect to. * @param transportAddress transport address to remove * @return this transport client */ public TransportClient removeTransportAddress(TransportAddress transportAddress) { synchronized (mutex) { if (closed) { throw new IllegalStateException("transport client is closed, can't remove an address"); } List<DiscoveryNode> builder = new ArrayList<>(); for (DiscoveryNode otherNode : listedNodes) { if (!otherNode.address().equals(transportAddress)) { builder.add(otherNode); } else { logger.debug("removing address [{}]", otherNode); } } listedNodes = Collections.unmodifiableList(builder); } return this; } @Override public void close() { synchronized (mutex) { if (closed) { return; } closed = true; for (DiscoveryNode node : nodes) { transportService.disconnectFromNode(node); } for (DiscoveryNode listedNode : listedNodes) { transportService.disconnectFromNode(listedNode); } nodes = Collections.emptyList(); } injector.getInstance(TransportService.class).close(); try { injector.getInstance(MonitorService.class).close(); } catch (Exception e) { // ignore, might not be bounded } for (Class<? extends LifecycleComponent> plugin : injector.getInstance(PluginsService.class).nodeServices()) { injector.getInstance(plugin).close(); } try { ThreadPool.terminate(injector.getInstance(ThreadPool.class), 10, TimeUnit.SECONDS); } catch (Exception e) { // ignore } injector.getInstance(PageCacheRecycler.class).close(); } private void connect() { Set<DiscoveryNode> newNodes = new HashSet<>(); Set<DiscoveryNode> newFilteredNodes = new HashSet<>(); for (DiscoveryNode listedNode : listedNodes) { if (!transportService.nodeConnected(listedNode)) { try { logger.trace("connecting to listed node (light) [{}]", listedNode); transportService.connectToNodeLight(listedNode); } catch (Throwable e) { logger.debug("failed to connect to node [{}], removed from nodes list", e, listedNode); continue; } } try { LivenessResponse livenessResponse = transportService.submitRequest(listedNode, TransportLivenessAction.NAME, headers.applyTo(new LivenessRequest()), TransportRequestOptions.builder().withType(TransportRequestOptions.Type.STATE) .withTimeout(pingTimeout).build(), new FutureTransportResponseHandler<LivenessResponse>() { @Override public LivenessResponse newInstance() { return new LivenessResponse(); } }).txGet(); if (!clusterName.equals(livenessResponse.getClusterName())) { logger.warn("node {} not part of the cluster {}, ignoring...", listedNode, clusterName); newFilteredNodes.add(listedNode); } else if (livenessResponse.getDiscoveryNode() != null) { DiscoveryNode nodeWithInfo = livenessResponse.getDiscoveryNode(); newNodes.add(new DiscoveryNode(nodeWithInfo.name(), nodeWithInfo.id(), nodeWithInfo.getHostName(), nodeWithInfo.getHostAddress(), listedNode.address(), nodeWithInfo.attributes(), nodeWithInfo.version())); } else { logger.debug("node {} didn't return any discovery info, temporarily using transport discovery node", listedNode); newNodes.add(listedNode); } } catch (Throwable e) { logger.info("failed to get node info for {}, disconnecting...", e, listedNode); transportService.disconnectFromNode(listedNode); } } 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 (Throwable e) { it.remove(); logger.debug("failed to connect to discovered node [" + node + "]", e); } } } this.nodes = Collections.unmodifiableList(new ArrayList<>(newNodes)); this.filteredNodes = Collections.unmodifiableList(new ArrayList<>(newFilteredNodes)); } @Override @SuppressWarnings("unchecked") protected <Request extends ActionRequest, Response extends ActionResponse, RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder>> void doExecute(Action<Request, Response, RequestBuilder> action, final Request request, ActionListener<Response> listener) { final TransportActionNodeProxy<Request, Response> proxyAction = proxyActionMap.getProxies().get(action); if (proxyAction == null) { throw new IllegalStateException("undefined action " + action); } NodeListenerCallback<Response> callback = new NodeListenerCallback<Response>() { @Override public void doWithNode(DiscoveryNode node, ActionListener<Response> listener) { proxyAction.execute(node, request, listener); } }; List<DiscoveryNode> nodes = this.nodes; if (nodes.isEmpty()) { throw new NoNodeAvailableException("none of the configured nodes are available: " + this.listedNodes); } int index = nodeCounter.incrementAndGet(); if (index < 0) { index = 0; nodeCounter.set(0); } RetryListener<Response> retryListener = new RetryListener<>(callback, listener, nodes, index); DiscoveryNode node = nodes.get((index) % nodes.size()); try { callback.doWithNode(node, retryListener); } catch (Throwable t) { listener.onFailure(t); } } interface NodeListenerCallback<Response> { void doWithNode(DiscoveryNode node, ActionListener<Response> listener); } static class RetryListener<Response> implements ActionListener<Response> { private final ESLogger logger = ESLoggerFactory.getLogger(RetryListener.class.getName()); private final NodeListenerCallback<Response> callback; private final ActionListener<Response> listener; private final List<DiscoveryNode> nodes; private final int index; private volatile int n; public RetryListener(NodeListenerCallback<Response> callback, ActionListener<Response> listener, List<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 n = ++this.n; if (n >= nodes.size()) { listener.onFailure(new NoNodeAvailableException("none of the configured nodes were available: " + nodes, e)); } else { try { logger.warn("retrying on anoher node (n={}, nodes={})", n, nodes.size()); callback.doWithNode(nodes.get((index + n) % nodes.size()), this); } catch (final Throwable t) { listener.onFailure(t); } } } else { listener.onFailure(e); } } } public static class ProxyActionMap { private final ImmutableMap<Action, TransportActionNodeProxy> proxies; @Inject @SuppressWarnings("unchecked") public ProxyActionMap(Settings settings, TransportService transportService, Map<String, GenericAction> actions) { MapBuilder<Action, TransportActionNodeProxy> actionsBuilder = new MapBuilder<>(); for (GenericAction action : actions.values()) { if (action instanceof Action) { actionsBuilder.put((Action) action, new TransportActionNodeProxy(settings, action, transportService)); } } this.proxies = actionsBuilder.immutableMap(); } public ImmutableMap<Action, TransportActionNodeProxy> getProxies() { return proxies; } } }