diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 3d308ab67..03874be61 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -255,12 +255,6 @@ jobs: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v1 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any diff --git a/.github/workflows/resilience.yml b/.github/workflows/resilience.yml new file mode 100644 index 000000000..b3c3edd04 --- /dev/null +++ b/.github/workflows/resilience.yml @@ -0,0 +1,32 @@ +name: Resilience Tests + +on: + workflow_dispatch: + push: + tags: [ v** ] + +jobs: + test: + timeout-minutes: 20 + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v2 + with: + java-version: 19 + distribution: 'adopt' + cache: maven + - name: Start Database + run: ./docker/start_db.sh + - name: Info + run: mvn -version + - name: Start Toxiproxy + working-directory: resilience-tests + run: ./bin/toxiproxy-server-linux-amd64 + - name: Test + run: mvn --no-transfer-progress -am -pl resilience-tests test diff --git a/driver/pom.xml b/driver/pom.xml index f44e0a0f2..d3a035714 100644 --- a/driver/pom.xml +++ b/driver/pom.xml @@ -267,6 +267,27 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + 3.1.0 + + + enforce + + enforce + + + + + + 3.6 + + + + + + diff --git a/pom.xml b/pom.xml index 16ae8611b..e4625ef8e 100644 --- a/pom.xml +++ b/pom.xml @@ -9,6 +9,7 @@ 2016 driver + resilience-tests pom @@ -38,32 +39,6 @@ - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.1.0 - - - enforce - - enforce - - - - - - 3.6 - - - - - - - - - ArangoDB GmbH https://www.arangodb.com diff --git a/resilience-tests/README.md b/resilience-tests/README.md new file mode 100644 index 000000000..4cd121c14 --- /dev/null +++ b/resilience-tests/README.md @@ -0,0 +1,15 @@ +# arangodb-java-driver-resiliency-tests + +## run + +Start (single server) ArangoDB: +```shell +./docker/start_db.sh +``` + +Start [toxiproxy-server](https://github.com/Shopify/toxiproxy) at `127.0.0.1:8474`. + +Run the tests: +```shell + mvn test -am -pl resilience-tests +``` diff --git a/resilience-tests/bin/toxiproxy-server-linux-amd64 b/resilience-tests/bin/toxiproxy-server-linux-amd64 new file mode 100755 index 000000000..8b474e1a6 Binary files /dev/null and b/resilience-tests/bin/toxiproxy-server-linux-amd64 differ diff --git a/resilience-tests/pom.xml b/resilience-tests/pom.xml new file mode 100644 index 000000000..b9dc62299 --- /dev/null +++ b/resilience-tests/pom.xml @@ -0,0 +1,111 @@ + + + + arangodb-java-driver-parent + com.arangodb + 7.0.0-SNAPSHOT + + 4.0.0 + + resilience-tests + + + 19 + 19 + 19 + UTF-8 + + + + + org.mock-server + mockserver-netty + 5.13.2 + + + com.arangodb + arangodb-java-driver + ${version} + + + com.arangodb + jackson-dataformat-velocypack + 3.0.1 + + + eu.rekawek.toxiproxy + toxiproxy-java + 2.1.7 + + + ch.qos.logback + logback-classic + 1.2.11 + + + org.assertj + assertj-core + 3.23.1 + + + org.junit.platform + junit-platform-launcher + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-engine + + + org.junit.jupiter + junit-jupiter-params + + + org.awaitility + awaitility + 4.2.0 + + + + + + + io.netty + netty-bom + 4.1.84.Final + import + pom + + + com.fasterxml.jackson + jackson-bom + 2.13.4 + import + pom + + + org.junit + junit-bom + 5.9.1 + pom + import + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + + + \ No newline at end of file diff --git a/resilience-tests/src/test/java/resilience/ActiveFailoverTest.java b/resilience-tests/src/test/java/resilience/ActiveFailoverTest.java new file mode 100644 index 000000000..4c639c55b --- /dev/null +++ b/resilience-tests/src/test/java/resilience/ActiveFailoverTest.java @@ -0,0 +1,75 @@ +package resilience; + +import ch.qos.logback.classic.Level; +import com.arangodb.ArangoDB; +import com.arangodb.async.ArangoDBAsync; +import resilience.utils.MemoryAppender; +import eu.rekawek.toxiproxy.Proxy; +import eu.rekawek.toxiproxy.ToxiproxyClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; + +import java.io.IOException; +import java.util.List; + +@Tag("activeFailover") +public abstract class ActiveFailoverTest { + + protected static final String HOST = "127.0.0.1"; + protected static final String PASSWORD = "test"; + protected static final MemoryAppender logs = new MemoryAppender(Level.WARN); + private static final List endpoints = List.of( + new Endpoint("activeFailover1", HOST, 18529, "172.28.0.1:8529"), + new Endpoint("activeFailover2", HOST, 18539, "172.28.0.1:8539"), + new Endpoint("activeFailover3", HOST, 18549, "172.28.0.1:8549") + ); + + @BeforeAll + static void beforeAll() throws IOException { + ToxiproxyClient client = new ToxiproxyClient(HOST, 8474); + for (Endpoint ph : endpoints) { + Proxy p = client.getProxyOrNull(ph.getName()); + if (p != null) { + p.delete(); + } + ph.setProxy(client.createProxy(ph.getName(), ph.getHost() + ":" + ph.getPort(), ph.getUpstream())); + } + } + + @AfterAll + static void afterAll() throws IOException { + for (Endpoint ph : endpoints) { + ph.getProxy().delete(); + } + } + + @BeforeEach + void beforeEach() throws IOException { + for (Endpoint ph : endpoints) { + ph.getProxy().enable(); + } + } + + protected static List getEndpoints() { + return endpoints; + } + + protected static ArangoDB.Builder dbBuilder() { + ArangoDB.Builder builder = new ArangoDB.Builder().password(PASSWORD); + for (Endpoint ph : endpoints) { + builder.host(ph.getHost(), ph.getPort()); + } + return builder; + } + + protected static ArangoDBAsync.Builder dbBuilderAsync() { + ArangoDBAsync.Builder builder = new ArangoDBAsync.Builder().password(PASSWORD); + for (Endpoint ph : endpoints) { + builder.host(ph.getHost(), ph.getPort()); + } + return builder; + } + +} diff --git a/resilience-tests/src/test/java/resilience/ClusterTest.java b/resilience-tests/src/test/java/resilience/ClusterTest.java new file mode 100644 index 000000000..5b343bb28 --- /dev/null +++ b/resilience-tests/src/test/java/resilience/ClusterTest.java @@ -0,0 +1,75 @@ +package resilience; + +import ch.qos.logback.classic.Level; +import com.arangodb.ArangoDB; +import com.arangodb.async.ArangoDBAsync; +import resilience.utils.MemoryAppender; +import eu.rekawek.toxiproxy.Proxy; +import eu.rekawek.toxiproxy.ToxiproxyClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; + +import java.io.IOException; +import java.util.List; + +@Tag("cluster") +public abstract class ClusterTest { + + protected static final String HOST = "127.0.0.1"; + protected static final String PASSWORD = "test"; + protected static final MemoryAppender logs = new MemoryAppender(Level.WARN); + private static final List endpoints = List.of( + new Endpoint("cluster1", HOST, 18529, "172.28.0.1:8529"), + new Endpoint("cluster2", HOST, 18539, "172.28.0.1:8539"), + new Endpoint("cluster3", HOST, 18549, "172.28.0.1:8549") + ); + + @BeforeAll + static void beforeAll() throws IOException { + ToxiproxyClient client = new ToxiproxyClient(HOST, 8474); + for (Endpoint ph : endpoints) { + Proxy p = client.getProxyOrNull(ph.getName()); + if (p != null) { + p.delete(); + } + ph.setProxy(client.createProxy(ph.getName(), ph.getHost() + ":" + ph.getPort(), ph.getUpstream())); + } + } + + @AfterAll + static void afterAll() throws IOException { + for (Endpoint ph : endpoints) { + ph.getProxy().delete(); + } + } + + @BeforeEach + void beforeEach() throws IOException { + for (Endpoint ph : endpoints) { + ph.getProxy().enable(); + } + } + + protected static List getEndpoints() { + return endpoints; + } + + protected static ArangoDB.Builder dbBuilder() { + ArangoDB.Builder builder = new ArangoDB.Builder().password(PASSWORD); + for (Endpoint ph : endpoints) { + builder.host(ph.getHost(), ph.getPort()); + } + return builder; + } + + protected static ArangoDBAsync.Builder dbBuilderAsync() { + ArangoDBAsync.Builder builder = new ArangoDBAsync.Builder().password(PASSWORD); + for (Endpoint ph : endpoints) { + builder.host(ph.getHost(), ph.getPort()); + } + return builder; + } + +} diff --git a/resilience-tests/src/test/java/resilience/Endpoint.java b/resilience-tests/src/test/java/resilience/Endpoint.java new file mode 100644 index 000000000..9e8c697d4 --- /dev/null +++ b/resilience-tests/src/test/java/resilience/Endpoint.java @@ -0,0 +1,45 @@ +package resilience; + +import eu.rekawek.toxiproxy.Proxy; + +/** + * class representing a proxied db endpoint + */ +public class Endpoint { + private final String name; + private final String host; + private final int port; + private final String upstream; + private Proxy proxy; + + public Endpoint(String name, String host, int port, String upstream) { + this.name = name; + this.host = host; + this.port = port; + this.upstream = upstream; + } + + public String getName() { + return name; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public String getUpstream() { + return upstream; + } + + public Proxy getProxy() { + return proxy; + } + + public void setProxy(Proxy proxy) { + this.proxy = proxy; + } +} diff --git a/resilience-tests/src/test/java/resilience/SingleServerTest.java b/resilience-tests/src/test/java/resilience/SingleServerTest.java new file mode 100644 index 000000000..6ba01612e --- /dev/null +++ b/resilience-tests/src/test/java/resilience/SingleServerTest.java @@ -0,0 +1,60 @@ +package resilience; + +import ch.qos.logback.classic.Level; +import com.arangodb.ArangoDB; +import com.arangodb.async.ArangoDBAsync; +import resilience.utils.MemoryAppender; +import eu.rekawek.toxiproxy.Proxy; +import eu.rekawek.toxiproxy.ToxiproxyClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; + +import java.io.IOException; + +@Tag("singleServer") +public abstract class SingleServerTest { + + protected static final String HOST = "127.0.0.1"; + protected static final String PASSWORD = "test"; + protected static final MemoryAppender logs = new MemoryAppender(Level.DEBUG); + private static final Endpoint endpoint = new Endpoint("singleServer", HOST, 18529, "172.28.0.1:8529"); + + @BeforeAll + static void beforeAll() throws IOException { + ToxiproxyClient client = new ToxiproxyClient(HOST, 8474); + Proxy p = client.getProxyOrNull(endpoint.getName()); + if (p != null) { + p.delete(); + } + endpoint.setProxy(client.createProxy(endpoint.getName(), HOST + ":" + endpoint.getPort(), endpoint.getUpstream())); + } + + @AfterAll + static void afterAll() throws IOException { + endpoint.getProxy().delete(); + } + + @BeforeEach + void beforeEach() throws IOException { + endpoint.getProxy().enable(); + } + + protected static Endpoint getEndpoint() { + return endpoint; + } + + protected static ArangoDB.Builder dbBuilder() { + return new ArangoDB.Builder() + .host(endpoint.getHost(), endpoint.getPort()) + .password(PASSWORD); + } + + protected static ArangoDBAsync.Builder dbBuilderAsync() { + return new ArangoDBAsync.Builder() + .host(endpoint.getHost(), endpoint.getPort()) + .password(PASSWORD); + } + +} diff --git a/resilience-tests/src/test/java/resilience/connection/ConnectionTest.java b/resilience-tests/src/test/java/resilience/connection/ConnectionTest.java new file mode 100644 index 000000000..6c2453acf --- /dev/null +++ b/resilience-tests/src/test/java/resilience/connection/ConnectionTest.java @@ -0,0 +1,79 @@ +package resilience.connection; + +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDBException; +import com.arangodb.ArangoDBMultipleException; +import com.arangodb.Protocol; +import resilience.SingleServerTest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +/** + * @author Michele Rastelli + */ +class ConnectionTest extends SingleServerTest { + + static Stream protocolProvider() { + return Stream.of( + Protocol.VST, + Protocol.HTTP_VPACK, + Protocol.HTTP2_VPACK + ); + } + + static Stream arangoProvider() { + return Stream.of( + dbBuilder().useProtocol(Protocol.VST).build(), + dbBuilder().useProtocol(Protocol.HTTP_VPACK).build(), + dbBuilder().useProtocol(Protocol.HTTP2_VPACK).build() + ); + } + + @ParameterizedTest + @MethodSource("protocolProvider") + void nameResolutionFailTest(Protocol protocol) { + ArangoDB arangoDB = new ArangoDB.Builder() + .host("wrongHost", 8529) + .useProtocol(protocol) + .build(); + + Throwable thrown = catchThrowable(arangoDB::getVersion); + assertThat(thrown).isInstanceOf(ArangoDBException.class); + assertThat(thrown.getMessage()).contains("Cannot contact any host!"); + assertThat(thrown.getCause()).isNotNull(); + assertThat(thrown.getCause()).isInstanceOf(ArangoDBMultipleException.class); + ((ArangoDBMultipleException) thrown.getCause()).getExceptions().forEach(e -> { + assertThat(e).isInstanceOf(UnknownHostException.class); + assertThat(e.getMessage()).contains("wrongHost"); + }); + arangoDB.shutdown(); + } + + @ParameterizedTest + @MethodSource("arangoProvider") + void connectionFailTest(ArangoDB arangoDB) throws IOException, InterruptedException { + getEndpoint().getProxy().disable(); + Thread.sleep(100); + + Throwable thrown = catchThrowable(arangoDB::getVersion); + assertThat(thrown).isInstanceOf(ArangoDBException.class); + assertThat(thrown.getMessage()).contains("Cannot contact any host"); + assertThat(thrown.getCause()).isNotNull(); + assertThat(thrown.getCause()).isInstanceOf(ArangoDBMultipleException.class); + ((ArangoDBMultipleException) thrown.getCause()).getExceptions().forEach(e -> { + assertThat(e).isInstanceOf(ConnectException.class); + }); + arangoDB.shutdown(); + getEndpoint().getProxy().enable(); + Thread.sleep(100); + } + +} diff --git a/resilience-tests/src/test/java/resilience/http/MockTest.java b/resilience-tests/src/test/java/resilience/http/MockTest.java new file mode 100644 index 000000000..e5eb6fab0 --- /dev/null +++ b/resilience-tests/src/test/java/resilience/http/MockTest.java @@ -0,0 +1,60 @@ +package resilience.http; + +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDBException; +import com.arangodb.Protocol; +import resilience.SingleServerTest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.integration.ClientAndServer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockserver.integration.ClientAndServer.startClientAndServer; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +class MockTest extends SingleServerTest { + + private ClientAndServer mockServer; + private ArangoDB arangoDB; + + @BeforeEach + void before() { + mockServer = startClientAndServer(getEndpoint().getHost(), getEndpoint().getPort()); + arangoDB = new ArangoDB.Builder() + .useProtocol(Protocol.HTTP_JSON) + .password(PASSWORD) + .host("127.0.0.1", mockServer.getPort()) + .build(); + } + + @AfterEach + void after() { + arangoDB.shutdown(); + mockServer.stop(); + } + + @Test + void doTest() { + arangoDB.getVersion(); + + mockServer + .when( + request() + .withMethod("GET") + .withPath("/.*/_api/version") + ) + .respond( + response() + .withStatusCode(503) + .withBody("{\"error\":true,\"errorNum\":503,\"errorMessage\":\"boom\",\"code\":503}") + ); + + Throwable thrown = catchThrowable(arangoDB::getVersion); + assertThat(thrown) + .isInstanceOf(ArangoDBException.class) + .hasMessageContaining("boom"); + } +} diff --git a/resilience-tests/src/test/java/resilience/reconnection/ReconnectionTest.java b/resilience-tests/src/test/java/resilience/reconnection/ReconnectionTest.java new file mode 100644 index 000000000..276e5236e --- /dev/null +++ b/resilience-tests/src/test/java/resilience/reconnection/ReconnectionTest.java @@ -0,0 +1,119 @@ +package resilience.reconnection; + +import ch.qos.logback.classic.Level; +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDBException; +import com.arangodb.ArangoDBMultipleException; +import com.arangodb.Protocol; +import org.junit.jupiter.params.provider.EnumSource; +import resilience.SingleServerTest; +import eu.rekawek.toxiproxy.model.ToxicDirection; +import eu.rekawek.toxiproxy.model.toxic.Latency; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.net.ConnectException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * @author Michele Rastelli + */ +class ReconnectionTest extends SingleServerTest { + + static Stream arangoProvider() { + return Stream.of( + dbBuilder().timeout(1_000).useProtocol(Protocol.VST).build(), + dbBuilder().timeout(1_000).useProtocol(Protocol.HTTP_VPACK).build(), + dbBuilder().timeout(1_000).useProtocol(Protocol.HTTP2_VPACK).build() + ); + } + + /** + * on reconnection failure: + * - 3x logs WARN Could not connect to host[addr=127.0.0.1,port=8529] + * - ArangoDBException("Cannot contact any host") + *

