All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.tinkerpop.gremlin.driver.Client Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.apache.tinkerpop.gremlin.driver;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.tinkerpop.gremlin.driver.exception.ConnectionException;
import org.apache.tinkerpop.gremlin.driver.exception.NoHostAvailableException;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLException;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * A {@code Client} is constructed from a {@link Cluster} and represents a way to send messages to Gremlin Server.
 * This class itself is a base class as there are different implementations that provide differing kinds of
 * functionality.  See the implementations for specifics on their individual usage.
 * 

* The {@code Client} is designed to be re-used and shared across threads. * * @author Stephen Mallette (http://stephen.genoprime.com) */ public abstract class Client { private static final Logger logger = LoggerFactory.getLogger(Client.class); public static final String TOO_MANY_IN_FLIGHT_REQUESTS = "Number of active requests (%s) exceeds pool size (%s). " + "Consider increasing the value for maxConnectionPoolSize."; protected final Cluster cluster; protected volatile boolean initialized; private static final Random random = new Random(); Client(final Cluster cluster) { this.cluster = cluster; } /** * Makes any initial changes to the builder and returns the constructed {@link RequestMessage}. Implementers * may choose to override this message to append data to the request before sending. By default, this method * will simply return the {@code builder} passed in by the caller. */ public RequestMessage.Builder buildMessage(final RequestMessage.Builder builder) { return builder; } /** * Called in the {@link #init} method. */ protected abstract void initializeImplementation(); /** * Chooses a {@link Connection} to write the message to. */ protected abstract Connection chooseConnection(final RequestMessage msg) throws TimeoutException, ConnectionException; /** * Asynchronous close of the {@code Client}. */ public abstract CompletableFuture closeAsync(); /** * Create a new {@code Client} that aliases the specified {@link Graph} or {@link TraversalSource} name on the * server to a variable called "g" for the context of the requests made through that {@code Client}. * * @param graphOrTraversalSource rebinds the specified global Gremlin Server variable to "g" */ public Client alias(final String graphOrTraversalSource) { return new AliasClusteredClient(this, graphOrTraversalSource); } /** * Initializes the client which typically means that a connection is established to the server. Depending on the * implementation and configuration this blocking call may take some time. This method will be called * automatically if it is not called directly and multiple calls will not have effect. */ public synchronized Client init() { if (initialized) return this; logger.debug("Initializing client on cluster [{}]", cluster); cluster.init(); initializeImplementation(); initialized = true; return this; } /** * Submits a Gremlin script to the server and returns a {@link ResultSet} once the write of the request is * complete. * * @param gremlin the gremlin script to execute */ public ResultSet submit(final String gremlin) { return submit(gremlin, RequestOptions.EMPTY); } /** * Submits a Gremlin script and bound parameters to the server and returns a {@link ResultSet} once the write of * the request is complete. If a script is to be executed repeatedly with slightly different arguments, prefer * this method to concatenating a Gremlin script from dynamically produced strings and sending it to * {@link #submit(String)}. Parameterized scripts will perform better. * * @param gremlin the gremlin script to execute * @param parameters a map of parameters that will be bound to the script on execution */ public ResultSet submit(final String gremlin, final Map parameters) { try { return submitAsync(gremlin, parameters).get(); } catch (RuntimeException re) { throw re; } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Submits a Gremlin script to the server and returns a {@link ResultSet} once the write of the request is * complete. * * @param gremlin the gremlin script to execute * @param options for the request */ public ResultSet submit(final String gremlin, final RequestOptions options) { try { return submitAsync(gremlin, options).get(); } catch (RuntimeException re) { throw re; } catch (Exception ex) { throw new RuntimeException(ex); } } /** * The asynchronous version of {@link #submit(String)} where the returned future will complete when the * write of the request completes. * * @param gremlin the gremlin script to execute */ public CompletableFuture submitAsync(final String gremlin) { return submitAsync(gremlin, RequestOptions.build().create()); } /** * The asynchronous version of {@link #submit(String, Map)}} where the returned future will complete when the * write of the request completes. * * @param gremlin the gremlin script to execute * @param parameters a map of parameters that will be bound to the script on execution */ public CompletableFuture submitAsync(final String gremlin, final Map parameters) { final RequestOptions.Builder options = RequestOptions.build(); if (parameters != null && !parameters.isEmpty()) { parameters.forEach(options::addParameter); } return submitAsync(gremlin, options.create()); } /** * The asynchronous version of {@link #submit(String, Map)}} where the returned future will complete when the * write of the request completes. * * @param gremlin the gremlin script to execute * @param parameters a map of parameters that will be bound to the script on execution * @param graphOrTraversalSource rebinds the specified global Gremlin Server variable to "g" * @deprecated As of release 3.4.0, replaced by {@link #submitAsync(String, RequestOptions)}. */ @Deprecated public CompletableFuture submitAsync(final String gremlin, final String graphOrTraversalSource, final Map parameters) { final RequestOptions.Builder options = RequestOptions.build(); options.addG(graphOrTraversalSource); if (parameters != null && !parameters.isEmpty()) { parameters.forEach(options::addParameter); } options.batchSize(cluster.connectionPoolSettings().resultIterationBatchSize); return submitAsync(gremlin, options.create()); } /** * The asynchronous version of {@link #submit(String, RequestOptions)}} where the returned future will complete when the * write of the request completes. * * @param gremlin the gremlin script to execute * @param options the options to supply for this request */ public CompletableFuture submitAsync(final String gremlin, final RequestOptions options) { final int batchSize = options.getBatchSize().orElse(cluster.connectionPoolSettings().resultIterationBatchSize); // need to call buildMessage() right away to get client specific configurations, that way request specific // ones can override as needed final RequestMessage.Builder request = buildMessage(RequestMessage.build(gremlin)) .addChunkSize(batchSize); // apply settings if they were made available options.getTimeout().ifPresent(timeout -> request.addTimeoutMillis(timeout)); options.getParameters().ifPresent(params -> request.addBindings(params)); options.getG().ifPresent(g -> request.addG(g)); options.getLanguage().ifPresent(lang -> request.addLanguage(lang)); options.getMaterializeProperties().ifPresent(mp -> request.addMaterializeProperties(mp)); options.getBulkResults().ifPresent(bulked -> request.addBulkResults(Boolean.parseBoolean(bulked))); return submitAsync(request.create()); } /** * A low-level method that allows the submission of a manually constructed {@link RequestMessage}. */ public CompletableFuture submitAsync(final RequestMessage msg) { if (isClosing()) throw new IllegalStateException("Client is closed"); if (!initialized) init(); final CompletableFuture future = new CompletableFuture<>(); Connection connection = null; try { // the connection is returned to the pool once the response has been completed...see Connection.write() // the connection may be returned to the pool with the host being marked as "unavailable" connection = chooseConnection(msg); connection.write(msg, future); return future; } catch (RuntimeException re) { throw re; } catch (Exception ex) { throw new RuntimeException(ex); } finally { if (logger.isDebugEnabled()) logger.debug("Submitted {} to - {}", msg, null == connection ? "connection not initialized" : connection.toString()); } } public abstract boolean isClosing(); /** * Closes the client by making a synchronous call to {@link #closeAsync()}. */ public void close() { closeAsync().join(); } /** * Gets the {@link Cluster} that spawned this {@code Client}. */ public Cluster getCluster() { return cluster; } /** * A {@code Client} implementation. Requests are sent to multiple servers given a {@link LoadBalancingStrategy}. * Transactions are automatically committed (or rolled-back on error) after each request. */ public final static class ClusteredClient extends Client { ConcurrentMap hostConnectionPools = new ConcurrentHashMap<>(); private final AtomicReference> closing = new AtomicReference<>(null); private Throwable initializationFailure = null; ClusteredClient(final Cluster cluster) { super(cluster); } @Override public boolean isClosing() { return closing.get() != null; } /** * Submits a Gremlin script to the server and returns a {@link ResultSet} once the write of the request is * complete. * * @param gremlin the gremlin script to execute */ public ResultSet submit(final String gremlin, final String graphOrTraversalSource) { return submit(gremlin, graphOrTraversalSource, null); } /** * Submits a Gremlin script and bound parameters to the server and returns a {@link ResultSet} once the write of * the request is complete. If a script is to be executed repeatedly with slightly different arguments, prefer * this method to concatenating a Gremlin script from dynamically produced strings and sending it to * {@link #submit(String)}. Parameterized scripts will perform better. * * @param gremlin the gremlin script to execute * @param parameters a map of parameters that will be bound to the script on execution * @param graphOrTraversalSource rebinds the specified global Gremlin Server variable to "g" */ public ResultSet submit(final String gremlin, final String graphOrTraversalSource, final Map parameters) { try { return submitAsync(gremlin, graphOrTraversalSource, parameters).get(); } catch (RuntimeException re) { throw re; } catch (Exception ex) { throw new RuntimeException(ex); } } /** * {@inheritDoc} */ @Override public Client alias(final String graphOrTraversalSource) { return new AliasClusteredClient(this, graphOrTraversalSource); } /** * Uses a {@link LoadBalancingStrategy} to choose the best {@link Host} and then selects the best connection * from that host's connection pool. */ @Override protected Connection chooseConnection(final RequestMessage msg) throws TimeoutException, ConnectionException { final Iterator possibleHosts = this.cluster.loadBalancingStrategy().select(msg); // try a random host if none are marked available. maybe it will reconnect in the meantime. better than // going straight to a fast NoHostAvailableException as was the case in versions 3.5.4 and earlier final Host bestHost = possibleHosts.hasNext() ? possibleHosts.next() : chooseRandomHost(); final ConnectionPool pool = hostConnectionPools.get(bestHost); return pool.borrowConnection(cluster.connectionPoolSettings().maxWaitForConnection, TimeUnit.MILLISECONDS); } private Host chooseRandomHost() { final List hosts = new ArrayList<>(cluster.allHosts()); final int ix = random.nextInt(hosts.size()); return hosts.get(ix); } /** * Initializes the connection pools on all hosts. */ @Override protected void initializeImplementation() { try { CompletableFuture.allOf(cluster.allHosts().stream() .map(host -> CompletableFuture.runAsync( () -> initializeConnectionSetupForHost.accept(host), cluster.hostScheduler())) .toArray(CompletableFuture[]::new)) .join(); } catch (CompletionException ex) { logger.error("Initialization failed", ex); this.initializationFailure = ex; } // throw an error if there is no host available after initializing connection pool. if (cluster.availableHosts().isEmpty()) throwNoHostAvailableException(); // try to re-initiate any unavailable hosts in the background. final List unavailableHosts = cluster.allHosts() .stream().filter(host -> !host.isAvailable()).collect(Collectors.toList()); if (!unavailableHosts.isEmpty()) { handleUnavailableHosts(unavailableHosts); } } private void throwNoHostAvailableException() { final Throwable rootCause = ExceptionUtils.getRootCause(initializationFailure); // allow the certain exceptions to propagate as a cause if (rootCause instanceof SSLException || rootCause instanceof ConnectException) { throw new NoHostAvailableException(initializationFailure); } else { throw new NoHostAvailableException(); } } /** * Closes all the connection pools on all hosts. */ @Override public synchronized CompletableFuture closeAsync() { if (closing.get() != null) return closing.get(); final CompletableFuture allPoolsClosedFuture = CompletableFuture.allOf(hostConnectionPools.values().stream() .map(ConnectionPool::closeAsync) .toArray(CompletableFuture[]::new)); closing.set(allPoolsClosedFuture); return closing.get(); } private final Consumer initializeConnectionSetupForHost = host -> { try { // hosts that don't initialize connection pools will come up as a dead host. hostConnectionPools.put(host, new ConnectionPool(host, ClusteredClient.this)); // hosts are not marked as available at cluster initialization, and are made available here instead. host.makeAvailable(); // added a new host to the cluster so let the load-balancer know. ClusteredClient.this.cluster.loadBalancingStrategy().onNew(host); } catch (RuntimeException ex) { final String errMsg = "Could not initialize client for " + host; logger.error(errMsg); throw ex; } }; private void handleUnavailableHosts(final List unavailableHosts) { // start the re-initialization attempt for each of the unavailable hosts through Host.makeUnavailable(). for (Host host : unavailableHosts) { final CompletableFuture f = CompletableFuture.runAsync( () -> host.makeUnavailable(this::tryReInitializeHost), cluster.hostScheduler()); f.exceptionally(t -> { logger.error("", (t.getCause() == null) ? t : t.getCause()); return null; }); } } /** * Attempt to re-initialize the {@link Host} that was previously marked as unavailable. This method gets called * as part of a schedule in {@link Host} to periodically try to re-initialize. */ public boolean tryReInitializeHost(final Host host) { logger.debug("Trying to re-initiate host connection pool on {}", host); try { initializeConnectionSetupForHost.accept(host); return true; } catch (Exception ex) { logger.debug("Failed re-initialization attempt on {}", host, ex); return false; } } } /** * Uses a {@link Client.ClusteredClient} that rebinds requests to a specified {@link Graph} or * {@link TraversalSource} instances on the server-side. */ public static class AliasClusteredClient extends Client { private final Client client; private final String graphOrTraversalSource; final CompletableFuture close = new CompletableFuture<>(); AliasClusteredClient(final Client client, final String graphOrTraversalSource) { super(client.cluster); this.client = client; this.graphOrTraversalSource = graphOrTraversalSource; } @Override public CompletableFuture submitAsync(final RequestMessage msg) { final RequestMessage.Builder builder = RequestMessage.from(msg); builder.addG(graphOrTraversalSource); return super.submitAsync(builder.create()); } @Override public synchronized Client init() { if (close.isDone()) throw new IllegalStateException("Client is closed"); // the underlying client may not have been init'd client.init(); return this; } @Override public RequestMessage.Builder buildMessage(final RequestMessage.Builder builder) { if (close.isDone()) throw new IllegalStateException("Client is closed"); builder.addG(graphOrTraversalSource); return client.buildMessage(builder); } @Override protected void initializeImplementation() { // no init required if (close.isDone()) { throw new IllegalStateException("Client is closed"); } else if (cluster.availableHosts().isEmpty()) { throw new NoHostAvailableException(); } } /** * Delegates to the underlying {@link Client.ClusteredClient}. */ @Override protected Connection chooseConnection(final RequestMessage msg) throws TimeoutException, ConnectionException { if (close.isDone()) throw new IllegalStateException("Client is closed"); return client.chooseConnection(msg); } @Override public void close() { client.close(); } @Override public synchronized CompletableFuture closeAsync() { return client.closeAsync(); } @Override public boolean isClosing() { return client.isClosing(); } /** * {@inheritDoc} */ @Override public Client alias(final String graphOrTraversalSource) { if (close.isDone()) throw new IllegalStateException("Client is closed"); return new AliasClusteredClient(client, graphOrTraversalSource); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy