diff --git a/ChangeLog b/ChangeLog index aa60c6c5e..b4fb3d105 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +v4.3.0 (2017-11-23) +--------------------------- +* added load balancing (ArangoDB.Builder.loadBalancingStrategy()) +* added automatic acquiring of hosts for load balancing or as fallback (ArangoDB.Builder.acquireHostList()) + v4.2.7 (2017-11-03) --------------------------- * added ArangoGraph.exists() diff --git a/README.md b/README.md index df87521c0..9cdc503ec 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,10 @@ - - - + + + +
arangodb-java-driverArangoDBnetwork protocolJava version
4.2.x3.0.x, 3.1.x, 3.2.xVelocyStream, HTTP1.6+
4.1.x3.1.x, 3.2.xVelocyStream1.6+
3.1.x3.1.x, 3.2.xHTTP1.6+
4.3.x3.0.0+VelocyStream, HTTP1.6+
4.2.x3.0.0+VelocyStream, HTTP1.6+
4.1.x3.1.0+VelocyStream1.6+
3.1.x3.1.0+HTTP1.6+
3.0.x3.0.xHTTP1.6+
2.7.42.7.x, 2.8.xHTTP1.6+
@@ -37,12 +38,12 @@ ArangoDB 3.x.x com.arangodb arangodb-java-driver - 4.2.7 + 4.3.0 ``` -If you want to test with a snapshot version (e.g. 4.2.0-SNAPSHOT), add the staging repository of oss.sonatype.org to your pom.xml: +If you want to test with a snapshot version (e.g. 4.3.0-SNAPSHOT), add the staging repository of oss.sonatype.org to your pom.xml: ```XML @@ -65,8 +66,10 @@ mvn clean install -DskipTests=true -Dgpg.skip=true -Dmaven.javadoc.skip=true -B * [Network protocol](#network-protocol) * [SSL](#ssl) * [Connection pooling](#connection-pooling) + * [Fallback hosts](#fallback-hosts) + * [Load Balancing](#load-balancing) * [configure VelocyPack serialization](#configure-velocypack-serialization) - * [Java 8 types](#java-8-types) + * [Java 8 types](#java-8-types) * [Scala types](#scala-types) * [Joda-Time](#joda-time) * [custom serializer](#custom-serializer) @@ -144,14 +147,14 @@ To customize the configuration the parameters can be changed in the code... ``` Java ArangoDB arangoDB = new ArangoDB.Builder().host("192.168.182.50", 8888).build(); - + ``` ... or with a custom properties file (my.properties) ``` Java InputStream in = MyClass.class.getResourceAsStream("my.properties"); ArangoDB arangoDB = new ArangoDB.Builder().loadProperties(in).build(); - + ``` Example for arangodb.properties: @@ -167,12 +170,12 @@ Example for arangodb.properties: The drivers default used network protocol is the binary protocol VelocyStream which offers the best performance within the driver. To use HTTP, you have to set the configuration `useProtocol` to `Protocol.HTTP_JSON` for HTTP with Json content or `Protocol.HTTP_VPACK` for HTTP with [VelocyPack](https://github.com/arangodb/velocypack/blob/master/VelocyPack.md) content. ``` Java - + ArangoDB arangoDB = new ArangoDB.Builder().useProtocol(Protocol.VST).build(); - + ``` -In addition to set the configuration for HTTP you have to add the apache httpclient to your classpath. +In addition to set the configuration for HTTP you have to add the apache httpclient to your classpath. ```XML @@ -189,9 +192,9 @@ In addition to set the configuration for HTTP you have to add the apache httpcli To use SSL, you have to set the configuration `useSsl` to `true` and set a `SSLContext`. (see [example code](../src/test/java/com/arangodb/example/ssl/SslExample.java)) ``` Java - + ArangoDB arangoDB = new ArangoDB.Builder().useSsl(true).sslContext(sc).build(); - + ``` ## Connection Pooling @@ -203,6 +206,54 @@ The driver supports connection pooling for VelocyStream with a default of 1 and ArangoDB arangoDB = new ArangoDB.Builder().maxConnections(8).build(); ``` + +## Fallback hosts + +The driver supports configuring multiple hosts. The first host is used to open a connection to. When this host is not reachable the next host from the list is used. To use this feature just call the method `host(String, int)` multiple times. + +``` Java + + ArangoDB arangoDB = new ArangoDB.Builder().host("host1", 8529).host("host2", 8529).build(); + +``` + +Since version 4.3 the driver support acquiring a list of known hosts in a cluster setup or a single server setup with followers. For this the driver has to be able to successfully open a connection to at least one host to get the list of hosts. Then it can use this list when fallback is needed. To use this feature just pass `true` to the method `acquireHostList(boolean)`. + +``` Java + + ArangoDB arangoDB = new ArangoDB.Builder().acquireHostList(true).build(); + +``` + +## Load Balancing + +Since version 4.3 the driver supports load balancing for cluster setups in two different ways. + +The first one is a round robin load balancing where the driver iterates through a list of known hosts and performs every request on a different host than the request before. This load balancing strategy only work when the maximun of connections is greater 1. + +``` Java + + ArangoDB arangoDB = new ArangoDB.Builder().loadBalancingStrategy(LoadBalancingStrategy.ROUND_ROBIN).maxConnections(8).build(); + +``` + +Just like the Fallback hosts feature the round robin load balancing strategy can use the `acquireHostList` configuration to acquire a list of all known hosts in the cluster. Do so only requires the manually configuration of only one host. Because this list is updated frequently it makes load balancing over the whole cluster very comfortable. + +``` Java + + ArangoDB arangoDB = new ArangoDB.Builder().loadBalancingStrategy(LoadBalancingStrategy.ROUND_ROBIN).maxConnections(8).acquireHostList(true).build(); + +``` + +The second load balancing strategy allows to pick a random host from the configured or acquired list of hosts and sticks to that host as long as the connection is open. This strategy is useful for an application - using the driver - which provides a session management where each session has its own instance of `ArangoDB` build from a global configured list of hosts. In this case it could be wanted that every sessions sticks with all its requests to the same host but not all sessions should use the same host. This load balancing strategy also works together with `acquireHostList`. + + +``` Java + + ArangoDB arangoDB = new ArangoDB.Builder().loadBalancingStrategy(LoadBalancingStrategy.ONE_RANDOM).acquireHostList(true).build(); + +``` + ## configure VelocyPack serialization Since version `4.1.11` you can extend the VelocyPack serialization by registering additional `VPackModule`s on `ArangoDB.Builder`. @@ -230,7 +281,7 @@ Added support for: ``` Java ArangoDB arangoDB = new ArangoDB.Builder().registerModule(new VPackJdk8Module()).build(); -``` +``` ### Scala types @@ -251,7 +302,7 @@ Added support for: ``` Scala val arangoDB: ArangoDB = new ArangoDB.Builder().registerModule(new VPackScalaModule).build -``` +``` ### Joda-Time @@ -273,7 +324,7 @@ Added support for: ``` Java ArangoDB arangoDB = new ArangoDB.Builder().registerModule(new VPackJodaModule()).build(); -``` +``` ## custom serializer ``` Java @@ -300,23 +351,23 @@ ArangoDB arangoDB = new ArangoDB.Builder().registerModule(new VPackJodaModule()) }); } }).build(); -``` +``` # Manipulating databases ## create database ``` Java - // create database + // create database arangoDB.createDatabase("myDatabase"); - + ``` ## drop database ``` Java - // drop database + // drop database arangoDB.db("myDatabase").drop(); - + ``` # Manipulating collections @@ -325,14 +376,14 @@ ArangoDB arangoDB = new ArangoDB.Builder().registerModule(new VPackJodaModule()) ``` Java // create collection arangoDB.db("myDatabase").createCollection("myCollection", null); - + ``` ## drop collection ``` Java - // delete collection + // delete collection arangoDB.db("myDatabase").collection("myCollection").drop(); - + ``` ## truncate collection @@ -366,15 +417,15 @@ For the next examples we use a small object: /* * + getter and setter */ - - } + + } ``` ## insert document ``` Java MyObject myObject = new MyObject("Homer", 38); arangoDB.db("myDatabase").collection("myCollection").insertDocument(myObject); - + ``` When creating a document, the attributes of the object will be stored as key-value pair @@ -387,19 +438,19 @@ E.g. in the previous example the object was stored as follows: ## delete document ``` Java arangoDB.db("myDatabase").collection("myCollection").deleteDocument(myObject.getKey()); - + ``` ## update document ``` Java arangoDB.db("myDatabase").collection("myCollection").updateDocument(myObject.getKey(), myUpdatedObject); - + ``` ## replace document ``` Java arangoDB.db("myDatabase").collection("myCollection").replaceDocument(myObject.getKey(), myObject2); - + ``` ## read document as JavaBean @@ -407,7 +458,7 @@ E.g. in the previous example the object was stored as follows: MyObject document = arangoDB.db("myDatabase").collection("myCollection").getDocument(myObject.getKey(), MyObject.class); document.getName(); document.getAge(); - + ``` ## read document as VelocyPack @@ -415,25 +466,25 @@ E.g. in the previous example the object was stored as follows: VPackSlice document = arangoDB.db("myDatabase").collection("myCollection").getDocument(myObject.getKey(), VPackSlice.class); document.get("name").getAsString(); document.get("age").getAsInt(); - + ``` ## read document as Json ``` Java String json = arangoDB.db("myDatabase").collection("myCollection").getDocument(myObject.getKey(), String.class); - + ``` ## read document by key ``` Java arangoDB.db("myDatabase").collection("myCollection").getDocument("myKey", MyObject.class); - + ``` ## read document by id ``` Java arangoDB.db("myDatabase").getDocument("myCollection/myKey", MyObject.class); - + ``` # Multi Document operations @@ -445,7 +496,7 @@ E.g. in the previous example the object was stored as follows: documents.add(myObject2); documents.add(myObject3); arangoDB.db("myDatabase").collection("myCollection").insertDocuments(documents); - + ``` ## delete documents @@ -455,7 +506,7 @@ E.g. in the previous example the object was stored as follows: keys.add(myObject2.getKey()); keys.add(myObject3.getKey()); arangoDB.db("myDatabase").collection("myCollection").deleteDocuments(keys); - + ``` ## update documents @@ -465,7 +516,7 @@ E.g. in the previous example the object was stored as follows: documents.add(myObject2); documents.add(myObject3); arangoDB.db("myDatabase").collection("myCollection").updateDocuments(documents); - + ``` ## replace documents @@ -475,7 +526,7 @@ E.g. in the previous example the object was stored as follows: documents.add(myObject2); documents.add(myObject3); arangoDB.db("myDatabase").collection("myCollection").replaceDocuments(documents); - + ``` # AQL @@ -489,21 +540,21 @@ E.g. get all Simpsons aged 3 or older in ascending order: ``` Java arangoDB.createDatabase("myDatabase"); ArangoDatabase db = arangoDB.db("myDatabase"); - + db.createCollection("myCollection"); ArangoCollection collection = db.collection("myCollection"); - + collection.insertDocument(new MyObject("Homer", 38)); collection.insertDocument(new MyObject("Marge", 36)); collection.insertDocument(new MyObject("Bart", 10)); collection.insertDocument(new MyObject("Lisa", 8)); collection.insertDocument(new MyObject("Maggie", 2)); - + Map bindVars = new HashMap<>(); bindVars.put("age", 3); - + ArangoCursor cursor = db.query(query, bindVars, null, MyObject.class); - + for(; cursor.hasNext;) { MyObject obj = cursor.next(); System.out.println(obj.getName()); @@ -514,7 +565,7 @@ or return the AQL result as VelocyPack: ``` Java ArangoCursor cursor = db.query(query, bindVars, null, VPackSlice.class); - + for(; cursor.hasNext;) { VPackSlice obj = cursor.next(); System.out.println(obj.get("name").getAsString()); @@ -539,19 +590,19 @@ A graph consists of vertices and edges (stored in collections). Which collection edgeDefinition.collection("myEdgeCollection"); // define a set of collections where an edge is going out... edgeDefinition.from("myCollection1", "myCollection2"); - - // repeat this for the collections where an edge is going into + + // repeat this for the collections where an edge is going into edgeDefinition.to("myCollection1", "myCollection3"); - + edgeDefinitions.add(edgeDefinition); - + // A graph can contain additional vertex collections, defined in the set of orphan collections GraphCreateOptions options = new GraphCreateOptions(); options.orphanCollections("myCollection4", "myCollection5"); - + // now it's possible to create a graph arangoDB.db("myDatabase").createGraph("myGraph", edgeDefinitions, options); - + ``` ## delete graph @@ -571,7 +622,7 @@ Vertices are stored in the vertex collections defined above. MyObject myObject2 = new MyObject("Marge", 36); arangoDB.db("myDatabase").graph("myGraph").vertexCollection("collection1").insertVertex(myObject1, null); arangoDB.db("myDatabase").graph("myGraph").vertexCollection("collection3").insertVertex(myObject2, null); - + ``` ## add edge @@ -580,8 +631,8 @@ Now an edge can be created to set a relation between vertices ``` Java arangoDB.db("myDatabase").graph("myGraph").edgeCollection("myEdgeCollection").insertEdge(myEdgeObject, null); - -``` + +``` # Foxx @@ -589,8 +640,8 @@ Now an edge can be created to set a relation between vertices ``` Java Request request = new Request("mydb", RequestType.GET, "/my/foxx/service") Response response = arangoDB.execute(request); - -``` + +``` # User management @@ -641,7 +692,7 @@ The driver can serialize/deserialize JavaBeans. They need at least a constructor super(); } - } + } ``` ## internal fields @@ -652,7 +703,7 @@ To use Arango-internal fields (like _id, _key, _rev, _from, _to) in your JavaBea @DocumentField(Type.KEY) private String key; - + private String name; private Gender gender; private int age; @@ -661,7 +712,7 @@ To use Arango-internal fields (like _id, _key, _rev, _from, _to) in your JavaBea super(); } - } + } ``` ## serialized fieldnames @@ -672,7 +723,7 @@ To use a different serialized name for a field, use the annotation `SerializedNa @SerializedName("title") private String name; - + private Gender gender; private int age; @@ -680,7 +731,7 @@ To use a different serialized name for a field, use the annotation `SerializedNa super(); } - } + } ``` ## ignore fields @@ -699,7 +750,7 @@ To ignore fields at serialization/deserialization, use the annotation `Expose` super(); } - } + } ``` ## custom serializer @@ -727,7 +778,7 @@ To ignore fields at serialization/deserialization, use the annotation `Expose` }); } }).build(); -``` +``` ## manually serialization To de-/serialize from and to VelocyPack before or after a database call, use the `ArangoUtil` from the method `util()` in `ArangoDB`, `ArangoDatabase`, `ArangoCollection`, `ArangoGraph`, `ArangoEdgeCollection`or `ArangoVertexCollection`. diff --git a/pom.xml b/pom.xml index 43ff45f1a..96ec2dbcc 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.arangodb arangodb-java-driver - 4.2.8-SNAPSHOT + 4.3.0-SNAPSHOT 2016 jar diff --git a/src/main/java/com/arangodb/ArangoCursor.java b/src/main/java/com/arangodb/ArangoCursor.java index fe6cb173f..904397eda 100644 --- a/src/main/java/com/arangodb/ArangoCursor.java +++ b/src/main/java/com/arangodb/ArangoCursor.java @@ -34,6 +34,7 @@ import com.arangodb.internal.ArangoCursorExecute; import com.arangodb.internal.ArangoCursorIterator; import com.arangodb.internal.InternalArangoDatabase; +import com.arangodb.internal.net.HostHandle; /** * @author Mark Vollmary @@ -45,13 +46,15 @@ public class ArangoCursor implements Iterable, Iterator, Closeable { protected final ArangoCursorIterator iterator; private final String id; private final ArangoCursorExecute execute; + private final HostHandle hostHandle; protected ArangoCursor(final InternalArangoDatabase db, final ArangoCursorExecute execute, final Class type, final CursorEntity result) { super(); this.execute = execute; this.type = type; - iterator = createIterator(this, db, execute, result); + hostHandle = new HostHandle(); + iterator = createIterator(this, db, execute, result, hostHandle); id = result.getId(); } @@ -59,8 +62,9 @@ protected ArangoCursorIterator createIterator( final ArangoCursor cursor, final InternalArangoDatabase db, final ArangoCursorExecute execute, - final CursorEntity result) { - return new ArangoCursorIterator(cursor, execute, db, result); + final CursorEntity result, + final HostHandle hostHandle) { + return new ArangoCursorIterator(cursor, execute, db, result, hostHandle); } /** @@ -103,7 +107,7 @@ public boolean isCached() { @Override public void close() throws IOException { if (id != null) { - execute.close(id); + execute.close(id, hostHandle); } } diff --git a/src/main/java/com/arangodb/ArangoDB.java b/src/main/java/com/arangodb/ArangoDB.java index d33f8d33d..1075c0208 100644 --- a/src/main/java/com/arangodb/ArangoDB.java +++ b/src/main/java/com/arangodb/ArangoDB.java @@ -25,12 +25,15 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Properties; import javax.net.ssl.SSLContext; import com.arangodb.entity.ArangoDBVersion; +import com.arangodb.entity.LoadBalancingStrategy; import com.arangodb.entity.LogEntity; import com.arangodb.entity.LogLevelEntity; import com.arangodb.entity.Permissions; @@ -41,13 +44,21 @@ import com.arangodb.internal.ArangoExecutorSync; import com.arangodb.internal.CollectionCache; import com.arangodb.internal.CollectionCache.DBAccess; -import com.arangodb.internal.CommunicationProtocol; -import com.arangodb.internal.DefaultHostHandler; import com.arangodb.internal.DocumentCache; import com.arangodb.internal.Host; import com.arangodb.internal.InternalArangoDB; import com.arangodb.internal.http.HttpCommunication; import com.arangodb.internal.http.HttpProtocol; +import com.arangodb.internal.net.CommunicationProtocol; +import com.arangodb.internal.net.ExtendedHostResolver; +import com.arangodb.internal.net.FallbackHostHandler; +import com.arangodb.internal.net.HostHandle; +import com.arangodb.internal.net.HostHandler; +import com.arangodb.internal.net.HostResolver; +import com.arangodb.internal.net.HostResolver.EndpointResolver; +import com.arangodb.internal.net.RandomHostHandler; +import com.arangodb.internal.net.RoundRobinHostHandler; +import com.arangodb.internal.net.SimpleHostResolver; import com.arangodb.internal.util.ArangoDeserializerImpl; import com.arangodb.internal.util.ArangoSerializerImpl; import com.arangodb.internal.util.ArangoUtilImpl; @@ -74,9 +85,11 @@ import com.arangodb.velocypack.VPackParser; import com.arangodb.velocypack.VPackParserModule; import com.arangodb.velocypack.VPackSerializer; +import com.arangodb.velocypack.VPackSlice; import com.arangodb.velocypack.ValueType; import com.arangodb.velocypack.exception.VPackException; import com.arangodb.velocystream.Request; +import com.arangodb.velocystream.RequestType; import com.arangodb.velocystream.Response; /** @@ -101,6 +114,8 @@ public static class Builder { private ArangoSerializer serializer; private ArangoDeserializer deserializer; private Protocol protocol; + private Boolean acquireHostList; + private LoadBalancingStrategy loadBalancingStrategy; public Builder() { super(); @@ -130,6 +145,8 @@ public Builder loadProperties(final InputStream in) throws ArangoDBException { chunksize = loadChunkSize(properties, chunksize); maxConnections = loadMaxConnections(properties, maxConnections); protocol = loadProtocol(properties, protocol); + acquireHostList = loadAcquireHostList(properties, acquireHostList); + loadBalancingStrategy = loadLoadBalancingStrategy(properties, loadBalancingStrategy); } catch (final IOException e) { throw new ArangoDBException(e); } @@ -215,6 +232,16 @@ public Builder useProtocol(final Protocol protocol) { return this; } + public Builder acquireHostList(final Boolean acquireHostList) { + this.acquireHostList = acquireHostList; + return this; + } + + public Builder loadBalancingStrategy(final LoadBalancingStrategy loadBalancingStrategy) { + this.loadBalancingStrategy = loadBalancingStrategy; + return this; + } + public Builder registerSerializer(final Class clazz, final VPackSerializer serializer) { vpackBuilder.registerSerializer(clazz, serializer); return this; @@ -349,14 +376,42 @@ public synchronized ArangoDB build() { : new ArangoSerializerImpl(vpacker, vpackerNull, vpackParser); final ArangoDeserializer deserializerTemp = deserializer != null ? deserializer : new ArangoDeserializerImpl(vpackerNull, vpackParser); + + final HostResolver hostResolver = createHostResolver(); + final HostHandler hostHandler = createHostHandler(hostResolver); return new ArangoDB( - new VstCommunicationSync.Builder(new DefaultHostHandler(new ArrayList(hosts))) - .timeout(timeout).user(user).password(password).useSsl(useSsl).sslContext(sslContext) - .chunksize(chunksize).maxConnections(maxConnections), - new HttpCommunication.Builder(new DefaultHostHandler(new ArrayList(hosts)), protocol) - .timeout(timeout).user(user).password(password).useSsl(useSsl).sslContext(sslContext) - .maxConnections(maxConnections), - new ArangoUtilImpl(serializerTemp, deserializerTemp), collectionCache, protocol); + new VstCommunicationSync.Builder(hostHandler).timeout(timeout).user(user).password(password) + .useSsl(useSsl).sslContext(sslContext).chunksize(chunksize).maxConnections(maxConnections), + new HttpCommunication.Builder(hostHandler, protocol).timeout(timeout).user(user).password(password) + .useSsl(useSsl).sslContext(sslContext).maxConnections(maxConnections), + new ArangoUtilImpl(serializerTemp, deserializerTemp), collectionCache, protocol, hostResolver); + } + + private HostResolver createHostResolver() { + return acquireHostList != null && acquireHostList.booleanValue() + ? new ExtendedHostResolver(new ArrayList(hosts)) + : new SimpleHostResolver(new ArrayList(hosts)); + } + + private HostHandler createHostHandler(final HostResolver hostResolver) { + final HostHandler hostHandler; + if (loadBalancingStrategy != null) { + switch (loadBalancingStrategy) { + case ONE_RANDOM: + hostHandler = new RandomHostHandler(hostResolver, new FallbackHostHandler(hostResolver)); + break; + case ROUND_ROBIN: + hostHandler = new RoundRobinHostHandler(hostResolver); + break; + case NONE: + default: + hostHandler = new FallbackHostHandler(hostResolver); + break; + } + } else { + hostHandler = new FallbackHostHandler(hostResolver); + } + return hostHandler; } } @@ -365,10 +420,12 @@ public synchronized ArangoDB build() { private CommunicationProtocol cp; public ArangoDB(final VstCommunicationSync.Builder vstBuilder, final HttpCommunication.Builder httpBuilder, - final ArangoSerialization util, final CollectionCache collectionCache, final Protocol protocol) { + final ArangoSerialization util, final CollectionCache collectionCache, final Protocol protocol, + final HostResolver hostResolver) { super(new ArangoExecutorSync(createProtocol(vstBuilder, httpBuilder, util, collectionCache, protocol), util, new DocumentCache()), util); - cp = createProtocol(vstBuilder, httpBuilder, util, collectionCache, protocol); + cp = createProtocol(new VstCommunicationSync.Builder(vstBuilder).maxConnections(1), + new HttpCommunication.Builder(httpBuilder).maxConnections(1), util, collectionCache, protocol); collectionCache.init(new DBAccess() { @Override public ArangoDatabase db(final String name) { @@ -376,6 +433,48 @@ public ArangoDatabase db(final String name) { .setCursorInitializer(cursorInitializer); } }); + hostResolver.init(new EndpointResolver() { + @Override + public Collection resolve(final boolean closeConnections) throws ArangoDBException { + Collection response; + try { + response = executor.execute( + new Request(ArangoDBConstants.SYSTEM, RequestType.GET, ArangoDBConstants.PATH_ENDPOINTS), + new ResponseDeserializer>() { + @Override + public Collection deserialize(final Response response) throws VPackException { + final VPackSlice field = response.getBody().get(ArangoDBConstants.ENDPOINTS); + Collection endpoints; + if (field.isNone()) { + endpoints = Collections. emptyList(); + } else { + final Collection> tmp = util().deserialize(field, + Collection.class); + endpoints = new ArrayList(); + for (final Map map : tmp) { + for (final String value : map.values()) { + endpoints.add(value); + } + } + } + return endpoints; + } + }, null); + } catch (final ArangoDBException e) { + final Integer responseCode = e.getResponseCode(); + if (responseCode != null && responseCode == 403) { + response = Collections. emptyList(); + } else { + throw e; + } + } finally { + if (closeConnections) { + ArangoDB.this.shutdown(); + } + } + return response; + } + }); } private static CommunicationProtocol createProtocol( @@ -699,6 +798,25 @@ public Response deserialize(final Response response) throws VPackException { }); } + /** + * Generic Execute. Use this method to execute custom FOXX services. + * + * @param request + * VelocyStream request + * @param hostHandle + * Used to stick to a specific host when using {@link LoadBalancingStrategy#ROUND_ROBIN} + * @return VelocyStream response + * @throws ArangoDBException + */ + public Response execute(final Request request, final HostHandle hostHandle) throws ArangoDBException { + return executor.execute(request, new ResponseDeserializer() { + @Override + public Response deserialize(final Response response) throws VPackException { + return response; + } + }, hostHandle); + } + /** * Returns fatal, error, warning or info log messages from the server's global log. * diff --git a/src/main/java/com/arangodb/ArangoDatabase.java b/src/main/java/com/arangodb/ArangoDatabase.java index d2a2f1a8f..31971bbf6 100644 --- a/src/main/java/com/arangodb/ArangoDatabase.java +++ b/src/main/java/com/arangodb/ArangoDatabase.java @@ -40,9 +40,10 @@ import com.arangodb.entity.TraversalEntity; import com.arangodb.internal.ArangoCursorExecute; import com.arangodb.internal.ArangoExecutorSync; -import com.arangodb.internal.CommunicationProtocol; import com.arangodb.internal.DocumentCache; import com.arangodb.internal.InternalArangoDatabase; +import com.arangodb.internal.net.CommunicationProtocol; +import com.arangodb.internal.net.HostHandle; import com.arangodb.internal.velocystream.internal.ConnectionSync; import com.arangodb.model.AqlFunctionCreateOptions; import com.arangodb.model.AqlFunctionDeleteOptions; @@ -379,13 +380,13 @@ public ArangoCursor cursor(final String cursorId, final Class type) th private ArangoCursor createCursor(final CursorEntity result, final Class type) { final ArangoCursorExecute execute = new ArangoCursorExecute() { @Override - public CursorEntity next(final String id) { - return executor.execute(queryNextRequest(id), CursorEntity.class); + public CursorEntity next(final String id, final HostHandle hostHandle) { + return executor.execute(queryNextRequest(id), CursorEntity.class, hostHandle); } @Override - public void close(final String id) { - executor.execute(queryCloseRequest(id), Void.class); + public void close(final String id, final HostHandle hostHandle) { + executor.execute(queryCloseRequest(id), Void.class, hostHandle); } }; return cursorInitializer != null ? cursorInitializer.createInstance(this, execute, type, result) diff --git a/src/main/java/com/arangodb/entity/LoadBalancingStrategy.java b/src/main/java/com/arangodb/entity/LoadBalancingStrategy.java new file mode 100644 index 000000000..b75257b0a --- /dev/null +++ b/src/main/java/com/arangodb/entity/LoadBalancingStrategy.java @@ -0,0 +1,29 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.entity; + +/** + * @author Mark Vollmary + * + */ +public enum LoadBalancingStrategy { + NONE, ROUND_ROBIN, ONE_RANDOM +} diff --git a/src/main/java/com/arangodb/entity/ServerMode.java b/src/main/java/com/arangodb/entity/ServerMode.java new file mode 100644 index 000000000..2072cb064 --- /dev/null +++ b/src/main/java/com/arangodb/entity/ServerMode.java @@ -0,0 +1,29 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.entity; + +/** + * @author Mark Vollmary + * + */ +public enum ServerMode { + DEFAULT, RESILIENT +} diff --git a/src/main/java/com/arangodb/entity/ServerRole.java b/src/main/java/com/arangodb/entity/ServerRole.java index ecaa6ec21..5976e1107 100644 --- a/src/main/java/com/arangodb/entity/ServerRole.java +++ b/src/main/java/com/arangodb/entity/ServerRole.java @@ -25,5 +25,5 @@ * */ public enum ServerRole { - SINGLE, AGENT, COORDINATOR, PRIMARY + SINGLE, AGENT, COORDINATOR, PRIMARY, SECONDARY, UNDEFINED } diff --git a/src/main/java/com/arangodb/internal/ArangoCursorExecute.java b/src/main/java/com/arangodb/internal/ArangoCursorExecute.java index fc8edd784..78aa5e009 100644 --- a/src/main/java/com/arangodb/internal/ArangoCursorExecute.java +++ b/src/main/java/com/arangodb/internal/ArangoCursorExecute.java @@ -22,6 +22,7 @@ import com.arangodb.ArangoDBException; import com.arangodb.entity.CursorEntity; +import com.arangodb.internal.net.HostHandle; /** * @author Mark Vollmary @@ -29,8 +30,8 @@ */ public interface ArangoCursorExecute { - CursorEntity next(String id) throws ArangoDBException; + CursorEntity next(String id, HostHandle hostHandle) throws ArangoDBException; - void close(String id) throws ArangoDBException; + void close(String id, HostHandle hostHandle) throws ArangoDBException; } diff --git a/src/main/java/com/arangodb/internal/ArangoCursorIterator.java b/src/main/java/com/arangodb/internal/ArangoCursorIterator.java index ba6bb1821..d7f18bed9 100644 --- a/src/main/java/com/arangodb/internal/ArangoCursorIterator.java +++ b/src/main/java/com/arangodb/internal/ArangoCursorIterator.java @@ -25,6 +25,7 @@ import com.arangodb.ArangoCursor; import com.arangodb.entity.CursorEntity; +import com.arangodb.internal.net.HostHandle; import com.arangodb.velocypack.VPackSlice; /** @@ -40,14 +41,16 @@ public class ArangoCursorIterator implements Iterator { private final ArangoCursor cursor; private final InternalArangoDatabase db; private final ArangoCursorExecute execute; + private final HostHandle hostHandle; public ArangoCursorIterator(final ArangoCursor cursor, final ArangoCursorExecute execute, - final InternalArangoDatabase db, final CursorEntity result) { + final InternalArangoDatabase db, final CursorEntity result, final HostHandle hostHandle) { super(); this.cursor = cursor; this.execute = execute; this.db = db; this.result = result; + this.hostHandle = hostHandle; pos = 0; } @@ -63,7 +66,7 @@ public boolean hasNext() { @Override public T next() { if (pos >= result.getResult().size() && result.getHasMore()) { - result = execute.next(cursor.getId()); + result = execute.next(cursor.getId(), hostHandle); pos = 0; } if (!hasNext()) { diff --git a/src/main/java/com/arangodb/internal/ArangoDBConstants.java b/src/main/java/com/arangodb/internal/ArangoDBConstants.java index 78abef06e..c9851e6f4 100644 --- a/src/main/java/com/arangodb/internal/ArangoDBConstants.java +++ b/src/main/java/com/arangodb/internal/ArangoDBConstants.java @@ -21,6 +21,7 @@ package com.arangodb.internal; import com.arangodb.Protocol; +import com.arangodb.entity.LoadBalancingStrategy; /** * @author Mark Vollmary @@ -42,6 +43,8 @@ public class ArangoDBConstants { public static final int MAX_CONNECTIONS_VST_DEFAULT = 1; public static final int MAX_CONNECTIONS_HTTP_DEFAULT = 20; public static final Protocol DEFAULT_NETWORK_PROTOCOL = Protocol.VST; + public static final boolean DEFAULT_ACQUIRE_HOST_LIST = false; + public static final LoadBalancingStrategy DEFAULT_LOAD_BALANCING_STRATEGY = LoadBalancingStrategy.NONE; public static final String PATH_API_DOCUMENT = "/_api/document"; public static final String PATH_API_COLLECTION = "/_api/collection"; @@ -66,6 +69,7 @@ public class ArangoDBConstants { public static final String PATH_API_ADMIN_ROUTING_RELOAD = "/_admin/routing/reload"; public static final String PATH_API_IMPORT = "/_api/import"; public static final String PATH_API_ROLE = "/_admin/server/role"; + public static final String PATH_ENDPOINTS = "/_api/cluster/endpoints"; public static final String ENCRYPTION_PLAIN = "plain"; @@ -116,5 +120,6 @@ public class ArangoDBConstants { public static final String TYPE = "type"; public static final String IS_SYSTEM = "isSystem"; public static final String ROLE = "role"; + public static final String ENDPOINTS = "endpoints"; } diff --git a/src/main/java/com/arangodb/internal/ArangoExecuteable.java b/src/main/java/com/arangodb/internal/ArangoExecuteable.java index fe07bd9df..2402ad7e9 100644 --- a/src/main/java/com/arangodb/internal/ArangoExecuteable.java +++ b/src/main/java/com/arangodb/internal/ArangoExecuteable.java @@ -20,14 +20,14 @@ package com.arangodb.internal; -import com.arangodb.internal.velocystream.internal.Connection; +import com.arangodb.internal.velocystream.internal.VstConnection; import com.arangodb.util.ArangoSerialization; /** * @author Mark Vollmary * */ -public abstract class ArangoExecuteable { +public abstract class ArangoExecuteable { protected final E executor; private final ArangoSerialization util; diff --git a/src/main/java/com/arangodb/internal/ArangoExecutorSync.java b/src/main/java/com/arangodb/internal/ArangoExecutorSync.java index 97cc149f3..703bcae64 100644 --- a/src/main/java/com/arangodb/internal/ArangoExecutorSync.java +++ b/src/main/java/com/arangodb/internal/ArangoExecutorSync.java @@ -24,6 +24,8 @@ import java.lang.reflect.Type; import com.arangodb.ArangoDBException; +import com.arangodb.internal.net.CommunicationProtocol; +import com.arangodb.internal.net.HostHandle; import com.arangodb.util.ArangoSerialization; import com.arangodb.velocypack.exception.VPackException; import com.arangodb.velocystream.Request; @@ -44,18 +46,29 @@ public ArangoExecutorSync(final CommunicationProtocol protocol, final ArangoSeri } public T execute(final Request request, final Type type) throws ArangoDBException { + return execute(request, type, null); + } + + public T execute(final Request request, final Type type, final HostHandle hostHandle) throws ArangoDBException { return execute(request, new ResponseDeserializer() { @Override public T deserialize(final Response response) throws VPackException { return createResult(type, response); } - }); + }, hostHandle); } public T execute(final Request request, final ResponseDeserializer responseDeserializer) throws ArangoDBException { + return execute(request, responseDeserializer, null); + } + + public T execute( + final Request request, + final ResponseDeserializer responseDeserializer, + final HostHandle hostHandle) throws ArangoDBException { try { - final Response response = protocol.execute(request); + final Response response = protocol.execute(request, hostHandle); return responseDeserializer.deserialize(response); } catch (final VPackException e) { throw new ArangoDBException(e); diff --git a/src/main/java/com/arangodb/internal/InternalArangoCollection.java b/src/main/java/com/arangodb/internal/InternalArangoCollection.java index 36ee3261b..21b5ca9f4 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoCollection.java +++ b/src/main/java/com/arangodb/internal/InternalArangoCollection.java @@ -36,7 +36,7 @@ import com.arangodb.entity.MultiDocumentEntity; import com.arangodb.entity.Permissions; import com.arangodb.internal.ArangoExecutor.ResponseDeserializer; -import com.arangodb.internal.velocystream.internal.Connection; +import com.arangodb.internal.velocystream.internal.VstConnection; import com.arangodb.model.CollectionPropertiesOptions; import com.arangodb.model.CollectionRenameOptions; import com.arangodb.model.DocumentCreateOptions; @@ -66,7 +66,7 @@ * @author Mark Vollmary * */ -public class InternalArangoCollection, D extends InternalArangoDatabase, E extends ArangoExecutor, R, C extends Connection> +public class InternalArangoCollection, D extends InternalArangoDatabase, E extends ArangoExecutor, R, C extends VstConnection> extends ArangoExecuteable { private final D db; diff --git a/src/main/java/com/arangodb/internal/InternalArangoDB.java b/src/main/java/com/arangodb/internal/InternalArangoDB.java index 94169f811..d4615ceb8 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoDB.java +++ b/src/main/java/com/arangodb/internal/InternalArangoDB.java @@ -28,12 +28,13 @@ import com.arangodb.ArangoDBException; import com.arangodb.Protocol; +import com.arangodb.entity.LoadBalancingStrategy; import com.arangodb.entity.LogLevelEntity; import com.arangodb.entity.Permissions; import com.arangodb.entity.ServerRole; import com.arangodb.entity.UserEntity; import com.arangodb.internal.ArangoExecutor.ResponseDeserializer; -import com.arangodb.internal.velocystream.internal.Connection; +import com.arangodb.internal.velocystream.internal.VstConnection; import com.arangodb.model.DBCreateOptions; import com.arangodb.model.LogOptions; import com.arangodb.model.OptionsBuilder; @@ -54,7 +55,7 @@ * @param * */ -public class InternalArangoDB extends ArangoExecuteable { +public class InternalArangoDB extends ArangoExecuteable { private static final String PROPERTY_KEY_HOSTS = "arangodb.hosts"; private static final String PROPERTY_KEY_HOST = "arangodb.host"; @@ -66,6 +67,8 @@ public class InternalArangoDB private static final String PROPERTY_KEY_V_STREAM_CHUNK_CONTENT_SIZE = "arangodb.chunksize"; private static final String PROPERTY_KEY_MAX_CONNECTIONS = "arangodb.connections.max"; private static final String PROPERTY_KEY_PROTOCOL = "arangodb.protocol"; + private static final String PROPERTY_KEY_ACQUIRE_HOST_LIST = "arangodb.acquireHostList"; + private static final String PROPERTY_KEY_LOAD_BALANCING_STRATEGY = "arangodb.loadBalancingStrategy"; protected static final String DEFAULT_PROPERTY_FILE = "/arangodb.properties"; public InternalArangoDB(final E executor, final ArangoSerialization util) { @@ -138,6 +141,18 @@ protected static Protocol loadProtocol(final Properties properties, final Protoc .toUpperCase()); } + protected static Boolean loadAcquireHostList(final Properties properties, final Boolean currentValue) { + return Boolean.parseBoolean(getProperty(properties, PROPERTY_KEY_ACQUIRE_HOST_LIST, currentValue, + ArangoDBConstants.DEFAULT_ACQUIRE_HOST_LIST)); + } + + protected static LoadBalancingStrategy loadLoadBalancingStrategy( + final Properties properties, + final LoadBalancingStrategy currentValue) { + return LoadBalancingStrategy.valueOf(getProperty(properties, PROPERTY_KEY_LOAD_BALANCING_STRATEGY, currentValue, + ArangoDBConstants.DEFAULT_LOAD_BALANCING_STRATEGY).toUpperCase()); + } + private static String getProperty( final Properties properties, final String key, diff --git a/src/main/java/com/arangodb/internal/InternalArangoDatabase.java b/src/main/java/com/arangodb/internal/InternalArangoDatabase.java index 7a3bdd5de..2032faf9f 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoDatabase.java +++ b/src/main/java/com/arangodb/internal/InternalArangoDatabase.java @@ -35,7 +35,7 @@ import com.arangodb.entity.QueryTrackingPropertiesEntity; import com.arangodb.entity.TraversalEntity; import com.arangodb.internal.ArangoExecutor.ResponseDeserializer; -import com.arangodb.internal.velocystream.internal.Connection; +import com.arangodb.internal.velocystream.internal.VstConnection; import com.arangodb.model.AqlFunctionCreateOptions; import com.arangodb.model.AqlFunctionDeleteOptions; import com.arangodb.model.AqlFunctionGetOptions; @@ -61,7 +61,7 @@ * @author Mark Vollmary * */ -public class InternalArangoDatabase, E extends ArangoExecutor, R, C extends Connection> +public class InternalArangoDatabase, E extends ArangoExecutor, R, C extends VstConnection> extends ArangoExecuteable { private final String name; diff --git a/src/main/java/com/arangodb/internal/InternalArangoEdgeCollection.java b/src/main/java/com/arangodb/internal/InternalArangoEdgeCollection.java index e9a7c222e..fb1f748b2 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoEdgeCollection.java +++ b/src/main/java/com/arangodb/internal/InternalArangoEdgeCollection.java @@ -27,7 +27,7 @@ import com.arangodb.entity.EdgeEntity; import com.arangodb.entity.EdgeUpdateEntity; import com.arangodb.internal.ArangoExecutor.ResponseDeserializer; -import com.arangodb.internal.velocystream.internal.Connection; +import com.arangodb.internal.velocystream.internal.VstConnection; import com.arangodb.model.DocumentReadOptions; import com.arangodb.model.EdgeCreateOptions; import com.arangodb.model.EdgeDeleteOptions; @@ -44,7 +44,7 @@ * @author Mark Vollmary * */ -public class InternalArangoEdgeCollection, D extends InternalArangoDatabase, G extends InternalArangoGraph, E extends ArangoExecutor, R, C extends Connection> +public class InternalArangoEdgeCollection, D extends InternalArangoDatabase, G extends InternalArangoGraph, E extends ArangoExecutor, R, C extends VstConnection> extends ArangoExecuteable { private final G graph; diff --git a/src/main/java/com/arangodb/internal/InternalArangoGraph.java b/src/main/java/com/arangodb/internal/InternalArangoGraph.java index 2f492150e..355d63acc 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoGraph.java +++ b/src/main/java/com/arangodb/internal/InternalArangoGraph.java @@ -25,7 +25,7 @@ import com.arangodb.entity.EdgeDefinition; import com.arangodb.entity.GraphEntity; import com.arangodb.internal.ArangoExecutor.ResponseDeserializer; -import com.arangodb.internal.velocystream.internal.Connection; +import com.arangodb.internal.velocystream.internal.VstConnection; import com.arangodb.model.OptionsBuilder; import com.arangodb.model.VertexCollectionCreateOptions; import com.arangodb.velocypack.Type; @@ -38,7 +38,7 @@ * @author Mark Vollmary * */ -public class InternalArangoGraph, D extends InternalArangoDatabase, E extends ArangoExecutor, R, C extends Connection> +public class InternalArangoGraph, D extends InternalArangoDatabase, E extends ArangoExecutor, R, C extends VstConnection> extends ArangoExecuteable { private final D db; diff --git a/src/main/java/com/arangodb/internal/InternalArangoVertexCollection.java b/src/main/java/com/arangodb/internal/InternalArangoVertexCollection.java index 6d1d0e588..735d1aed1 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoVertexCollection.java +++ b/src/main/java/com/arangodb/internal/InternalArangoVertexCollection.java @@ -27,7 +27,7 @@ import com.arangodb.entity.VertexEntity; import com.arangodb.entity.VertexUpdateEntity; import com.arangodb.internal.ArangoExecutor.ResponseDeserializer; -import com.arangodb.internal.velocystream.internal.Connection; +import com.arangodb.internal.velocystream.internal.VstConnection; import com.arangodb.model.DocumentReadOptions; import com.arangodb.model.VertexCreateOptions; import com.arangodb.model.VertexDeleteOptions; @@ -44,7 +44,7 @@ * @author Mark Vollmary * */ -public class InternalArangoVertexCollection, D extends InternalArangoDatabase, G extends InternalArangoGraph, E extends ArangoExecutor, R, C extends Connection> +public class InternalArangoVertexCollection, D extends InternalArangoDatabase, G extends InternalArangoGraph, E extends ArangoExecutor, R, C extends VstConnection> extends ArangoExecuteable { private final G graph; diff --git a/src/main/java/com/arangodb/internal/http/HttpCommunication.java b/src/main/java/com/arangodb/internal/http/HttpCommunication.java index 324bfefad..fafa9945e 100644 --- a/src/main/java/com/arangodb/internal/http/HttpCommunication.java +++ b/src/main/java/com/arangodb/internal/http/HttpCommunication.java @@ -21,67 +21,20 @@ package com.arangodb.internal.http; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.SocketException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import javax.net.ssl.SSLContext; -import org.apache.http.HeaderElement; -import org.apache.http.HeaderElementIterator; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.auth.AuthenticationException; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpHead; -import org.apache.http.client.methods.HttpPatch; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.ConnectionKeepAliveStrategy; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.auth.BasicScheme; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.message.BasicHeaderElementIterator; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.protocol.HTTP; -import org.apache.http.protocol.HttpContext; -import org.apache.http.ssl.SSLContexts; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.arangodb.ArangoDBException; import com.arangodb.Protocol; -import com.arangodb.entity.ErrorEntity; import com.arangodb.internal.ArangoDBConstants; import com.arangodb.internal.Host; -import com.arangodb.internal.HostHandler; -import com.arangodb.internal.util.CURLLogger; -import com.arangodb.internal.util.IOUtils; +import com.arangodb.internal.net.ArangoDBRedirectException; +import com.arangodb.internal.net.ConnectionPool; +import com.arangodb.internal.net.DelHostHandler; +import com.arangodb.internal.net.HostHandle; +import com.arangodb.internal.net.HostHandler; +import com.arangodb.internal.util.HostUtils; import com.arangodb.util.ArangoSerialization; -import com.arangodb.util.ArangoSerializer.Options; -import com.arangodb.velocypack.VPackSlice; -import com.arangodb.velocypack.exception.VPackParserException; import com.arangodb.velocystream.Request; import com.arangodb.velocystream.Response; @@ -108,6 +61,12 @@ public Builder(final HostHandler hostHandler, final Protocol protocol) { this.protocol = protocol; } + public Builder(final Builder builder) { + this(builder.hostHandler, builder.protocol); + timeout(builder.timeout).user(builder.user).password(builder.password).useSsl(builder.useSsl) + .sslContext(builder.sslContext).maxConnections(builder.maxConnections); + } + public Builder timeout(final Integer timeout) { this.timeout = timeout; return this; @@ -144,250 +103,45 @@ public HttpCommunication build(final ArangoSerialization util) { } } - private static final Logger LOGGER = LoggerFactory.getLogger(HttpCommunication.class); - private static final ContentType CONTENT_TYPE_APPLICATION_JSON_UTF8 = ContentType.create("application/json", - "utf-8"); - private static final ContentType CONTENT_TYPE_VPACK = ContentType.create("application/x-velocypack"); - private static final int ERROR_STATUS = 300; - private final PoolingHttpClientConnectionManager cm; - private final CloseableHttpClient client; - private final String user; - private final String password; - private final ArangoSerialization util; - private final HostHandler hostHandler; - private final Boolean useSsl; - private final Protocol contentType; + private final ConnectionPool connectionPool; private HttpCommunication(final Integer timeout, final String user, final String password, final Boolean useSsl, final SSLContext sslContext, final ArangoSerialization util, final HostHandler hostHandler, final Integer maxConnections, final Protocol contentType) { super(); - this.user = user; - this.password = password; - this.useSsl = useSsl; - this.util = util; - this.hostHandler = hostHandler; - this.contentType = contentType; - final RegistryBuilder registryBuilder = RegistryBuilder - . create(); - if (useSsl != null && useSsl) { - if (sslContext != null) { - registryBuilder.register("https", new SSLConnectionSocketFactory(sslContext)); - } else { - registryBuilder.register("https", new SSLConnectionSocketFactory(SSLContexts.createSystemDefault())); - } - } else { - registryBuilder.register("http", new PlainConnectionSocketFactory()); - } - cm = new PoolingHttpClientConnectionManager(registryBuilder.build()); - final int connections = maxConnections != null ? Math.max(1, maxConnections) - : ArangoDBConstants.MAX_CONNECTIONS_HTTP_DEFAULT; - cm.setDefaultMaxPerRoute(connections); - cm.setMaxTotal(connections); - final RequestConfig.Builder requestConfig = RequestConfig.custom(); - if (timeout != null && timeout >= 0) { - requestConfig.setConnectTimeout(timeout); - requestConfig.setConnectionRequestTimeout(timeout); - requestConfig.setSocketTimeout(timeout); - } - final ConnectionKeepAliveStrategy keepAliveStrategy = new ConnectionKeepAliveStrategy() { + connectionPool = new ConnectionPool( + maxConnections != null ? Math.max(1, maxConnections) : ArangoDBConstants.MAX_CONNECTIONS_HTTP_DEFAULT) { @Override - public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) { - return HttpCommunication.this.getKeepAliveDuration(response); + public HttpConnection createConnection(final Host host) { + return new HttpConnection(timeout, user, password, useSsl, sslContext, util, + new DelHostHandler(hostHandler, host), contentType); } }; - final HttpClientBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig.build()) - .setConnectionManager(cm).setKeepAliveStrategy(keepAliveStrategy) - .setRetryHandler(new DefaultHttpRequestRetryHandler()); - client = builder.build(); } - private long getKeepAliveDuration(final HttpResponse response) { - final HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE)); - while (it.hasNext()) { - final HeaderElement he = it.nextElement(); - final String param = he.getName(); - final String value = he.getValue(); - if (value != null && "timeout".equalsIgnoreCase(param)) { - try { - return Long.parseLong(value) * 1000L; - } catch (final NumberFormatException ignore) { - } - } - } - return 30L * 1000L; + public void disconnect() throws IOException { + connectionPool.disconnect(); } - public void disconnect() { - cm.shutdown(); + public Response execute(final Request request, final HostHandle hostHandle) throws ArangoDBException, IOException { + final HttpConnection connection = connectionPool.connection(hostHandle); try { - client.close(); - } catch (final IOException e) { - } - } - - public Response execute(final Request request) throws ArangoDBException, IOException { - Host host = hostHandler.get(); - while (true) { - try { - final String url = buildUrl(buildBaseUrl(host), request); - final HttpRequestBase httpRequest = buildHttpRequestBase(request, url); - httpRequest.setHeader("User-Agent", - "Mozilla/5.0 (compatible; ArangoDB-JavaDriver/1.1; +http://mt.orz.at/)"); - if (contentType == Protocol.HTTP_VPACK) { - httpRequest.setHeader("Accept", "application/x-velocypack"); - } - addHeader(request, httpRequest); - final Credentials credentials = addCredentials(httpRequest); - if (LOGGER.isDebugEnabled()) { - CURLLogger.log(url, request, credentials, util); - } - Response response; - response = buildResponse(client.execute(httpRequest)); - checkError(response); - hostHandler.success(); - return response; - } catch (final SocketException e) { - hostHandler.fail(); - final Host failedHost = host; - host = hostHandler.change(); - if (host != null) { - LOGGER.warn(String.format("Could not connect to %s. Try connecting to %s", failedHost, host)); - } else { - throw e; - } - } - } - } - - private HttpRequestBase buildHttpRequestBase(final Request request, final String url) { - final HttpRequestBase httpRequest; - switch (request.getRequestType()) { - case POST: - httpRequest = requestWithBody(new HttpPost(url), request); - break; - case PUT: - httpRequest = requestWithBody(new HttpPut(url), request); - break; - case PATCH: - httpRequest = requestWithBody(new HttpPatch(url), request); - break; - case DELETE: - httpRequest = requestWithBody(new HttpDeleteWithBody(url), request); - break; - case HEAD: - httpRequest = new HttpHead(url); - break; - case GET: - default: - httpRequest = new HttpGet(url); - break; - } - return httpRequest; - } - - private HttpRequestBase requestWithBody(final HttpEntityEnclosingRequestBase httpRequest, final Request request) { - final VPackSlice body = request.getBody(); - if (body != null) { - if (contentType == Protocol.HTTP_VPACK) { - httpRequest.setEntity(new ByteArrayEntity( - Arrays.copyOfRange(body.getBuffer(), body.getStart(), body.getStart() + body.getByteSize()), - CONTENT_TYPE_VPACK)); - } else { - httpRequest.setEntity(new StringEntity(body.toString(), CONTENT_TYPE_APPLICATION_JSON_UTF8)); - } - } - return httpRequest; - } - - private String buildBaseUrl(final Host host) { - return (useSsl != null && useSsl ? "https://" : "http://") + host.getHost() + ":" + host.getPort(); - } - - private static String buildUrl(final String baseUrl, final Request request) throws UnsupportedEncodingException { - final StringBuilder sb = new StringBuilder().append(baseUrl); - final String database = request.getDatabase(); - if (database != null && !database.isEmpty()) { - sb.append("/_db/").append(database); - } - sb.append(request.getRequest()); - if (!request.getQueryParam().isEmpty()) { - if (request.getRequest().contains("?")) { - sb.append("&"); + return execute(request, connection); + } catch (final ArangoDBException e) { + if (e instanceof ArangoDBRedirectException) { + final String location = ArangoDBRedirectException.class.cast(e).getLocation(); + final Host host = HostUtils.createFromLocation(location); + connectionPool.closeConnectionOnError(connection); + return execute(request, new HostHandle().setHost(host)); } else { - sb.append("?"); - } - final String paramString = URLEncodedUtils.format(toList(request.getQueryParam()), "utf-8"); - sb.append(paramString); - } - return sb.toString(); - } - - private static List toList(final Map parameters) { - final ArrayList paramList = new ArrayList(parameters.size()); - for (final Entry param : parameters.entrySet()) { - if (param.getValue() != null) { - paramList.add(new BasicNameValuePair(param.getKey(), param.getValue().toString())); + throw e; } } - return paramList; - } - - private static void addHeader(final Request request, final HttpRequestBase httpRequest) { - for (final Entry header : request.getHeaderParam().entrySet()) { - httpRequest.addHeader(header.getKey(), header.getValue()); - } } - public Credentials addCredentials(final HttpRequestBase httpRequest) { - Credentials credentials = null; - if (user != null) { - credentials = new UsernamePasswordCredentials(user, password != null ? password : ""); - try { - httpRequest.addHeader(new BasicScheme().authenticate(credentials, httpRequest, null)); - } catch (final AuthenticationException e) { - throw new ArangoDBException(e); - } - } - return credentials; - } - - public Response buildResponse(final CloseableHttpResponse httpResponse) - throws UnsupportedOperationException, IOException { - final Response response = new Response(); - response.setResponseCode(httpResponse.getStatusLine().getStatusCode()); - final HttpEntity entity = httpResponse.getEntity(); - if (entity != null && entity.getContent() != null) { - if (contentType == Protocol.HTTP_VPACK) { - final byte[] content = IOUtils.toByteArray(entity.getContent()); - if (content.length > 0) { - response.setBody(new VPackSlice(content)); - } - } else { - final String content = IOUtils.toString(entity.getContent()); - if (!content.isEmpty()) { - response.setBody( - util.serialize(content, new Options().stringAsJson(true).serializeNullValues(true))); - } - } - } - return response; - } - - protected void checkError(final Response response) throws ArangoDBException { - try { - if (response.getResponseCode() >= ERROR_STATUS) { - if (response.getBody() != null) { - final ErrorEntity errorEntity = util.deserialize(response.getBody(), ErrorEntity.class); - throw new ArangoDBException(errorEntity); - } else { - throw new ArangoDBException(String.format("Response Code: %s", response.getResponseCode()), - response.getResponseCode()); - } - } - } catch (final VPackParserException e) { - throw new ArangoDBException(e); - } + protected Response execute(final Request request, final HttpConnection connection) + throws ArangoDBException, IOException { + return connection.execute(request); } } diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java new file mode 100644 index 000000000..bea703dd7 --- /dev/null +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -0,0 +1,343 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.http; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.net.ssl.SSLContext; + +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.HeaderElementIterator; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.ConnectionKeepAliveStrategy; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.message.BasicHeaderElementIterator; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.protocol.HTTP; +import org.apache.http.protocol.HttpContext; +import org.apache.http.ssl.SSLContexts; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.arangodb.ArangoDBException; +import com.arangodb.Protocol; +import com.arangodb.internal.Host; +import com.arangodb.internal.net.Connection; +import com.arangodb.internal.net.HostHandler; +import com.arangodb.internal.util.CURLLogger; +import com.arangodb.internal.util.IOUtils; +import com.arangodb.internal.util.ResponseUtils; +import com.arangodb.util.ArangoSerialization; +import com.arangodb.util.ArangoSerializer.Options; +import com.arangodb.velocypack.VPackSlice; +import com.arangodb.velocystream.Request; +import com.arangodb.velocystream.Response; + +/** + * @author Mark Vollmary + * + */ +public class HttpConnection implements Connection { + + private static final Logger LOGGER = LoggerFactory.getLogger(HttpCommunication.class); + private static final ContentType CONTENT_TYPE_APPLICATION_JSON_UTF8 = ContentType.create("application/json", + "utf-8"); + private static final ContentType CONTENT_TYPE_VPACK = ContentType.create("application/x-velocypack"); + private final PoolingHttpClientConnectionManager cm; + private final CloseableHttpClient client; + private final String user; + private final String password; + private final ArangoSerialization util; + private final HostHandler hostHandler; + private final Boolean useSsl; + private final Protocol contentType; + private Host host; + + public HttpConnection(final Integer timeout, final String user, final String password, final Boolean useSsl, + final SSLContext sslContext, final ArangoSerialization util, final HostHandler hostHandler, + final Protocol contentType) { + super(); + this.user = user; + this.password = password; + this.useSsl = useSsl; + this.util = util; + this.hostHandler = hostHandler; + this.contentType = contentType; + final RegistryBuilder registryBuilder = RegistryBuilder + . create(); + if (useSsl != null && useSsl) { + if (sslContext != null) { + registryBuilder.register("https", new SSLConnectionSocketFactory(sslContext)); + } else { + registryBuilder.register("https", new SSLConnectionSocketFactory(SSLContexts.createSystemDefault())); + } + } else { + registryBuilder.register("http", new PlainConnectionSocketFactory()); + } + cm = new PoolingHttpClientConnectionManager(registryBuilder.build()); + cm.setDefaultMaxPerRoute(1); + cm.setMaxTotal(1); + final RequestConfig.Builder requestConfig = RequestConfig.custom(); + if (timeout != null && timeout >= 0) { + requestConfig.setConnectTimeout(timeout); + requestConfig.setConnectionRequestTimeout(timeout); + requestConfig.setSocketTimeout(timeout); + } + final ConnectionKeepAliveStrategy keepAliveStrategy = new ConnectionKeepAliveStrategy() { + @Override + public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) { + return HttpConnection.this.getKeepAliveDuration(response); + } + }; + final HttpClientBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig.build()) + .setConnectionManager(cm).setKeepAliveStrategy(keepAliveStrategy) + .setRetryHandler(new DefaultHttpRequestRetryHandler()); + client = builder.build(); + } + + @Override + public Host getHost() { + return host; + } + + private long getKeepAliveDuration(final HttpResponse response) { + final HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE)); + while (it.hasNext()) { + final HeaderElement he = it.nextElement(); + final String param = he.getName(); + final String value = he.getValue(); + if (value != null && "timeout".equalsIgnoreCase(param)) { + try { + return Long.parseLong(value) * 1000L; + } catch (final NumberFormatException ignore) { + } + } + } + return 30L * 1000L; + } + + @Override + public void close() throws IOException { + cm.shutdown(); + client.close(); + } + + @Override + public void closeOnError() throws IOException { + hostHandler.fail(); + close(); + } + + public Response execute(final Request request) throws ArangoDBException, IOException { + host = hostHandler.get(); + while (true) { + if (host == null) { + throw new ArangoDBException("Was not able to connect to any host"); + } + try { + final String url = buildUrl(buildBaseUrl(host), request); + final HttpRequestBase httpRequest = buildHttpRequestBase(request, url); + httpRequest.setHeader("User-Agent", + "Mozilla/5.0 (compatible; ArangoDB-JavaDriver/1.1; +http://mt.orz.at/)"); + if (contentType == Protocol.HTTP_VPACK) { + httpRequest.setHeader("Accept", "application/x-velocypack"); + } + addHeader(request, httpRequest); + final Credentials credentials = addCredentials(httpRequest); + if (LOGGER.isDebugEnabled()) { + CURLLogger.log(url, request, credentials, util); + } + Response response; + response = buildResponse(client.execute(httpRequest)); + checkError(response); + hostHandler.success(); + return response; + } catch (final SocketException e) { + hostHandler.fail(); + final Host failedHost = host; + host = hostHandler.get(); + if (host != null) { + LOGGER.warn(String.format("Could not connect to %s. Try connecting to %s", failedHost, host)); + } else { + throw e; + } + } + } + } + + private HttpRequestBase buildHttpRequestBase(final Request request, final String url) { + final HttpRequestBase httpRequest; + switch (request.getRequestType()) { + case POST: + httpRequest = requestWithBody(new HttpPost(url), request); + break; + case PUT: + httpRequest = requestWithBody(new HttpPut(url), request); + break; + case PATCH: + httpRequest = requestWithBody(new HttpPatch(url), request); + break; + case DELETE: + httpRequest = requestWithBody(new HttpDeleteWithBody(url), request); + break; + case HEAD: + httpRequest = new HttpHead(url); + break; + case GET: + default: + httpRequest = new HttpGet(url); + break; + } + return httpRequest; + } + + private HttpRequestBase requestWithBody(final HttpEntityEnclosingRequestBase httpRequest, final Request request) { + final VPackSlice body = request.getBody(); + if (body != null) { + if (contentType == Protocol.HTTP_VPACK) { + httpRequest.setEntity(new ByteArrayEntity( + Arrays.copyOfRange(body.getBuffer(), body.getStart(), body.getStart() + body.getByteSize()), + CONTENT_TYPE_VPACK)); + } else { + httpRequest.setEntity(new StringEntity(body.toString(), CONTENT_TYPE_APPLICATION_JSON_UTF8)); + } + } + return httpRequest; + } + + private String buildBaseUrl(final Host host) { + return (useSsl != null && useSsl ? "https://" : "http://") + host.getHost() + ":" + host.getPort(); + } + + private static String buildUrl(final String baseUrl, final Request request) throws UnsupportedEncodingException { + final StringBuilder sb = new StringBuilder().append(baseUrl); + final String database = request.getDatabase(); + if (database != null && !database.isEmpty()) { + sb.append("/_db/").append(database); + } + sb.append(request.getRequest()); + if (!request.getQueryParam().isEmpty()) { + if (request.getRequest().contains("?")) { + sb.append("&"); + } else { + sb.append("?"); + } + final String paramString = URLEncodedUtils.format(toList(request.getQueryParam()), "utf-8"); + sb.append(paramString); + } + return sb.toString(); + } + + private static List toList(final Map parameters) { + final ArrayList paramList = new ArrayList(parameters.size()); + for (final Entry param : parameters.entrySet()) { + if (param.getValue() != null) { + paramList.add(new BasicNameValuePair(param.getKey(), param.getValue().toString())); + } + } + return paramList; + } + + private static void addHeader(final Request request, final HttpRequestBase httpRequest) { + for (final Entry header : request.getHeaderParam().entrySet()) { + httpRequest.addHeader(header.getKey(), header.getValue()); + } + } + + public Credentials addCredentials(final HttpRequestBase httpRequest) { + Credentials credentials = null; + if (user != null) { + credentials = new UsernamePasswordCredentials(user, password != null ? password : ""); + try { + httpRequest.addHeader(new BasicScheme().authenticate(credentials, httpRequest, null)); + } catch (final AuthenticationException e) { + throw new ArangoDBException(e); + } + } + return credentials; + } + + public Response buildResponse(final CloseableHttpResponse httpResponse) + throws UnsupportedOperationException, IOException { + final Response response = new Response(); + response.setResponseCode(httpResponse.getStatusLine().getStatusCode()); + final HttpEntity entity = httpResponse.getEntity(); + if (entity != null && entity.getContent() != null) { + if (contentType == Protocol.HTTP_VPACK) { + final byte[] content = IOUtils.toByteArray(entity.getContent()); + if (content.length > 0) { + response.setBody(new VPackSlice(content)); + } + } else { + final String content = IOUtils.toString(entity.getContent()); + if (!content.isEmpty()) { + response.setBody( + util.serialize(content, new Options().stringAsJson(true).serializeNullValues(true))); + } + } + } + final Header[] headers = httpResponse.getAllHeaders(); + final Map meta = response.getMeta(); + for (final Header header : headers) { + meta.put(header.getName(), header.getValue()); + } + return response; + } + + protected void checkError(final Response response) throws ArangoDBException { + ResponseUtils.checkError(util, response); + } + +} diff --git a/src/main/java/com/arangodb/internal/http/HttpProtocol.java b/src/main/java/com/arangodb/internal/http/HttpProtocol.java index f5dab9b81..4c5e54bd9 100644 --- a/src/main/java/com/arangodb/internal/http/HttpProtocol.java +++ b/src/main/java/com/arangodb/internal/http/HttpProtocol.java @@ -25,7 +25,8 @@ import org.apache.http.client.ClientProtocolException; import com.arangodb.ArangoDBException; -import com.arangodb.internal.CommunicationProtocol; +import com.arangodb.internal.net.CommunicationProtocol; +import com.arangodb.internal.net.HostHandle; import com.arangodb.velocystream.Request; import com.arangodb.velocystream.Response; @@ -43,9 +44,9 @@ public HttpProtocol(final HttpCommunication httpCommunitaction) { } @Override - public Response execute(final Request request) throws ArangoDBException { + public Response execute(final Request request, final HostHandle hostHandle) throws ArangoDBException { try { - return httpCommunitaction.execute(request); + return httpCommunitaction.execute(request, hostHandle); } catch (final ClientProtocolException e) { throw new ArangoDBException(e); } catch (final IOException e) { diff --git a/src/main/java/com/arangodb/internal/net/ArangoDBRedirectException.java b/src/main/java/com/arangodb/internal/net/ArangoDBRedirectException.java new file mode 100644 index 000000000..a8461572c --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/ArangoDBRedirectException.java @@ -0,0 +1,43 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import com.arangodb.ArangoDBException; + +/** + * @author Mark Vollmary + * + */ +public class ArangoDBRedirectException extends ArangoDBException { + + private static final long serialVersionUID = -94810262465567613L; + private final String location; + + public ArangoDBRedirectException(final String message, final String location) { + super(message); + this.location = location; + } + + public String getLocation() { + return location; + } + +} diff --git a/src/main/java/com/arangodb/internal/CommunicationProtocol.java b/src/main/java/com/arangodb/internal/net/CommunicationProtocol.java similarity index 85% rename from src/main/java/com/arangodb/internal/CommunicationProtocol.java rename to src/main/java/com/arangodb/internal/net/CommunicationProtocol.java index f44ec30de..10540b932 100644 --- a/src/main/java/com/arangodb/internal/CommunicationProtocol.java +++ b/src/main/java/com/arangodb/internal/net/CommunicationProtocol.java @@ -18,7 +18,7 @@ * Copyright holder is ArangoDB GmbH, Cologne, Germany */ -package com.arangodb.internal; +package com.arangodb.internal.net; import java.io.Closeable; @@ -32,6 +32,6 @@ */ public interface CommunicationProtocol extends Closeable { - Response execute(final Request request) throws ArangoDBException; + Response execute(final Request request, HostHandle hostHandle) throws ArangoDBException; } diff --git a/src/main/java/com/arangodb/internal/net/Connection.java b/src/main/java/com/arangodb/internal/net/Connection.java new file mode 100644 index 000000000..f790467f1 --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/Connection.java @@ -0,0 +1,38 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import java.io.Closeable; +import java.io.IOException; + +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public interface Connection extends Closeable { + + Host getHost(); + + void closeOnError() throws IOException; + +} diff --git a/src/main/java/com/arangodb/internal/net/ConnectionPool.java b/src/main/java/com/arangodb/internal/net/ConnectionPool.java new file mode 100644 index 000000000..a1af33972 --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/ConnectionPool.java @@ -0,0 +1,96 @@ +/* + * DISCLAIMER + * + * Copyright 2016 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import java.io.IOException; +import java.util.LinkedList; + +import com.arangodb.ArangoDBException; +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public abstract class ConnectionPool { + + private final LinkedList connections; + private final int maxConnections; + + public ConnectionPool(final Integer maxConnections) { + super(); + this.maxConnections = maxConnections; + connections = new LinkedList(); + } + + public abstract C createConnection(final Host host); + + public synchronized C connection(final HostHandle hostHandle) { + final C c; + if (hostHandle == null || hostHandle.getHost() == null) { + if (connections.size() < maxConnections) { + c = createConnection(null); + } else { + c = connections.removeFirst(); + } + if (hostHandle != null) { + hostHandle.setHost(c.getHost()); + } + } else { + final Host host = hostHandle.getHost(); + C tmp = null; + for (final C connection : connections) { + if (connection.getHost().equals(host)) { + tmp = connection; + connections.remove(tmp); + break; + } + } + c = tmp != null ? tmp : createConnection(host); + } + connections.add(c); + return c; + } + + public void disconnect() throws IOException { + while (!connections.isEmpty()) { + connections.removeLast().close(); + } + } + + public void closeConnection(final C connection) { + try { + connection.close(); + connections.remove(connection); + } catch (final IOException e) { + throw new ArangoDBException(e); + } + } + + public void closeConnectionOnError(final C connection) { + try { + connection.closeOnError(); + connections.remove(connection); + } catch (final IOException e) { + throw new ArangoDBException(e); + } + } +} diff --git a/src/main/java/com/arangodb/internal/net/DelHostHandler.java b/src/main/java/com/arangodb/internal/net/DelHostHandler.java new file mode 100644 index 000000000..6b8c40bb8 --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/DelHostHandler.java @@ -0,0 +1,61 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public class DelHostHandler implements HostHandler { + + private final HostHandler hostHandler; + private Host host; + + public DelHostHandler(final HostHandler hostHandler, final Host host) { + super(); + this.hostHandler = hostHandler; + this.host = host; + } + + @Override + public Host get() { + return host != null ? host : hostHandler.get(); + } + + @Override + public void success() { + if (host == null) { + hostHandler.success(); + } + } + + @Override + public void fail() { + if (host == null) { + hostHandler.fail(); + } else { + host = null; + } + } + +} diff --git a/src/main/java/com/arangodb/internal/net/ExtendedHostResolver.java b/src/main/java/com/arangodb/internal/net/ExtendedHostResolver.java new file mode 100644 index 000000000..02acad2cb --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/ExtendedHostResolver.java @@ -0,0 +1,74 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public class ExtendedHostResolver implements HostResolver { + + private static final long MAX_CACHE_TIME = 60 * 60 * 1000; + private EndpointResolver resolver; + private final List hosts; + private long lastUpdate; + + public ExtendedHostResolver(final List hosts) { + super(); + this.hosts = new ArrayList(hosts); + lastUpdate = 0; + } + + @Override + public void init(final EndpointResolver resolver) { + this.resolver = resolver; + } + + @Override + public List resolve(final boolean initial, final boolean closeConnections) { + if (!initial && isExpired()) { + lastUpdate = System.currentTimeMillis(); + final Collection endpoints = resolver.resolve(closeConnections); + if (!endpoints.isEmpty()) { + hosts.clear(); + } + for (final String endpoint : endpoints) { + if (endpoint.matches(".*://.+:[0-9]+")) { + final String[] s = endpoint.replaceAll(".*://", "").split(":"); + if (s.length == 2) { + hosts.add(new Host(s[0], Integer.valueOf(s[1]))); + } + } + } + } + return hosts; + } + + private boolean isExpired() { + return System.currentTimeMillis() > lastUpdate + MAX_CACHE_TIME; + } +} diff --git a/src/main/java/com/arangodb/internal/DefaultHostHandler.java b/src/main/java/com/arangodb/internal/net/FallbackHostHandler.java similarity index 56% rename from src/main/java/com/arangodb/internal/DefaultHostHandler.java rename to src/main/java/com/arangodb/internal/net/FallbackHostHandler.java index 523222b9e..d2b3e23e7 100644 --- a/src/main/java/com/arangodb/internal/DefaultHostHandler.java +++ b/src/main/java/com/arangodb/internal/net/FallbackHostHandler.java @@ -18,42 +18,32 @@ * Copyright holder is ArangoDB GmbH, Cologne, Germany */ -package com.arangodb.internal; +package com.arangodb.internal.net; import java.util.List; +import com.arangodb.internal.Host; + /** * @author Mark Vollmary * */ -public class DefaultHostHandler implements HostHandler { +public class FallbackHostHandler implements HostHandler { - private final List hosts; - private int current; - private int lastSuccess; + private Host current; + private Host lastSuccess; private int iterations; + private final HostResolver resolver; - /** - * @param hosts - */ - public DefaultHostHandler(final List hosts) { - this.hosts = hosts; - current = lastSuccess = iterations = 0; + public FallbackHostHandler(final HostResolver resolver) { + this.resolver = resolver; + iterations = 0; + current = lastSuccess = resolver.resolve(true, false).get(0); } @Override public Host get() { - return hosts.get(current); - } - - @Override - public Host change() { - current++; - if ((current + 1) > hosts.size()) { - current -= hosts.size(); - iterations++; - } - return current != lastSuccess || iterations < 3 ? get() : null; + return current != lastSuccess || iterations < 3 ? current : null; } @Override @@ -63,6 +53,13 @@ public void success() { @Override public void fail() { + final List hosts = resolver.resolve(false, false); + final int index = hosts.indexOf(current) + 1; + final boolean inBound = index < hosts.size(); + current = hosts.get(inBound ? index : 0); + if (!inBound) { + iterations++; + } } } diff --git a/src/main/java/com/arangodb/internal/net/HostHandle.java b/src/main/java/com/arangodb/internal/net/HostHandle.java new file mode 100644 index 000000000..1569a790a --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/HostHandle.java @@ -0,0 +1,46 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public class HostHandle { + + private Host host; + + public HostHandle() { + super(); + } + + public Host getHost() { + return host; + } + + public HostHandle setHost(final Host host) { + this.host = host; + return this; + } + +} diff --git a/src/main/java/com/arangodb/internal/HostHandler.java b/src/main/java/com/arangodb/internal/net/HostHandler.java similarity index 88% rename from src/main/java/com/arangodb/internal/HostHandler.java rename to src/main/java/com/arangodb/internal/net/HostHandler.java index f89c4e1c1..1740f041a 100644 --- a/src/main/java/com/arangodb/internal/HostHandler.java +++ b/src/main/java/com/arangodb/internal/net/HostHandler.java @@ -18,7 +18,9 @@ * Copyright holder is ArangoDB GmbH, Cologne, Germany */ -package com.arangodb.internal; +package com.arangodb.internal.net; + +import com.arangodb.internal.Host; /** * @author Mark Vollmary @@ -28,8 +30,6 @@ public interface HostHandler { Host get(); - Host change(); - void success(); void fail(); diff --git a/src/main/java/com/arangodb/internal/net/HostResolver.java b/src/main/java/com/arangodb/internal/net/HostResolver.java new file mode 100644 index 000000000..c71bbeee9 --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/HostResolver.java @@ -0,0 +1,43 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import java.util.Collection; +import java.util.List; + +import com.arangodb.ArangoDBException; +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public interface HostResolver { + + public interface EndpointResolver { + Collection resolve(boolean closeConnections) throws ArangoDBException; + } + + void init(final EndpointResolver resolver); + + List resolve(boolean initial, boolean closeConnections); + +} diff --git a/src/main/java/com/arangodb/internal/net/RandomHostHandler.java b/src/main/java/com/arangodb/internal/net/RandomHostHandler.java new file mode 100644 index 000000000..e4888d982 --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/RandomHostHandler.java @@ -0,0 +1,71 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import java.util.ArrayList; +import java.util.Collections; + +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public class RandomHostHandler implements HostHandler { + + private final HostResolver resolver; + private final HostHandler fallback; + private Host origin; + private Host current; + + public RandomHostHandler(final HostResolver resolver, final HostHandler fallback) { + super(); + this.resolver = resolver; + this.fallback = fallback; + origin = current = getRandomHost(true, false); + } + + @Override + public Host get() { + if (current == null) { + origin = current = getRandomHost(false, true); + } + return current; + } + + @Override + public void success() { + current = origin; + } + + @Override + public void fail() { + fallback.fail(); + current = fallback.get(); + } + + private Host getRandomHost(final boolean initial, final boolean closeConnections) { + final ArrayList hosts = new ArrayList(resolver.resolve(initial, closeConnections)); + Collections.shuffle(hosts); + return hosts.get(0); + } + +} diff --git a/src/main/java/com/arangodb/internal/net/RoundRobinHostHandler.java b/src/main/java/com/arangodb/internal/net/RoundRobinHostHandler.java new file mode 100644 index 000000000..ca663f42b --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/RoundRobinHostHandler.java @@ -0,0 +1,58 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import java.util.List; + +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public class RoundRobinHostHandler implements HostHandler { + + private final HostResolver resolver; + private Host current; + + public RoundRobinHostHandler(final HostResolver resolver) { + super(); + this.resolver = resolver; + current = resolver.resolve(true, false).get(0); + } + + @Override + public Host get() { + final List hosts = resolver.resolve(false, false); + final int index = hosts.indexOf(current) + 1; + current = hosts.get(index < hosts.size() ? index : 0); + return current; + } + + @Override + public void success() { + } + + @Override + public void fail() { + } + +} diff --git a/src/main/java/com/arangodb/internal/net/SimpleHostResolver.java b/src/main/java/com/arangodb/internal/net/SimpleHostResolver.java new file mode 100644 index 000000000..2a2348c68 --- /dev/null +++ b/src/main/java/com/arangodb/internal/net/SimpleHostResolver.java @@ -0,0 +1,49 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.net; + +import java.util.List; + +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public class SimpleHostResolver implements HostResolver { + + private final List hosts; + + public SimpleHostResolver(final List hosts) { + super(); + this.hosts = hosts; + } + + @Override + public void init(final EndpointResolver resolver) { + } + + @Override + public List resolve(final boolean initial, final boolean closeConnections) { + return hosts; + } + +} diff --git a/src/main/java/com/arangodb/internal/util/HostUtils.java b/src/main/java/com/arangodb/internal/util/HostUtils.java new file mode 100644 index 000000000..0e5e53692 --- /dev/null +++ b/src/main/java/com/arangodb/internal/util/HostUtils.java @@ -0,0 +1,46 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.util; + +import com.arangodb.internal.Host; + +/** + * @author Mark Vollmary + * + */ +public class HostUtils { + + private HostUtils() { + super(); + } + + public static Host createFromLocation(final String location) { + final Host host; + if (location != null) { + final String[] tmp = location.replaceAll(".*://", "").replaceAll("/.*", "").split(":"); + host = tmp.length == 2 ? new Host(tmp[0], Integer.valueOf(tmp[1])) : null; + } else { + host = null; + } + return host; + } + +} diff --git a/src/main/java/com/arangodb/internal/util/ResponseUtils.java b/src/main/java/com/arangodb/internal/util/ResponseUtils.java new file mode 100644 index 000000000..0b26104fa --- /dev/null +++ b/src/main/java/com/arangodb/internal/util/ResponseUtils.java @@ -0,0 +1,62 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.util; + +import com.arangodb.ArangoDBException; +import com.arangodb.entity.ErrorEntity; +import com.arangodb.internal.net.ArangoDBRedirectException; +import com.arangodb.util.ArangoSerialization; +import com.arangodb.velocypack.exception.VPackParserException; +import com.arangodb.velocystream.Response; + +/** + * @author Mark Vollmary + * + */ +public class ResponseUtils { + + private static final int ERROR_STATUS = 300; + private static final int ERROR_INTERNAL = 503; + private static final String HEADER_ENDPOINT = "x-arango-endpoint"; + + private ResponseUtils() { + super(); + } + + public static void checkError(final ArangoSerialization util, final Response response) throws ArangoDBException { + try { + final int responseCode = response.getResponseCode(); + if (responseCode >= ERROR_STATUS) { + if (responseCode == ERROR_INTERNAL && response.getMeta().containsKey(HEADER_ENDPOINT)) { + throw new ArangoDBRedirectException(String.format("Response Code: %s", responseCode), + response.getMeta().get(HEADER_ENDPOINT)); + } else if (response.getBody() != null) { + final ErrorEntity errorEntity = util.deserialize(response.getBody(), ErrorEntity.class); + throw new ArangoDBException(errorEntity); + } else { + throw new ArangoDBException(String.format("Response Code: %s", responseCode), responseCode); + } + } + } catch (final VPackParserException e) { + throw new ArangoDBException(e); + } + } +} diff --git a/src/main/java/com/arangodb/internal/velocypack/VPackDeserializers.java b/src/main/java/com/arangodb/internal/velocypack/VPackDeserializers.java index 305eee780..b23fe3dbf 100644 --- a/src/main/java/com/arangodb/internal/velocypack/VPackDeserializers.java +++ b/src/main/java/com/arangodb/internal/velocypack/VPackDeserializers.java @@ -52,6 +52,7 @@ public class VPackDeserializers { private static final String DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; public static final VPackDeserializer RESPONSE = new VPackDeserializer() { + @SuppressWarnings("unchecked") @Override public Response deserialize( final VPackSlice parent, @@ -61,6 +62,9 @@ public Response deserialize( response.setVersion(vpack.get(0).getAsInt()); response.setType(vpack.get(1).getAsInt()); response.setResponseCode(vpack.get(2).getAsInt()); + if (vpack.size() > 3) { + response.setMeta(context.deserialize(vpack.get(3), Map.class)); + } return response; } }; diff --git a/src/main/java/com/arangodb/internal/velocystream/VstCommunication.java b/src/main/java/com/arangodb/internal/velocystream/VstCommunication.java index 221a3a886..ed3891c84 100644 --- a/src/main/java/com/arangodb/internal/velocystream/VstCommunication.java +++ b/src/main/java/com/arangodb/internal/velocystream/VstCommunication.java @@ -31,12 +31,16 @@ import org.slf4j.LoggerFactory; import com.arangodb.ArangoDBException; -import com.arangodb.entity.ErrorEntity; import com.arangodb.internal.ArangoDBConstants; +import com.arangodb.internal.Host; +import com.arangodb.internal.net.ArangoDBRedirectException; +import com.arangodb.internal.net.ConnectionPool; +import com.arangodb.internal.net.HostHandle; +import com.arangodb.internal.util.HostUtils; +import com.arangodb.internal.util.ResponseUtils; import com.arangodb.internal.velocystream.internal.Chunk; -import com.arangodb.internal.velocystream.internal.Connection; -import com.arangodb.internal.velocystream.internal.ConnectionPool; import com.arangodb.internal.velocystream.internal.Message; +import com.arangodb.internal.velocystream.internal.VstConnection; import com.arangodb.util.ArangoSerialization; import com.arangodb.velocypack.VPackSlice; import com.arangodb.velocypack.exception.VPackParserException; @@ -47,9 +51,7 @@ * @author Mark Vollmary * */ -public abstract class VstCommunication { - - private static final int ERROR_STATUS = 300; +public abstract class VstCommunication { private static final Logger LOGGER = LoggerFactory.getLogger(VstCommunication.class); @@ -88,36 +90,36 @@ protected void connect(final C connection) { protected abstract void authenticate(final C connection); - public void disconnect() { + public void disconnect() throws IOException { connectionPool.disconnect(); } - public R execute(final Request request) throws ArangoDBException { - return execute(request, connectionPool.connection()); + public R execute(final Request request, final HostHandle hostHandle) throws ArangoDBException { + final C connection = connectionPool.connection(hostHandle); + try { + return execute(request, connection); + } catch (final ArangoDBException e) { + if (e instanceof ArangoDBRedirectException) { + final String location = ArangoDBRedirectException.class.cast(e).getLocation(); + final Host host = HostUtils.createFromLocation(location); + connectionPool.closeConnectionOnError(connection); + return execute(request, new HostHandle().setHost(host)); + } else { + throw e; + } + } } - public abstract R execute(final Request request, C connection) throws ArangoDBException; + protected abstract R execute(final Request request, C connection) throws ArangoDBException; protected void checkError(final Response response) throws ArangoDBException { - try { - if (response.getResponseCode() >= ERROR_STATUS) { - if (response.getBody() != null) { - final ErrorEntity errorEntity = util.deserialize(response.getBody(), ErrorEntity.class); - throw new ArangoDBException(errorEntity); - } else { - throw new ArangoDBException(String.format("Response Code: %s", response.getResponseCode()), - response.getResponseCode()); - } - } - } catch (final VPackParserException e) { - throw new ArangoDBException(e); - } + ResponseUtils.checkError(util, response); } - protected Response createResponse(final Message messsage) throws VPackParserException { - final Response response = util.deserialize(messsage.getHead(), Response.class); - if (messsage.getBody() != null) { - response.setBody(messsage.getBody()); + protected Response createResponse(final Message message) throws VPackParserException { + final Response response = util.deserialize(message.getHead(), Response.class); + if (message.getBody() != null) { + response.setBody(message.getBody()); } return response; } diff --git a/src/main/java/com/arangodb/internal/velocystream/VstCommunicationSync.java b/src/main/java/com/arangodb/internal/velocystream/VstCommunicationSync.java index 1a5a48bf6..e90c5a6e6 100644 --- a/src/main/java/com/arangodb/internal/velocystream/VstCommunicationSync.java +++ b/src/main/java/com/arangodb/internal/velocystream/VstCommunicationSync.java @@ -28,9 +28,11 @@ import com.arangodb.ArangoDBException; import com.arangodb.internal.ArangoDBConstants; import com.arangodb.internal.CollectionCache; -import com.arangodb.internal.HostHandler; +import com.arangodb.internal.Host; +import com.arangodb.internal.net.ConnectionPool; +import com.arangodb.internal.net.DelHostHandler; +import com.arangodb.internal.net.HostHandler; import com.arangodb.internal.velocystream.internal.AuthenticationRequest; -import com.arangodb.internal.velocystream.internal.ConnectionPool; import com.arangodb.internal.velocystream.internal.ConnectionSync; import com.arangodb.internal.velocystream.internal.Message; import com.arangodb.internal.velocystream.internal.MessageStore; @@ -64,6 +66,12 @@ public Builder(final HostHandler hostHandler) { this.hostHandler = hostHandler; } + public Builder(final Builder builder) { + this(builder.hostHandler); + timeout(builder.timeout).user(builder.user).password(builder.password).useSsl(builder.useSsl) + .sslContext(builder.sslContext).chunksize(builder.chunksize).maxConnections(builder.maxConnections); + } + public Builder timeout(final Integer timeout) { this.timeout = timeout; return this; @@ -105,26 +113,27 @@ public VstCommunication build( return new VstCommunicationSync(hostHandler, timeout, user, password, useSsl, sslContext, util, collectionCache, chunksize, maxConnections); } + } protected VstCommunicationSync(final HostHandler hostHandler, final Integer timeout, final String user, final String password, final Boolean useSsl, final SSLContext sslContext, final ArangoSerialization util, final CollectionCache collectionCache, final Integer chunksize, final Integer maxConnections) { - super(timeout, user, password, useSsl, sslContext, util, chunksize, - new ConnectionPool(maxConnections) { - private final ConnectionSync.Builder builder = new ConnectionSync.Builder(hostHandler, - new MessageStore()).timeout(timeout).useSsl(useSsl).sslContext(sslContext); - - @Override - public ConnectionSync createConnection() { - return builder.build(); - } - }); + super(timeout, user, password, useSsl, sslContext, util, chunksize, new ConnectionPool( + maxConnections != null ? Math.max(1, maxConnections) : ArangoDBConstants.MAX_CONNECTIONS_VST_DEFAULT) { + private final ConnectionSync.Builder builder = new ConnectionSync.Builder(new MessageStore()) + .timeout(timeout).useSsl(useSsl).sslContext(sslContext); + + @Override + public ConnectionSync createConnection(final Host host) { + return builder.hostHandler(new DelHostHandler(hostHandler, host)).build(); + } + }); this.collectionCache = collectionCache; } @Override - public Response execute(final Request request, final ConnectionSync connection) throws ArangoDBException { + protected Response execute(final Request request, final ConnectionSync connection) throws ArangoDBException { connect(connection); try { final Message requestMessage = createMessage(request); @@ -136,7 +145,6 @@ public Response execute(final Request request, final ConnectionSync connection) } catch (final VPackParserException e) { throw new ArangoDBException(e); } - } private Message send(final Message message, final ConnectionSync connection) throws ArangoDBException { diff --git a/src/main/java/com/arangodb/internal/velocystream/VstProtocol.java b/src/main/java/com/arangodb/internal/velocystream/VstProtocol.java index db4c2cdf9..02d9e42fc 100644 --- a/src/main/java/com/arangodb/internal/velocystream/VstProtocol.java +++ b/src/main/java/com/arangodb/internal/velocystream/VstProtocol.java @@ -23,7 +23,8 @@ import java.io.IOException; import com.arangodb.ArangoDBException; -import com.arangodb.internal.CommunicationProtocol; +import com.arangodb.internal.net.CommunicationProtocol; +import com.arangodb.internal.net.HostHandle; import com.arangodb.internal.velocystream.internal.ConnectionSync; import com.arangodb.velocystream.Request; import com.arangodb.velocystream.Response; @@ -42,8 +43,8 @@ public VstProtocol(final VstCommunication communicatio } @Override - public Response execute(final Request request) throws ArangoDBException { - return communication.execute(request); + public Response execute(final Request request, final HostHandle hostHandle) throws ArangoDBException { + return communication.execute(request, hostHandle); } @Override diff --git a/src/main/java/com/arangodb/internal/velocystream/internal/ConnectionPool.java b/src/main/java/com/arangodb/internal/velocystream/internal/ConnectionPool.java deleted file mode 100644 index dde2697a6..000000000 --- a/src/main/java/com/arangodb/internal/velocystream/internal/ConnectionPool.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * DISCLAIMER - * - * Copyright 2016 ArangoDB GmbH, Cologne, Germany - * - * 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. - * - * Copyright holder is ArangoDB GmbH, Cologne, Germany - */ - -package com.arangodb.internal.velocystream.internal; - -import java.util.LinkedList; - -import com.arangodb.internal.ArangoDBConstants; - -/** - * @author Mark Vollmary - * - */ -public abstract class ConnectionPool { - - private final LinkedList connections; - private final int maxConnections; - - public ConnectionPool(final Integer maxConnections) { - super(); - this.maxConnections = maxConnections != null ? Math.max(1, maxConnections) - : ArangoDBConstants.MAX_CONNECTIONS_VST_DEFAULT; - connections = new LinkedList(); - } - - public abstract C createConnection(); - - public synchronized C connection() { - final C c; - if (connections.size() < maxConnections) { - c = createConnection(); - } else { - c = connections.removeFirst(); - } - connections.add(c); - return c; - } - - public void disconnect() { - while (!connections.isEmpty()) { - connections.removeLast().close(); - } - } - -} diff --git a/src/main/java/com/arangodb/internal/velocystream/internal/ConnectionSync.java b/src/main/java/com/arangodb/internal/velocystream/internal/ConnectionSync.java index f6bd9b1c4..f1ecefb8d 100644 --- a/src/main/java/com/arangodb/internal/velocystream/internal/ConnectionSync.java +++ b/src/main/java/com/arangodb/internal/velocystream/internal/ConnectionSync.java @@ -28,28 +28,32 @@ import javax.net.ssl.SSLContext; import com.arangodb.ArangoDBException; -import com.arangodb.internal.HostHandler; +import com.arangodb.internal.net.HostHandler; /** * @author Mark Vollmary * */ -public class ConnectionSync extends Connection { +public class ConnectionSync extends VstConnection { public static class Builder { - private final HostHandler hostHandler; private final MessageStore messageStore; + private HostHandler hostHandler; private Integer timeout; private Boolean useSsl; private SSLContext sslContext; - public Builder(final HostHandler hostHandler, final MessageStore messageStore) { + public Builder(final MessageStore messageStore) { super(); - this.hostHandler = hostHandler; this.messageStore = messageStore; } + public Builder hostHandler(final HostHandler hostHandler) { + this.hostHandler = hostHandler; + return this; + } + public Builder timeout(final Integer timeout) { this.timeout = timeout; return this; diff --git a/src/main/java/com/arangodb/internal/velocystream/internal/Connection.java b/src/main/java/com/arangodb/internal/velocystream/internal/VstConnection.java similarity index 88% rename from src/main/java/com/arangodb/internal/velocystream/internal/Connection.java rename to src/main/java/com/arangodb/internal/velocystream/internal/VstConnection.java index a001937d4..d1b3d4fc5 100644 --- a/src/main/java/com/arangodb/internal/velocystream/internal/Connection.java +++ b/src/main/java/com/arangodb/internal/velocystream/internal/VstConnection.java @@ -44,16 +44,17 @@ import com.arangodb.ArangoDBException; import com.arangodb.internal.ArangoDBConstants; import com.arangodb.internal.Host; -import com.arangodb.internal.HostHandler; +import com.arangodb.internal.net.Connection; +import com.arangodb.internal.net.HostHandler; import com.arangodb.velocypack.VPackSlice; /** * @author Mark Vollmary * */ -public abstract class Connection { +public abstract class VstConnection implements Connection { - private static final Logger LOGGER = LoggerFactory.getLogger(Connection.class); + private static final Logger LOGGER = LoggerFactory.getLogger(VstConnection.class); private static final byte[] PROTOCOL_HEADER = "VST/1.0\r\n\r\n".getBytes(); private ExecutorService executor; @@ -68,7 +69,9 @@ public abstract class Connection { private OutputStream outputStream; private InputStream inputStream; - protected Connection(final HostHandler hostHandler, final Integer timeout, final Boolean useSsl, + private Host host; + + protected VstConnection(final HostHandler hostHandler, final Integer timeout, final Boolean useSsl, final SSLContext sslContext, final MessageStore messageStore) { super(); this.hostHandler = hostHandler; @@ -78,6 +81,11 @@ protected Connection(final HostHandler hostHandler, final Integer timeout, final this.messageStore = messageStore; } + @Override + public Host getHost() { + return host; + } + public boolean isOpen() { return socket != null && socket.isConnected() && !socket.isClosed(); } @@ -86,8 +94,11 @@ public synchronized void open() throws IOException { if (isOpen()) { return; } - Host host = hostHandler.get(); + host = hostHandler.get(); while (true) { + if (host == null) { + throw new ArangoDBException("Was not able to connect to any host"); + } if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Open connection to %s", host)); } @@ -123,7 +134,7 @@ public synchronized void open() throws IOException { } catch (final IOException e) { hostHandler.fail(); final Host failedHost = host; - host = hostHandler.change(); + host = hostHandler.get(); if (host != null) { LOGGER.warn(String.format("Could not connect to %s or SSL Handshake failed. Try connecting to %s", failedHost, host)); @@ -164,16 +175,17 @@ public Void call() throws Exception { }); } + @Override public synchronized void close() { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("Close connection %s", socket)); - } messageStore.clear(); if (executor != null && !executor.isShutdown()) { executor.shutdown(); } - if (socket != null) { + if (socket != null && !socket.isClosed()) { try { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("Close connection %s", socket)); + } socket.close(); } catch (final IOException e) { throw new ArangoDBException(e); @@ -181,6 +193,12 @@ public synchronized void close() { } } + @Override + public synchronized void closeOnError() { + hostHandler.fail(); + close(); + } + private synchronized void sendProtocolHeader() throws IOException { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Send velocystream protocol header to %s", socket)); diff --git a/src/main/java/com/arangodb/velocystream/Response.java b/src/main/java/com/arangodb/velocystream/Response.java index cc134bb20..722b196d6 100644 --- a/src/main/java/com/arangodb/velocystream/Response.java +++ b/src/main/java/com/arangodb/velocystream/Response.java @@ -20,6 +20,9 @@ package com.arangodb.velocystream; +import java.util.HashMap; +import java.util.Map; + import com.arangodb.velocypack.VPackSlice; import com.arangodb.velocypack.annotations.Expose; @@ -32,11 +35,13 @@ public class Response { private int version = 1; private int type = 2; private int responseCode; + private Map meta; @Expose(deserialize = false) private VPackSlice body = null; public Response() { super(); + meta = new HashMap(); } public int getVersion() { @@ -63,6 +68,14 @@ public void setResponseCode(final int responseCode) { this.responseCode = responseCode; } + public Map getMeta() { + return meta; + } + + public void setMeta(final Map meta) { + this.meta = meta; + } + public VPackSlice getBody() { return body; } diff --git a/src/test/java/com/arangodb/internal/DefaultHostHandlerTest.java b/src/test/java/com/arangodb/internal/DefaultHostHandlerTest.java deleted file mode 100644 index 04b2854c3..000000000 --- a/src/test/java/com/arangodb/internal/DefaultHostHandlerTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * DISCLAIMER - * - * Copyright 2016 ArangoDB GmbH, Cologne, Germany - * - * 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. - * - * Copyright holder is ArangoDB GmbH, Cologne, Germany - */ - -package com.arangodb.internal; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.junit.Test; - -/** - * @author Mark Vollmary - * - */ -public class DefaultHostHandlerTest { - - @Test - public void singleHost() { - final Host h1 = new Host("127.0.0.1", 8529); - final List hosts = Collections. singletonList(h1); - final HostHandler hh = new DefaultHostHandler(hosts); - assertThat(hh.get(), is(h1)); - } - - @Test - public void multipleHosts() { - final Host h1 = new Host("127.0.0.1", 8529); - final Host h2 = new Host("127.0.0.2", 8529); - final List hosts = new ArrayList(); - hosts.add(h1); - hosts.add(h2); - - final HostHandler hh = new DefaultHostHandler(hosts); - for (int i = 0; i < 3; i++) { - assertThat(hh.get(), is(h1)); - assertThat(hh.change(), is(h2)); - assertThat(hh.get(), is(h2)); - if (i < 2) { - assertThat(hh.change(), is(h1)); - } else { - assertThat(hh.change(), is(nullValue())); - } - } - } - -} diff --git a/src/test/java/com/arangodb/internal/HostHandlerTest.java b/src/test/java/com/arangodb/internal/HostHandlerTest.java new file mode 100644 index 000000000..9c962629f --- /dev/null +++ b/src/test/java/com/arangodb/internal/HostHandlerTest.java @@ -0,0 +1,145 @@ +/* + * DISCLAIMER + * + * Copyright 2016 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal; + +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import com.arangodb.internal.net.FallbackHostHandler; +import com.arangodb.internal.net.HostHandler; +import com.arangodb.internal.net.HostResolver; +import com.arangodb.internal.net.RandomHostHandler; +import com.arangodb.internal.net.RoundRobinHostHandler; + +/** + * @author Mark Vollmary + * + */ +public class HostHandlerTest { + + private static final Host HOST_0 = new Host("127.0.0.1", 8529); + private static final Host HOST_1 = new Host("127.0.0.2", 8529); + private static final Host HOST_2 = new Host("127.0.0.3", 8529); + + private static final HostResolver SINGLE_HOST = new HostResolver() { + @Override + public List resolve(final boolean initial, final boolean closeConnections) { + return Collections. singletonList(HOST_0); + } + + @Override + public void init(final EndpointResolver resolver) { + } + }; + private static final HostResolver MULTIPLE_HOSTS = new HostResolver() { + @Override + public List resolve(final boolean initial, final boolean closeConnections) { + final ArrayList hosts = new ArrayList(); + hosts.add(HOST_0); + hosts.add(HOST_1); + hosts.add(HOST_2); + return hosts; + } + + @Override + public void init(final EndpointResolver resolver) { + } + }; + + @Test + public void fallbachHostHandlerSingleHost() { + final HostHandler handler = new FallbackHostHandler(SINGLE_HOST); + assertThat(handler.get(), is(HOST_0)); + handler.fail(); + assertThat(handler.get(), is(HOST_0)); + } + + @Test + public void fallbackHostHandlerMultipleHosts() { + final HostHandler handler = new FallbackHostHandler(MULTIPLE_HOSTS); + for (int i = 0; i < 3; i++) { + assertThat(handler.get(), is(HOST_0)); + handler.fail(); + assertThat(handler.get(), is(HOST_1)); + handler.fail(); + assertThat(handler.get(), is(HOST_2)); + if (i < 2) { + handler.fail(); + assertThat(handler.get(), is(HOST_0)); + } else { + handler.fail(); + assertThat(handler.get(), is(nullValue())); + } + } + } + + @Test + public void randomHostHandlerSingleHost() { + final HostHandler handler = new RandomHostHandler(SINGLE_HOST, new FallbackHostHandler(SINGLE_HOST)); + assertThat(handler.get(), is(HOST_0)); + handler.fail(); + assertThat(handler.get(), is(HOST_0)); + } + + @Test + public void randomHostHandlerMultipeHosts() { + final HostHandler handler = new RandomHostHandler(MULTIPLE_HOSTS, new FallbackHostHandler(MULTIPLE_HOSTS)); + final Host pick0 = handler.get(); + assertThat(pick0, anyOf(is(HOST_0), is(HOST_1), is(HOST_2))); + handler.fail(); + assertThat(handler.get(), anyOf(is(HOST_0), is(HOST_1), is(HOST_2))); + handler.success(); + assertThat(handler.get(), is(pick0)); + } + + @Test + public void roundRobinHostHandlerSingleHost() { + final HostHandler handler = new RoundRobinHostHandler(SINGLE_HOST); + assertThat(handler.get(), is(HOST_0)); + handler.fail(); + assertThat(handler.get(), is(HOST_0)); + } + + @Test + public void roundRobinHostHandlerMultipleHosts() { + final HostHandler handler = new RoundRobinHostHandler(MULTIPLE_HOSTS); + final Host pick0 = handler.get(); + assertThat(pick0, anyOf(is(HOST_0), is(HOST_1), is(HOST_2))); + final Host pick1 = handler.get(); + assertThat(pick1, anyOf(is(HOST_0), is(HOST_1), is(HOST_2))); + assertThat(pick1, is(not(pick0))); + final Host pick2 = handler.get(); + assertThat(pick2, anyOf(is(HOST_0), is(HOST_1), is(HOST_2))); + assertThat(pick2, not(anyOf(is(pick0), is(pick1)))); + final Host pick4 = handler.get(); + assertThat(pick4, is(pick0)); + } + +}