+ * once the proxy is re-enabled: + * - the subsequent requests should be successful + */ + @ParameterizedTest + @MethodSource("arangoProvider") + void unreachableHost(ArangoDB arangoDB) throws IOException, InterruptedException { + arangoDB.getVersion(); + + // close the driver connection + getEndpoint().getProxy().disable(); + Thread.sleep(100); + + for (int i = 0; i < 10; i++) { + Throwable thrown = catchThrowable(arangoDB::getVersion); + assertThat(thrown).isInstanceOf(ArangoDBException.class); + assertThat(thrown.getMessage()).contains("Cannot contact any host"); + assertThat(thrown.getCause()).isNotNull(); + assertThat(thrown.getCause()).isInstanceOf(ArangoDBMultipleException.class); + ((ArangoDBMultipleException) thrown.getCause()).getExceptions().forEach(e -> { + assertThat(e).isInstanceOf(ConnectException.class); + }); + } + + long warnsCount = logs.getLoggedEvents().stream() + .filter(e -> e.getLevel().equals(Level.WARN)) + .filter(e -> e.getMessage().contains("Could not connect to host[addr=127.0.0.1,port=18529]")) + .count(); + assertThat(warnsCount).isGreaterThanOrEqualTo(3); + + getEndpoint().getProxy().enable(); + Thread.sleep(100); + + arangoDB.getVersion(); + arangoDB.shutdown(); + } + + /** + * on reconnection failure: + * - 3x logs WARN Could not connect to host[addr=127.0.0.1,port=8529] + * - ArangoDBException("Cannot contact any host") + *

+ * once the proxy is re-enabled: + * - the subsequent requests should be successful + */ + @ParameterizedTest + @EnumSource(Protocol.class) + void connectionTimeout(Protocol protocol) throws IOException, InterruptedException { + // https://github.com/vert-x3/vertx-web/issues/2296 + // WebClient: HTTP/2 request timeout does not throw TimeoutException + assumeTrue(protocol != Protocol.HTTP2_VPACK); + assumeTrue(protocol != Protocol.HTTP2_JSON); + + ArangoDB arangoDB = dbBuilder() + .timeout(1_000) + .useProtocol(protocol) + .build(); + + arangoDB.getVersion(); + + // slow down the driver connection + Latency toxic = getEndpoint().getProxy().toxics().latency("latency", ToxicDirection.DOWNSTREAM, 10_000); + Thread.sleep(100); + + Throwable thrown = catchThrowable(arangoDB::getVersion); + thrown.printStackTrace(); + assertThat(thrown) + .isInstanceOf(ArangoDBException.class) + .extracting(Throwable::getCause) + .isInstanceOf(TimeoutException.class); + + toxic.remove(); + Thread.sleep(100); + + arangoDB.getVersion(); + arangoDB.shutdown(); + } + +} diff --git a/resilience-tests/src/test/java/resilience/timeout/TimeoutTest.java b/resilience-tests/src/test/java/resilience/timeout/TimeoutTest.java new file mode 100644 index 000000000..079c2e3fd --- /dev/null +++ b/resilience-tests/src/test/java/resilience/timeout/TimeoutTest.java @@ -0,0 +1,73 @@ +package resilience.timeout; + +import com.arangodb.ArangoCollection; +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDBException; +import com.arangodb.Protocol; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import resilience.SingleServerTest; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * @author Michele Rastelli + */ +class TimeoutTest extends SingleServerTest { + + /** + * on timeout failure: + * - throw exception + * - expect operation performed (at most) once + *

+ * after the exception: + * - the subsequent requests should be successful + */ + @ParameterizedTest + @EnumSource(Protocol.class) + void requestTimeout(Protocol protocol) throws InterruptedException { + // https://github.com/vert-x3/vertx-web/issues/2296 + // WebClient: HTTP/2 request timeout does not throw TimeoutException + assumeTrue(protocol != Protocol.HTTP2_VPACK); + assumeTrue(protocol != Protocol.HTTP2_JSON); + + ArangoDB arangoDB = dbBuilder() + .timeout(1_000) + .useProtocol(protocol) + .build(); + + arangoDB.getVersion(); + String colName = "timeoutTest"; + ArangoCollection col = arangoDB.db().collection(colName); + if (!col.exists()) col.create(); + col.truncate(); + + try { + arangoDB.db().query("" + + "INSERT {value:sleep(2)}\n" + + "INTO @@col\n" + + "RETURN NEW\n", + Collections.singletonMap("@col", colName), + Map.class); + } catch (Exception e) { + e.printStackTrace(); + assertThat(e) + .isInstanceOf(ArangoDBException.class) + .extracting(Throwable::getCause) + .isInstanceOf(TimeoutException.class); + } + + arangoDB.getVersion(); + + Thread.sleep(2_000); + assertThat(col.count().getCount()).isEqualTo(1); + + arangoDB.shutdown(); + } + +} diff --git a/resilience-tests/src/test/java/resilience/utils/MemoryAppender.java b/resilience-tests/src/test/java/resilience/utils/MemoryAppender.java new file mode 100644 index 000000000..018dd5415 --- /dev/null +++ b/resilience-tests/src/test/java/resilience/utils/MemoryAppender.java @@ -0,0 +1,30 @@ +package resilience.utils; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; + +public class MemoryAppender extends ListAppender { + + public MemoryAppender(Level level) { + setContext((LoggerContext) LoggerFactory.getILoggerFactory()); + start(); + Logger logger = (Logger) LoggerFactory.getLogger("root"); + logger.setLevel(level); + logger.addAppender(this); + } + + public void reset() { + this.list.clear(); + } + + public List getLoggedEvents() { + return Collections.unmodifiableList(this.list); + } +} \ No newline at end of file diff --git a/resilience-tests/src/test/java/resilience/vstKeepAlive/VstKeepAliveCloseAsyncTest.java b/resilience-tests/src/test/java/resilience/vstKeepAlive/VstKeepAliveCloseAsyncTest.java new file mode 100644 index 000000000..6a751da8d --- /dev/null +++ b/resilience-tests/src/test/java/resilience/vstKeepAlive/VstKeepAliveCloseAsyncTest.java @@ -0,0 +1,54 @@ +package resilience.vstKeepAlive; + +import ch.qos.logback.classic.Level; +import com.arangodb.async.ArangoDBAsync; +import resilience.SingleServerTest; +import eu.rekawek.toxiproxy.model.ToxicDirection; +import eu.rekawek.toxiproxy.model.toxic.Latency; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import static org.awaitility.Awaitility.await; + +/** + * @author Michele Rastelli + */ +class VstKeepAliveCloseAsyncTest extends SingleServerTest { + + private ArangoDBAsync arangoDB; + + @BeforeEach + void init() { + arangoDB = dbBuilderAsync() + .timeout(1000) + .keepAliveInterval(1) + .build(); + } + + @AfterEach + void shutDown() { + arangoDB.shutdown(); + } + + /** + * after 3 consecutive VST keepAlive failures: + * - log ERROR Connection unresponsive + * - reconnect on next request + */ + @Test + @Timeout(10) + void keepAliveCloseAndReconnect() throws IOException, ExecutionException, InterruptedException { + arangoDB.getVersion().get(); + Latency toxic = getEndpoint().getProxy().toxics().latency("latency", ToxicDirection.DOWNSTREAM, 10_000); + await().until(() -> logs.getLoggedEvents().stream() + .filter(e -> e.getLevel().equals(Level.ERROR)) + .anyMatch(e -> e.getMessage().contains("Connection unresponsive!"))); + toxic.setLatency(0L); + arangoDB.getVersion().get(); + } +} diff --git a/resilience-tests/src/test/java/resilience/vstKeepAlive/VstKeepAliveCloseTest.java b/resilience-tests/src/test/java/resilience/vstKeepAlive/VstKeepAliveCloseTest.java new file mode 100644 index 000000000..f55c97862 --- /dev/null +++ b/resilience-tests/src/test/java/resilience/vstKeepAlive/VstKeepAliveCloseTest.java @@ -0,0 +1,55 @@ +package resilience.vstKeepAlive; + +import ch.qos.logback.classic.Level; +import com.arangodb.ArangoDB; +import com.arangodb.Protocol; +import resilience.SingleServerTest; +import eu.rekawek.toxiproxy.model.ToxicDirection; +import eu.rekawek.toxiproxy.model.toxic.Latency; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.io.IOException; + +import static org.awaitility.Awaitility.await; + +/** + * @author Michele Rastelli + */ +class VstKeepAliveCloseTest extends SingleServerTest { + + private ArangoDB arangoDB; + + @BeforeEach + void init() { + arangoDB = dbBuilder() + .useProtocol(Protocol.VST) + .timeout(1000) + .keepAliveInterval(1) + .build(); + } + + @AfterEach + void shutDown() { + arangoDB.shutdown(); + } + + /** + * after 3 consecutive VST keepAlive failures: + * - log ERROR Connection unresponsive + * - reconnect on next request + */ + @Test + @Timeout(10) + void keepAliveCloseAndReconnect() throws IOException { + arangoDB.getVersion(); + Latency toxic = getEndpoint().getProxy().toxics().latency("latency", ToxicDirection.DOWNSTREAM, 10_000); + await().until(() -> logs.getLoggedEvents().stream() + .filter(e -> e.getLevel().equals(Level.ERROR)) + .anyMatch(e -> e.getMessage().contains("Connection unresponsive!"))); + toxic.setLatency(0); + arangoDB.getVersion(); + } +} diff --git a/resilience-tests/src/test/resources/logback-test.xml b/resilience-tests/src/test/resources/logback-test.xml new file mode 100644 index 000000000..840d133d7 --- /dev/null +++ b/resilience-tests/src/test/resources/logback-test.xml @@ -0,0 +1,13 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + +