diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index d34133e16271d7..187e93f1a513b0 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -280,9 +280,11 @@ labelPRBasedOnFilePath: - extensions/vertx/**/* - extensions/vertx-core/**/* - extensions/vertx-graphql/**/* + - extensions/vertx-redis/**/* - extensions/vertx-http/**/* - extensions/vertx-keycloak/**/* - extensions/vertx-web/**/* - integration-tests/vertx/**/* - integration-tests/vertx-graphql/**/* + - integration-tests/vertx-redis/**/* - integration-tests/vertx-http/**/* diff --git a/.github/workflows/ci-actions.yml b/.github/workflows/ci-actions.yml index c95c4b8a626bbe..abcf1ef097a76f 100644 --- a/.github/workflows/ci-actions.yml +++ b/.github/workflows/ci-actions.yml @@ -34,8 +34,8 @@ on: env: # Workaround testsuite locale issue LANG: en_US.UTF-8 - NATIVE_TEST_MAVEN_OPTS: "-B --settings .github/mvn-settings.xml --fail-at-end -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-native-image:19.3.1-java11 -Dtest-postgresql -Dtest-elasticsearch -Dtest-keycloak -Dtest-amazon-services -Dtest-mysql -Dtest-mariadb -Dmariadb.url='jdbc:mariadb://localhost:3308/hibernate_orm_test' -Dtest-mssql -Dtest-vault -Dtest-neo4j -Dtest-kafka -Dnative-image.xmx=5g -Dnative -Dformat.skip install" - JVM_TEST_MAVEN_OPTS: "-e -B --settings .github/mvn-settings.xml -Dtest-postgresql -Dtest-elasticsearch -Dtest-mysql -Dtest-mariadb -Dmariadb.url='jdbc:mariadb://localhost:3308/hibernate_orm_test' -Dtest-mssql -Dtest-amazon-services -Dtest-vault -Dtest-neo4j -Dtest-kafka -Dtest-keycloak -Dformat.skip" + NATIVE_TEST_MAVEN_OPTS: "-B --settings .github/mvn-settings.xml --fail-at-end -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-native-image:19.3.1-java11 -Dtest-postgresql -Dtest-elasticsearch -Dtest-keycloak -Dtest-amazon-services -Dtest-mysql -Dtest-redis -Dtest-mariadb -Dmariadb.url='jdbc:mariadb://localhost:3308/hibernate_orm_test' -Dtest-mssql -Dtest-vault -Dtest-neo4j -Dtest-kafka -Dnative-image.xmx=5g -Dnative -Dformat.skip install" + JVM_TEST_MAVEN_OPTS: "-e -B --settings .github/mvn-settings.xml -Dtest-postgresql -Dtest-elasticsearch -Dtest-mysql -Dtest-mariadb -Dmariadb.url='jdbc:mariadb://localhost:3308/hibernate_orm_test' -Dtest-mssql -Dtest-amazon-services -Dtest-vault -Dtest-neo4j -Dtest-kafka -Dtest-keycloak -Dtest-redis -Dformat.skip" DB_USER: hibernate_orm_test DB_PASSWORD: hibernate_orm_test DB_NAME: hibernate_orm_test @@ -70,7 +70,7 @@ jobs: - name: Compute cache restore key # Always recompute on a push so that the maven repo doesnt grow indefinitely with old versions run: | - if ${{ github.event_name == 'pull_request' }}; then echo "::set-env name=COMPUTED_RESTORE_KEY::q2maven-"; fi + if ${{ github.event_name == 'pull_request' }}; then echo "::set-env name=COMPUTED_RESTORE_KEY::q2maven-"; fi - name: Cache Maven Repository id: cache-maven uses: n1hility/cache@v2 @@ -158,7 +158,7 @@ jobs: image: neo4j/neo4j-experimental:4.0.0-rc01 env: NEO4J_AUTH: neo4j/secret - NEO4J_dbms_memory_pagecache_size: 10M + NEO4J_dbms_memory_pagecache_size: 10M NEO4J_dbms_memory_heap_initial__size: 10M ports: - 127.0.0.1:7687:7687 @@ -172,7 +172,10 @@ jobs: - 127.0.0.1:8008:4572 - 127.0.0.1:8009:4575 - 127.0.0.1:8010:4576 - + redis: + image: redis:5.0.8-alpine + ports: + - 127.0.0.1:6379:6379 steps: - name: Start mysql shell: bash @@ -209,7 +212,7 @@ jobs: - name: Prepare failure archive (if maven failed) if: failure() shell: bash - run: find . -name '*-reports' -type d | tar -czvf test-reports.tgz -T - + run: find . -name '*-reports' -type d | tar -czvf test-reports.tgz -T - - name: Upload failure Archive (if maven failed) uses: actions/upload-artifact@v1 if: failure() @@ -224,7 +227,7 @@ jobs: timeout-minutes: 120 env: MAVEN_OPTS: -Xmx1408m - + steps: - uses: actions/checkout@v2 - name: Set up JDK 11 @@ -275,7 +278,7 @@ jobs: image: neo4j/neo4j-experimental:4.0.0-rc01 env: NEO4J_AUTH: neo4j/secret - NEO4J_dbms_memory_pagecache_size: 10M + NEO4J_dbms_memory_pagecache_size: 10M NEO4J_dbms_memory_heap_initial__size: 10M ports: - 7687:7687 @@ -319,14 +322,14 @@ jobs: - name: Prepare failure archive (if maven failed) if: failure() shell: bash - run: find . -name '*-reports' -type d | tar -czvf test-reports.tgz -T - + run: find . -name '*-reports' -type d | tar -czvf test-reports.tgz -T - - name: Upload failure Archive (if maven failed) uses: actions/upload-artifact@v1 if: failure() with: name: test-reports-tcks path: 'test-reports.tgz' - + native-tests: name: Native Tests - ${{matrix.category}} needs: build-jdk11 @@ -373,10 +376,12 @@ jobs: panache-rest-hibernate-orm - category: Data4 neo4j: "true" - timeout: 30 + redis: "true" + timeout: 45 test-modules: > mongodb-client mongodb-panache + vertx-redis neo4j - category: Data5 timeout: 30 @@ -492,9 +497,9 @@ jobs: # These should be services, but services do not (yet) allow conditional execution - name: Postgres Service run: | - docker run --rm --publish 5432:5432 --name build-postgres \ - -e POSTGRES_USER=$DB_USER -e POSTGRES_PASSWORD=$DB_PASSWORD -e POSTGRES_DB=$DB_NAME \ - -d postgres:10.5 + docker run --rm --publish 5432:5432 --name build-postgres \ + -e POSTGRES_USER=$DB_USER -e POSTGRES_PASSWORD=$DB_PASSWORD -e POSTGRES_DB=$DB_NAME \ + -d postgres:10.5 if: matrix.postgres - name: MySQL Service run: | @@ -526,6 +531,9 @@ jobs: -e NEO4J_AUTH=neo4j/secret -e NEO4J_dbms_memory_pagecache_size=10M -e NEO4J_dbms_memory_heap_initial__size=10M \ -d neo4j/neo4j-experimental:4.0.0-rc01 if: matrix.neo4j + - name: Redis Service + run: docker run --rm --publish 6379:6379 --name build-redis -d redis:5.0.8-alpine + if: matrix.redis - name: Keycloak Service run: | docker run --rm --publish 8180:8080 --publish 8543:8443 --name build-keycloak \ @@ -552,7 +560,7 @@ jobs: shell: bash run: tar -xzvf maven-repo.tgz -C ~ - name: Build with Maven - env: + env: TEST_MODULES: ${{matrix.test-modules}} CATEGORY: ${{matrix.category}} run: | @@ -569,7 +577,7 @@ jobs: - name: Prepare failure archive (if maven failed) if: failure() shell: bash - run: find . -name '*-reports' -type d | tar -czvf test-reports.tgz -T - + run: find . -name '*-reports' -type d | tar -czvf test-reports.tgz -T - - name: Upload failure Archive (if maven failed) uses: actions/upload-artifact@v1 if: failure() diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml index ca6883a398aa42..406521bd9b7f93 100644 --- a/bom/deployment/pom.xml +++ b/bom/deployment/pom.xml @@ -753,6 +753,11 @@ quarkus-jsch-deployment ${project.version} + + io.quarkus + quarkus-vertx-redis-deployment + ${project.version} + io.quarkus quarkus-vault-deployment diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 01028bd349325f..15bfc9004f7c95 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -2586,6 +2586,11 @@ smallrye-mutiny-vertx-mail-client ${mutiny-client.version} + + io.smallrye.reactive + smallrye-mutiny-vertx-redis-client + ${mutiny-client.version} + io.vertx vertx-rx-java2 @@ -3229,6 +3234,12 @@ ${project.version} + + io.quarkus + quarkus-vertx-redis + ${project.version} + + io.quarkus.qute diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java index 4e701efa50068c..410cc718ead1b8 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java @@ -110,6 +110,7 @@ public final class FeatureBuildItem extends MultiBuildItem { public static final String VERTX = "vertx"; public static final String VERTX_WEB = "vertx-web"; public static final String VERTX_GRAPHQL = "vertx-graphql"; + public static final String VERTX_REDIS = "vertx-redis"; private final String info; diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc index c51934534119ae..493608f30149cd 100644 --- a/docs/src/main/asciidoc/index.adoc +++ b/docs/src/main/asciidoc/index.adoc @@ -62,6 +62,7 @@ include::quarkus-intro.adoc[tag=intro] * link:camel.html[Apache Camel] * link:command-mode-reference.html[Command Mode Applications] * link:grpc.html[Using gRPC] +* link:redis.html[Connecting to Redis] * link:faq.html[FAQs] diff --git a/docs/src/main/asciidoc/redis.adoc b/docs/src/main/asciidoc/redis.adoc new file mode 100644 index 00000000000000..716763512264aa --- /dev/null +++ b/docs/src/main/asciidoc/redis.adoc @@ -0,0 +1,530 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Using Vertx Redis extension +include::./attributes.adoc[] + +This guide demonstrates how your Quarkus application can use the Redis extension. + +[NOTE] +==== +This extension is considered `preview`. +API or configuration properties might change as the extension matures. +Feedback is welcome on our https://groups.google.com/d/forum/quarkus-dev[mailing list] or as issues in our https://github.com/quarkusio/quarkus/issues[GitHub issue tracker]. +==== + +== Prerequisites + +To complete this guide, you need: + +* less than 15 minutes +* an IDE +* JDK 1.8+ installed with `JAVA_HOME` configured appropriately +* Apache Maven 3.5.3+ +* A running Redis server, or Docker Compose to start one +* GraalVM installed if you want to run in native mode. + +== Architecture + +In this guide, we are going to expose a simple Rest API to increment numbers by using the https://redis.io/commands/incrby[`INCRBY`] command. +Along the way, we'll see how to use other Redis commands like `GET`, `SET`, `DEL` and `KEYS`. + +== Solution + +We recommend that you follow the instructions in the next sections and create the application step by step. +However, you can go right to the completed example. + +Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. + +The solution is located in the `redis-quickstart` {quickstarts-tree-url}/redis-quickstart[directory]. + +== Creating the Maven Project + +First, we need a new project. Create a new project with the following command: + +[source, subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=redis-quickstart \ + -Dextensions="vertx-redis, resteasy-jsonb, resteasy-mutiny" +cd redis-quickstart +---- + +This command generates a Maven project, importing the Redis extension. + +== Starting the Redis server + +Then, we need to start a Redis instance (if you do not have one already) using the following command: + +[source, bash] +---- +docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name redis_quarkus_test -p 6379:6379 redis:5.0.6 +---- + +== Configuring Redis properties + +Once we have the Redis server running, we need to configure the Redis connection properties. +This is done in the `application.properties` configuration file. Edit it to the following content: + +[source] +---- +quarkus.redis.hosts=localhost:6379 <1> +---- + +1. Configure Redis hosts to connect to. Here we connect to the Redis server we started in the previous section + +== Creating the Increment POJO + +We are going to model our increments using the `Increment` POJO. +Create the `src/main/java/org/acme/redis/Increment.java` file, with the following content: + +[source, java] +---- +package org.acme.redis; + +public class Increment { + public String key; <1> + public int value; <2> + + public Increment(String key, int value) { + this.key = key; + this.value = value; + } + + public Increment() { + } +} +---- + +1. The key that will be used as the Redis key +2. The value held by the Redis key + + +== Creating the Increment Service + +We are going to create an `IncrementService` class which will play the role of a Redis client. +With this class, we'll be able to do the perform the `SET`, `GET` , `DELET`, `KEYS` and `INCRBY` Redis commands. + +Create the `src/main/java/org/acme/redis/IncrementService.java` file, with the following content: + +[source, java] +---- +package org.acme.redis; + +import io.quarkus.vertx.redis.SyncRedisAPI; +import io.smallrye.mutiny.Uni; + +import io.vertx.mutiny.redis.client.Response; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +class IncrementService { + + @Inject + SyncRedisAPI syncRedisAPI; <1> + + @Inject + io.vertx.mutiny.redis.client.RedisAPI mutinyRedisAPI; <2> + + Uni del(String key) { + return mutinyRedisAPI.del(Arrays.asList(key)) + .map(response -> null); + } + + String get(String key) { + return syncRedisAPI.get(key).toString(); + } + + void set(String key, Integer value) { + syncRedisAPI.set(Arrays.asList(key, value.toString())); + } + + void increment(String key, Integer incrementBy) { + syncRedisAPI.incrby(key, incrementBy.toString()); + } + + Uni> keys() { + return mutinyRedisAPI + .keys("*") + .map(response -> { + List result = new ArrayList<>(); + for (Response r : response) { + result.add(r.toString()); + } + return result; + }); + } +} +---- + +1. Inject the Redis synchronous client +2. Inject the mutiny reactive redis client + +== Creating the Increment Resource + +Create the `src/main/java/org/acme/redis/IncrementResource.java` file, with the following content: + +[source, java] +---- +package org.acme.redis; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.PathParam; +import javax.ws.rs.PUT; +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.Path; +import javax.ws.rs.POST; +import javax.ws.rs.DELETE; +import javax.ws.rs.core.MediaType; +import java.util.List; + +import io.smallrye.mutiny.Uni; + +@Path("/increments") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class IncrementResource { + + @Inject + IncrementService service; + + @GET + public Uni> keys() { + return service.keys(); + } + + @POST + public Increment create(Increment increment) { + service.set(increment.key, increment.value); + return increment; + } + + @GET + @Path("/{key}") + public Increment get(@PathParam("key") String key) { + return new Increment(key, Integer.valueOf(service.get(key))); + } + + @PUT + @Path("/{key}") + public void increment(@PathParam("key") String key, Integer value) { + service.increment(key, value); + } + + @DELETE + @Path("/{key}") + public Uni delete(@PathParam("key") String key) { + return service.del(key); + } +} +---- + +== Modifying the test class + +Edit the `src/test/java/org/acme/redis/IncrementResourceTest.java` file to the following content: + +[source, java] +---- +package org.acme.redis; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +import static io.restassured.RestAssured.given; + +import io.restassured.http.ContentType; + +@QuarkusTest +public class IncrementResourceTest { + + @Test + public void testRedisOperations() { + // verify that we have nothing + given() + .accept(ContentType.JSON) + .when() + .get("/increments") + .then() + .statusCode(200) + .body("size()", is(0)); + + // create a first increment key with an initial value of 0 + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"key\":\"first-key\",\"value\":0}") + .when() + .post("/increments") + .then() + .statusCode(200) + .body("key", is("first-key")) + .body("value", is(0)); + + // create a second increment key with an initial value of 10 + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"key\":\"second-key\",\"value\":10}") + .when() + .post("/increments") + .then() + .statusCode(200) + .body("key", is("second-key")) + .body("value", is(10)); + + // increment first key by 1 + given() + .contentType(ContentType.JSON) + .body("1") + .when() + .put("/increments/first-key") + .then() + .statusCode(204); + + // verify that key has been incremented + given() + .accept(ContentType.JSON) + .when() + .get("/increments/first-key") + .then() + .statusCode(200) + .body("key", is("first-key")) + .body("value", is(1)); + + // increment second key by 1000 + given() + .contentType(ContentType.JSON) + .body("1000") + .when() + .put("/increments/second-key") + .then() + .statusCode(204); + + // verify that key has been incremented + given() + .accept(ContentType.JSON) + .when() + .get("/increments/second-key") + .then() + .statusCode(200) + .body("key", is("second-key")) + .body("value", is(1010)); + + // verify that we have two keys in registered + given() + .accept(ContentType.JSON) + .when() + .get("/increments") + .then() + .statusCode(200) + .body("size()", is(2)); + + // delete first key + given() + .accept(ContentType.JSON) + .when() + .delete("/increments/first-key") + .then() + .statusCode(204); + + // verify that we have one key left after deletion + given() + .accept(ContentType.JSON) + .when() + .get("/increments") + .then() + .statusCode(200) + .body("size()", is(1)); + + // delete second key + given() + .accept(ContentType.JSON) + .when() + .delete("/increments/second-key") + .then() + .statusCode(204); + + // verify that there is no key left + given() + .accept(ContentType.JSON) + .when() + .get("/increments") + .then() + .statusCode(200) + .body("size()", is(0)); + } +} +---- + +== Get it running + +If you followed the instructions, you should have the Redis server running. +Then, you just need to run the application using: + +[source, shell] +---- +./mvnw compile quarkus:dev +---- + +Open another terminal and run the `curl http://localhost:8080/increments` command. + +== Interacting with the application +As we have seen above, the API exposes five Rest endpoints. +In this section we are going to see how to initialise an increment, see the list of current increments, +incrementing a value given its key, retrieving the current value of an increment, and finally deleting +a key. + +=== Creating a new increment + +[source, shell] +---- +curl -X POST -H "Content-Type: application/json" -d '{"key":"first","value":10}' http://localhost:8080/increments <1> +---- + +1. We create the first increment, with the key `first` and an initial value of `10`. + +Running the above command should return the result below: + +[source, json] +----- +{ + "key": "first", + "value": 10 +} +----- + +=== See current increments keys + +To see the list of current increments keys, run the following command: + +[source, shell] +---- +curl http://localhost:8080/increments +---- + +The above command should return `["first"]` indicating that we have only one increment thus far. + +=== Retrieve an new increment + +To retrieve an increment using its key, we will have to run the below command: + +[source, shell] +---- +curl http://localhost:8080/increments/first <1> +---- + +1. Running this command, should return the following result: + +[source, json] +---- +{ + "key": "first", + "value": 10 +} +---- + +=== Increment a value given its key + +To increment a value, run the following command: + +[source, shell] +---- +curl -X PUT -H "Content-Type: application/json" -d '27' http://localhost:8080/increments/first <1> +---- + +1. Increment the `first` value by 27. + +Now, running the command `curl http://localhost:8080/increments/first` should return the following result: + +[source, json] +---- +{ + "key": "first", + "value": 37 <1> +} +---- + +1. We see that the value of the `first` key is now `37` which is exactly the result of `10 + 27`, quick maths. + +=== Deleting a key + +Use the command below, to delete an increment given its key. + +[source, shell] +---- +curl -X DELETE http://localhost:8080/increments/first <1> +---- + +1. Delete the `first` increment. + +Now, running the command `curl http://localhost:8080/increments` should return an empty list `[]` + +== Packaging and run in JVM mode + +You can run the application as a conventional jar file. + +First, we will need to package it: + +[source, shell] +---- +./mvnw package +---- + +NOTE: This command will start a Redis instance to execute the tests. Thus your Redis containers need to be stopped. + +Then run it: + +[source, shell] +---- +java -jar ./target/redis-quickstart-1.0-SNAPSHOT-runner.jar +---- + +== Running Native + +You can also create a native executable from this application without making any +source code changes. A native executable removes the dependency on the JVM: +everything needed to run the application on the target platform is included in +the executable, allowing the application to run with minimal resource overhead. + +Compiling a native executable takes a bit longer, as GraalVM performs additional +steps to remove unnecessary codepaths. Use the `native` profile to compile a +native executable: + +[source, shell] +---- +./mvnw package -Pnative +---- + +Once the build is finished, you can run the executable with: + +[source, shell] +---- +./target/redis-quickstart-1.0-SNAPSHOT-runner +---- + +== Connection Health Check + +If you are using the `quarkus-smallrye-health` extension, `quarkus-vertx-redis` will automatically add a readiness health check +to validate the connection to the Redis server. + +So when you access the `/health/ready` endpoint of your application you will have information about the connection validation status. + +This behavior can be disabled by setting the `quarkus.redis.health.enabled` property to `false` in your `application.properties`. + +== Configuration Reference + +include::{generated-dir}/config/quarkus-vertx-redis.adoc[opts=optional, leveloffset=+1] diff --git a/extensions/pom.xml b/extensions/pom.xml index 9b1457d27762f4..bf8f2d00095fde 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -74,6 +74,7 @@ reactive-mysql-client mailer grpc + vertx-redis narayana-jta diff --git a/extensions/vertx-redis/deployment/pom.xml b/extensions/vertx-redis/deployment/pom.xml new file mode 100644 index 00000000000000..d14f04337d6f94 --- /dev/null +++ b/extensions/vertx-redis/deployment/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + io.quarkus + quarkus-vertx-redis-parent + 999-SNAPSHOT + ../ + + + quarkus-vertx-redis-deployment + + Quarkus - Vertx Redis - Deployment + + + + io.quarkus + quarkus-vertx-deployment + + + io.quarkus + quarkus-vertx-redis + + + io.quarkus + quarkus-smallrye-health-spi + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/vertx-redis/deployment/src/main/java/io/quarkus/vertx/redis/deployment/RedisBuildTimeConfig.java b/extensions/vertx-redis/deployment/src/main/java/io/quarkus/vertx/redis/deployment/RedisBuildTimeConfig.java new file mode 100644 index 00000000000000..89ea6a71764848 --- /dev/null +++ b/extensions/vertx-redis/deployment/src/main/java/io/quarkus/vertx/redis/deployment/RedisBuildTimeConfig.java @@ -0,0 +1,13 @@ +package io.quarkus.vertx.redis.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot +public class RedisBuildTimeConfig { + /** + * Whether or not an health check is published in case the smallrye-health extension is present. + */ + @ConfigItem(name = "health.enabled", defaultValue = "true") + public boolean healthEnabled; +} diff --git a/extensions/vertx-redis/deployment/src/main/java/io/quarkus/vertx/redis/deployment/VertxRedisProcessor.java b/extensions/vertx-redis/deployment/src/main/java/io/quarkus/vertx/redis/deployment/VertxRedisProcessor.java new file mode 100644 index 00000000000000..9fb7a671822806 --- /dev/null +++ b/extensions/vertx-redis/deployment/src/main/java/io/quarkus/vertx/redis/deployment/VertxRedisProcessor.java @@ -0,0 +1,35 @@ +package io.quarkus.vertx.redis.deployment; + +import static io.quarkus.deployment.builditem.FeatureBuildItem.VERTX_REDIS; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; +import io.quarkus.vertx.redis.runtime.RedisAPIProducer; +import io.vertx.redis.client.impl.types.BulkType; + +public class VertxRedisProcessor { + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(VERTX_REDIS); + } + + @BuildStep + AdditionalBeanBuildItem registerAPIsProducer() { + return AdditionalBeanBuildItem.unremovableOf(RedisAPIProducer.class); + } + + @BuildStep + HealthBuildItem addHealthCheck(RedisBuildTimeConfig buildTimeConfig) { + return new HealthBuildItem("io.quarkus.vertx.redis.health.RedisHealthCheck", + buildTimeConfig.healthEnabled, "redis"); + } + + @BuildStep + RuntimeInitializedClassBuildItem initializeBulkTypeDuringRuntime() { + return new RuntimeInitializedClassBuildItem(BulkType.class.getName()); + } +} diff --git a/extensions/vertx-redis/pom.xml b/extensions/vertx-redis/pom.xml new file mode 100644 index 00000000000000..43fe8f2914648c --- /dev/null +++ b/extensions/vertx-redis/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + + quarkus-vertx-redis-parent + pom + + Quarkus - Vertx Redis + + + deployment + runtime + + + + diff --git a/extensions/vertx-redis/runtime/pom.xml b/extensions/vertx-redis/runtime/pom.xml new file mode 100644 index 00000000000000..e3415ea8e6e809 --- /dev/null +++ b/extensions/vertx-redis/runtime/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + io.quarkus + quarkus-vertx-redis-parent + 999-SNAPSHOT + ../ + + + quarkus-vertx-redis + + Quarkus - Vertx Redis - Runtime + Synchronous and Reactive Redis client + + + io.quarkus + quarkus-vertx + + + io.smallrye.reactive + smallrye-mutiny-vertx-redis-client + + + + io.quarkus + quarkus-smallrye-health + true + + + io.quarkus + quarkus-junit5-internal + test + + + org.assertj + assertj-core + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/SyncRedisAPI.java b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/SyncRedisAPI.java new file mode 100644 index 00000000000000..254b06364f06b2 --- /dev/null +++ b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/SyncRedisAPI.java @@ -0,0 +1,417 @@ +package io.quarkus.vertx.redis; + +import java.util.List; + +import io.vertx.redis.client.Response; + +/** + * A synchronous RedisAPI offering blocking Redis commands. + * The commands have a default timeout of 10 seconds which can be configured + * via {@code quarkus.redis.timeout} configuration knob. + * + * For more information about how each individual command visit + * the Redis Commands Page + */ +public interface SyncRedisAPI { + void close(); + + Response append(String arg0, String arg1); + + Response asking(); + + Response auth(String arg0); + + Response bgrewriteaof(); + + Response bgsave(List args); + + Response bitcount(List args); + + Response bitfield(List args); + + Response bitop(List args); + + Response bitpos(List args); + + Response blpop(List args); + + Response brpop(List args); + + Response brpoplpush(String arg0, String arg1, String arg2); + + Response bzpopmax(List args); + + Response bzpopmin(List args); + + Response client(List args); + + Response cluster(List args); + + Response command(); + + Response config(List args); + + Response dbsize(); + + Response debug(List args); + + Response decr(String arg0); + + Response decrby(String arg0, String arg1); + + Response del(List args); + + Response discard(); + + Response dump(String arg0); + + Response echo(String arg0); + + Response eval(List args); + + Response evalsha(List args); + + Response exec(); + + Response exists(List args); + + Response expire(String arg0, String arg1); + + Response expireat(String arg0, String arg1); + + Response flushall(List args); + + Response flushdb(List args); + + Response geoadd(List args); + + Response geodist(List args); + + Response geohash(List args); + + Response geopos(List args); + + Response georadius(List args); + + Response georadiusRo(List args); + + Response georadiusbymember(List args); + + Response georadiusbymemberRo(List args); + + Response get(String arg0); + + Response getbit(String arg0, String arg1); + + Response getrange(String arg0, String arg1, String arg2); + + Response getset(String arg0, String arg1); + + Response hdel(List args); + + Response hexists(String arg0, String arg1); + + Response hget(String arg0, String arg1); + + Response hgetall(String arg0); + + Response hincrby(String arg0, String arg1, String arg2); + + Response hincrbyfloat(String arg0, String arg1, String arg2); + + Response hkeys(String arg0); + + Response hlen(String arg0); + + Response hmget(List args); + + Response hmset(List args); + + Response host(List args); + + Response hscan(List args); + + Response hset(List args); + + Response hsetnx(String arg0, String arg1, String arg2); + + Response hstrlen(String arg0, String arg1); + + Response hvals(String arg0); + + Response incr(String arg0); + + Response incrby(String arg0, String arg1); + + Response incrbyfloat(String arg0, String arg1); + + Response info(List args); + + Response keys(String arg0); + + Response lastsave(); + + Response latency(List args); + + Response lindex(String arg0, String arg1); + + Response linsert(String arg0, String arg1, String arg2, String arg3); + + Response llen(String arg0); + + Response lolwut(List args); + + Response lpop(String arg0); + + Response lpush(List args); + + Response lpushx(List args); + + Response lrange(String arg0, String arg1, String arg2); + + Response lrem(String arg0, String arg1, String arg2); + + Response lset(String arg0, String arg1, String arg2); + + Response ltrim(String arg0, String arg1, String arg2); + + Response memory(List args); + + Response mget(List args); + + Response migrate(List args); + + Response module(List args); + + Response monitor(); + + Response move(String arg0, String arg1); + + Response mset(List args); + + Response msetnx(List args); + + Response multi(); + + Response object(List args); + + Response persist(String arg0); + + Response pexpire(String arg0, String arg1); + + Response pexpireat(String arg0, String arg1); + + Response pfadd(List args); + + Response pfcount(List args); + + Response pfdebug(List args); + + Response pfmerge(List args); + + Response pfselftest(); + + Response ping(List args); + + Response post(List args); + + Response psetex(String arg0, String arg1, String arg2); + + Response psubscribe(List args); + + Response psync(String arg0, String arg1); + + Response pttl(String arg0); + + Response publish(String arg0, String arg1); + + Response pubsub(List args); + + Response punsubscribe(List args); + + Response randomkey(); + + Response readonly(); + + Response readwrite(); + + Response rename(String arg0, String arg1); + + Response renamenx(String arg0, String arg1); + + Response replconf(List args); + + Response replicaof(String arg0, String arg1); + + Response restore(List args); + + Response restoreAsking(List args); + + Response role(); + + Response rpop(String arg0); + + Response rpoplpush(String arg0, String arg1); + + Response rpush(List args); + + Response rpushx(List args); + + Response sadd(List args); + + Response save(); + + Response scan(List args); + + Response scard(String arg0); + + Response script(List args); + + Response sdiff(List args); + + Response sdiffstore(List args); + + Response select(String arg0); + + Response set(List args); + + Response setbit(String arg0, String arg1, String arg2); + + Response setex(String arg0, String arg1, String arg2); + + Response setnx(String arg0, String arg1); + + Response setrange(String arg0, String arg1, String arg2); + + Response shutdown(List args); + + Response sinter(List args); + + Response sinterstore(List args); + + Response sismember(String arg0, String arg1); + + Response slaveof(String arg0, String arg1); + + Response slowlog(List args); + + Response smembers(String arg0); + + Response smove(String arg0, String arg1, String arg2); + + Response sort(List args); + + Response spop(List args); + + Response srandmember(List args); + + Response srem(List args); + + Response sscan(List args); + + Response strlen(String arg0); + + Response subscribe(List args); + + Response substr(String arg0, String arg1, String arg2); + + Response sunion(List args); + + Response sunionstore(List args); + + Response swapdb(String arg0, String arg1); + + Response sync(); + + Response time(); + + Response touch(List args); + + Response ttl(String arg0); + + Response type(String arg0); + + Response unlink(List args); + + Response unsubscribe(List args); + + Response unwatch(); + + Response wait(String arg0, String arg1); + + Response watch(List args); + + Response xack(List args); + + Response xadd(List args); + + Response xclaim(List args); + + Response xdel(List args); + + Response xgroup(List args); + + Response xinfo(List args); + + Response xlen(String arg0); + + Response xpending(List args); + + Response xrange(List args); + + Response xread(List args); + + Response xreadgroup(List args); + + Response xrevrange(List args); + + Response xsetid(String arg0, String arg1); + + Response xtrim(List args); + + Response zadd(List args); + + Response zcard(String arg0); + + Response zcount(String arg0, String arg1, String arg2); + + Response zincrby(String arg0, String arg1, String arg2); + + Response zinterstore(List args); + + Response zlexcount(String arg0, String arg1, String arg2); + + Response zpopmax(List args); + + Response zpopmin(List args); + + Response zrange(List args); + + Response zrangebylex(List args); + + Response zrangebyscore(List args); + + Response zrank(String arg0, String arg1); + + Response zrem(List args); + + Response zremrangebylex(String arg0, String arg1, String arg2); + + Response zremrangebyrank(String arg0, String arg1, String arg2); + + Response zremrangebyscore(String arg0, String arg1, String arg2); + + Response zrevrange(List args); + + Response zrevrangebylex(List args); + + Response zrevrangebyscore(List args); + + Response zrevrank(String arg0, String arg1); + + Response zscan(List args); + + Response zscore(String arg0, String arg1); + + Response zunionstore(List args); +} diff --git a/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/health/RedisHealthCheck.java b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/health/RedisHealthCheck.java new file mode 100644 index 00000000000000..ee839901b8e240 --- /dev/null +++ b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/health/RedisHealthCheck.java @@ -0,0 +1,36 @@ +package io.quarkus.vertx.redis.health; + +import java.util.Collections; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Readiness; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.vertx.redis.SyncRedisAPI; + +@Readiness +@ApplicationScoped +class RedisHealthCheck implements HealthCheck { + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder builder = HealthCheckResponse.named("Redis connection health check").up(); + + try (InstanceHandle instanceHandle = Arc.container().instance(SyncRedisAPI.class)) { + if (!instanceHandle.isAvailable()) { + builder.down(); + } else { + SyncRedisAPI redisAPI = instanceHandle.get(); + redisAPI.ping(Collections.emptyList()); + } + } catch (RuntimeException e) { + builder.down(); + } + return builder.build(); + } +} diff --git a/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/runtime/RedisAPIProducer.java b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/runtime/RedisAPIProducer.java new file mode 100644 index 00000000000000..4e41946831005c --- /dev/null +++ b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/runtime/RedisAPIProducer.java @@ -0,0 +1,119 @@ +package io.quarkus.vertx.redis.runtime; + +import java.net.InetSocketAddress; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Singleton; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.vertx.redis.SyncRedisAPI; +import io.vertx.core.Vertx; +import io.vertx.redis.client.Redis; +import io.vertx.redis.client.RedisAPI; +import io.vertx.redis.client.RedisClientType; +import io.vertx.redis.client.RedisOptions; + +@ApplicationScoped +public class RedisAPIProducer { + private static final char AT = '@'; + private static final char COLON = ':'; + private static final char SLASH = '/'; + private static final String REDIS_SCHEME = "redis://"; + private long timeout = 10; + + Redis redisClient; + + RedisAPI redisAPI; + + SyncRedisAPI syncRedisAPI; + + io.vertx.mutiny.redis.client.Redis mutinyRedisClient; + + io.vertx.mutiny.redis.client.RedisAPI mutinyRedisAPI; + + public RedisAPIProducer(RedisConfig config, Vertx vertx) { + RedisOptions options = new RedisOptions(); + options.setType(config.clientType); + + if (RedisClientType.STANDALONE == config.clientType) { + if (config.hosts.isPresent() && config.hosts.get().size() > 1) { + throw new ConfigurationException("Multiple hosts supplied for non clustered configuration"); + } + } + + if (config.hosts.isPresent()) { + Set hosts = config.hosts.get(); + for (InetSocketAddress host : hosts) { + options.addConnectionString(buildRedisUrl(host, config.password, config.database)); + } + } else { + InetSocketAddress defaultRedisAddress = new InetSocketAddress("localhost", 6379); + options.addConnectionString(buildRedisUrl(defaultRedisAddress, config.password, config.database)); + } + + if (config.timeout.isPresent()) { + timeout = config.timeout.get().getSeconds(); + } + + redisClient = Redis.createClient(vertx, options); + redisAPI = RedisAPI.api(redisClient); + mutinyRedisClient = io.vertx.mutiny.redis.client.Redis.newInstance(redisClient); + mutinyRedisAPI = io.vertx.mutiny.redis.client.RedisAPI.api(mutinyRedisClient); + syncRedisAPI = new SyncRedisAPIImpl(mutinyRedisAPI, timeout); + } + + @Produces + @Singleton + Redis redis() { + return redisClient; + } + + @Produces + @Singleton + RedisAPI redisAPI() { + return redisAPI; + } + + @Produces + @Singleton + SyncRedisAPI syncRedisAPI() { + return syncRedisAPI; + } + + @Produces + @Singleton + io.vertx.mutiny.redis.client.Redis mutinyRedisClient() { + return mutinyRedisClient; + } + + @Produces + @Singleton + io.vertx.mutiny.redis.client.RedisAPI mutinyRedisAPI() { + return mutinyRedisAPI; + } + + @PreDestroy + public void close() { + this.redis().close(); + } + + private String buildRedisUrl(InetSocketAddress address, Optional password, int database) { + StringBuilder builder = new StringBuilder(REDIS_SCHEME); + if (password.isPresent()) { + builder.append(password.get()); + builder.append(AT); + } + + builder.append(address.getHostString()); + builder.append(COLON); + builder.append(address.getPort()); + builder.append(SLASH); + builder.append(database); + + return builder.toString(); + } +} diff --git a/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/runtime/RedisConfig.java b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/runtime/RedisConfig.java new file mode 100644 index 00000000000000..855a078ca0720a --- /dev/null +++ b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/runtime/RedisConfig.java @@ -0,0 +1,45 @@ +package io.quarkus.vertx.redis.runtime; + +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.Optional; +import java.util.Set; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.vertx.redis.client.RedisClientType; + +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public class RedisConfig { + + /** + * The redis password + */ + @ConfigItem + public Optional password; + + /** + * The redis hosts + */ + @ConfigItem(defaultValue = "localhost:6379") + public Optional> hosts; + + /** + * The redis database + */ + @ConfigItem + public int database; + + /** + * The maximum delay to wait before a blocking command to redis server times out + */ + @ConfigItem(defaultValue = "10") + public Optional timeout; + + /** + * The redis client type + */ + @ConfigItem(defaultValue = "standalone") + public RedisClientType clientType; +} diff --git a/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/runtime/SyncRedisAPIImpl.java b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/runtime/SyncRedisAPIImpl.java new file mode 100644 index 00000000000000..06e248562f2c39 --- /dev/null +++ b/extensions/vertx-redis/runtime/src/main/java/io/quarkus/vertx/redis/runtime/SyncRedisAPIImpl.java @@ -0,0 +1,1039 @@ +package io.quarkus.vertx.redis.runtime; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import io.quarkus.vertx.redis.SyncRedisAPI; +import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.redis.client.RedisAPI; +import io.vertx.redis.client.Response; + +class SyncRedisAPIImpl implements SyncRedisAPI { + + private final RedisAPI redisAPI; + private final long timeout; + + public SyncRedisAPIImpl(RedisAPI redisAPI, long timeout) { + this.redisAPI = redisAPI; + this.timeout = timeout; + } + + @Override + public void close() { + redisAPI.close(); + } + + @Override + public Response append(String arg0, String arg1) { + return await(redisAPI.append(arg0, arg1)); + } + + @Override + public Response asking() { + return await(redisAPI.asking()); + } + + @Override + public Response auth(String arg0) { + return await(redisAPI.auth(arg0)); + } + + @Override + public Response bgrewriteaof() { + return await(redisAPI.bgrewriteaof()); + } + + @Override + public Response bgsave(List args) { + return await(redisAPI.bgsave(args)); + } + + @Override + public Response bitcount(List args) { + return await(redisAPI.bitcount(args)); + } + + @Override + public Response bitfield(List args) { + return await(redisAPI.bitfield(args)); + } + + @Override + public Response bitop(List args) { + return await(redisAPI.bitop(args)); + } + + @Override + public Response bitpos(List args) { + return await(redisAPI.bitpos(args)); + } + + @Override + public Response blpop(List args) { + return await(redisAPI.blpop(args)); + } + + @Override + public Response brpop(List args) { + return await(redisAPI.brpop(args)); + } + + @Override + public Response brpoplpush(String arg0, String arg1, String arg2) { + return await(redisAPI.brpoplpush(arg0, arg1, arg2)); + } + + @Override + public Response bzpopmax(List args) { + return await(redisAPI.bzpopmax(args)); + } + + @Override + public Response bzpopmin(List args) { + return await(redisAPI.bzpopmin(args)); + } + + @Override + public Response client(List args) { + return await(redisAPI.client(args)); + } + + @Override + public Response cluster(List args) { + return await(redisAPI.cluster(args)); + } + + @Override + public Response command() { + return await(redisAPI.command()); + } + + @Override + public Response config(List args) { + return await(redisAPI.config(args)); + } + + @Override + public Response dbsize() { + return await(redisAPI.dbsize()); + } + + @Override + public Response debug(List args) { + return await(redisAPI.debug(args)); + } + + @Override + public Response decr(String arg0) { + return await(redisAPI.decr(arg0)); + } + + @Override + public Response decrby(String arg0, String arg1) { + return await(redisAPI.decrby(arg0, arg1)); + } + + @Override + public Response del(List args) { + return await(redisAPI.del(args)); + } + + @Override + public Response discard() { + return await(redisAPI.discard()); + } + + @Override + public Response dump(String arg0) { + return await(redisAPI.dump(arg0)); + } + + @Override + public Response echo(String arg0) { + return await(redisAPI.echo(arg0)); + } + + @Override + public Response eval(List args) { + return await(redisAPI.eval(args)); + } + + @Override + public Response evalsha(List args) { + return await(redisAPI.evalsha(args)); + } + + @Override + public Response exec() { + return await(redisAPI.exec()); + } + + @Override + public Response exists(List args) { + return await(redisAPI.exists(args)); + } + + @Override + public Response expire(String arg0, String arg1) { + return await(redisAPI.expire(arg0, arg1)); + } + + @Override + public Response expireat(String arg0, String arg1) { + return await(redisAPI.expireat(arg0, arg1)); + } + + @Override + public Response flushall(List args) { + return await(redisAPI.flushall(args)); + } + + @Override + public Response flushdb(List args) { + return await(redisAPI.flushdb(args)); + } + + @Override + public Response geoadd(List args) { + return await(redisAPI.geoadd(args)); + } + + @Override + public Response geodist(List args) { + return await(redisAPI.geodist(args)); + } + + @Override + public Response geohash(List args) { + return await(redisAPI.geohash(args)); + } + + @Override + public Response geopos(List args) { + return await(redisAPI.geopos(args)); + } + + @Override + public Response georadius(List args) { + return await(redisAPI.georadius(args)); + } + + @Override + public Response georadiusRo(List args) { + return await(redisAPI.georadiusRo(args)); + } + + @Override + public Response georadiusbymember(List args) { + return await(redisAPI.georadiusbymember(args)); + } + + @Override + public Response georadiusbymemberRo(List args) { + return await(redisAPI.georadiusbymemberRo(args)); + } + + @Override + public Response get(String arg0) { + return await(redisAPI.get(arg0)); + } + + @Override + public Response getbit(String arg0, String arg1) { + return await(redisAPI.getbit(arg0, arg1)); + } + + @Override + public Response getrange(String arg0, String arg1, String arg2) { + return await(redisAPI.getrange(arg0, arg1, arg2)); + } + + @Override + public Response getset(String arg0, String arg1) { + return await(redisAPI.getset(arg0, arg1)); + } + + @Override + public Response hdel(List args) { + return await(redisAPI.hdel(args)); + } + + @Override + public Response hexists(String arg0, String arg1) { + return await(redisAPI.hexists(arg0, arg1)); + } + + @Override + public Response hget(String arg0, String arg1) { + return await(redisAPI.hget(arg0, arg1)); + } + + @Override + public Response hgetall(String arg0) { + return await(redisAPI.hgetall(arg0)); + } + + @Override + public Response hincrby(String arg0, String arg1, String arg2) { + return await(redisAPI.hincrby(arg0, arg1, arg2)); + } + + @Override + public Response hincrbyfloat(String arg0, String arg1, String arg2) { + return await(redisAPI.hincrbyfloat(arg0, arg1, arg2)); + } + + @Override + public Response hkeys(String arg0) { + return await(redisAPI.hkeys(arg0)); + } + + @Override + public Response hlen(String arg0) { + return await(redisAPI.hlen(arg0)); + } + + @Override + public Response hmget(List args) { + return await(redisAPI.hmget(args)); + } + + @Override + public Response hmset(List args) { + return await(redisAPI.hmset(args)); + } + + @Override + public Response host(List args) { + return await(redisAPI.host(args)); + } + + @Override + public Response hscan(List args) { + return await(redisAPI.hscan(args)); + } + + @Override + public Response hset(List args) { + return await(redisAPI.hset(args)); + } + + @Override + public Response hsetnx(String arg0, String arg1, String arg2) { + return await(redisAPI.hsetnx(arg0, arg1, arg2)); + } + + @Override + public Response hstrlen(String arg0, String arg1) { + return await(redisAPI.hstrlen(arg0, arg1)); + } + + @Override + public Response hvals(String arg0) { + return await(redisAPI.hvals(arg0)); + } + + @Override + public Response incr(String arg0) { + return await(redisAPI.incr(arg0)); + } + + @Override + public Response incrby(String arg0, String arg1) { + return await(redisAPI.incrby(arg0, arg1)); + } + + @Override + public Response incrbyfloat(String arg0, String arg1) { + return await(redisAPI.incrbyfloat(arg0, arg1)); + } + + @Override + public Response info(List args) { + return await(redisAPI.info(args)); + } + + @Override + public Response keys(String arg0) { + return await(redisAPI.keys(arg0)); + } + + @Override + public Response lastsave() { + return await(redisAPI.lastsave()); + } + + @Override + public Response latency(List args) { + return await(redisAPI.latency(args)); + } + + @Override + public Response lindex(String arg0, String arg1) { + return await(redisAPI.lindex(arg0, arg1)); + } + + @Override + public Response linsert(String arg0, String arg1, String arg2, String arg3) { + return await(redisAPI.linsert(arg0, arg1, arg2, arg3)); + } + + @Override + public Response llen(String arg0) { + return await(redisAPI.llen(arg0)); + } + + @Override + public Response lolwut(List args) { + return await(redisAPI.lolwut(args)); + } + + @Override + public Response lpop(String arg0) { + return await(redisAPI.lpop(arg0)); + } + + @Override + public Response lpush(List args) { + return await(redisAPI.lpush(args)); + } + + @Override + public Response lpushx(List args) { + return await(redisAPI.lpushx(args)); + } + + @Override + public Response lrange(String arg0, String arg1, String arg2) { + return await(redisAPI.lrange(arg0, arg1, arg2)); + } + + @Override + public Response lrem(String arg0, String arg1, String arg2) { + return await(redisAPI.lrem(arg0, arg1, arg2)); + } + + @Override + public Response lset(String arg0, String arg1, String arg2) { + return await(redisAPI.lset(arg0, arg1, arg2)); + } + + @Override + public Response ltrim(String arg0, String arg1, String arg2) { + return await(redisAPI.ltrim(arg0, arg1, arg2)); + } + + @Override + public Response memory(List args) { + return await(redisAPI.memory(args)); + } + + @Override + public Response mget(List args) { + return await(redisAPI.mget(args)); + } + + @Override + public Response migrate(List args) { + return await(redisAPI.migrate(args)); + } + + @Override + public Response module(List args) { + return await(redisAPI.module(args)); + } + + @Override + public Response monitor() { + return await(redisAPI.monitor()); + } + + @Override + public Response move(String arg0, String arg1) { + return await(redisAPI.move(arg0, arg1)); + } + + @Override + public Response mset(List args) { + return await(redisAPI.mset(args)); + } + + @Override + public Response msetnx(List args) { + return await(redisAPI.msetnx(args)); + } + + @Override + public Response multi() { + return await(redisAPI.multi()); + } + + @Override + public Response object(List args) { + return await(redisAPI.object(args)); + } + + @Override + public Response persist(String arg0) { + return await(redisAPI.persist(arg0)); + } + + @Override + public Response pexpire(String arg0, String arg1) { + return await(redisAPI.pexpire(arg0, arg1)); + } + + @Override + public Response pexpireat(String arg0, String arg1) { + return await(redisAPI.pexpireat(arg0, arg1)); + } + + @Override + public Response pfadd(List args) { + return await(redisAPI.pfadd(args)); + } + + @Override + public Response pfcount(List args) { + return await(redisAPI.pfcount(args)); + } + + @Override + public Response pfdebug(List args) { + return await(redisAPI.pfdebug(args)); + } + + @Override + public Response pfmerge(List args) { + return await(redisAPI.pfmerge(args)); + } + + @Override + public Response pfselftest() { + return await(redisAPI.pfselftest()); + } + + @Override + public Response ping(List args) { + return await(redisAPI.ping(args)); + } + + @Override + public Response post(List args) { + return await(redisAPI.post(args)); + } + + @Override + public Response psetex(String arg0, String arg1, String arg2) { + return await(redisAPI.psetex(arg0, arg1, arg2)); + } + + @Override + public Response psubscribe(List args) { + return await(redisAPI.psubscribe(args)); + } + + @Override + public Response psync(String arg0, String arg1) { + return await(redisAPI.psync(arg0, arg1)); + } + + @Override + public Response pttl(String arg0) { + return await(redisAPI.pttl(arg0)); + } + + @Override + public Response publish(String arg0, String arg1) { + return await(redisAPI.publish(arg0, arg1)); + } + + @Override + public Response pubsub(List args) { + return await(redisAPI.pubsub(args)); + } + + @Override + public Response punsubscribe(List args) { + return await(redisAPI.punsubscribe(args)); + } + + @Override + public Response randomkey() { + return await(redisAPI.randomkey()); + } + + @Override + public Response readonly() { + return await(redisAPI.readonly()); + } + + @Override + public Response readwrite() { + return await(redisAPI.readwrite()); + } + + @Override + public Response rename(String arg0, String arg1) { + return await(redisAPI.rename(arg0, arg1)); + } + + @Override + public Response renamenx(String arg0, String arg1) { + return await(redisAPI.renamenx(arg0, arg1)); + } + + @Override + public Response replconf(List args) { + return await(redisAPI.replconf(args)); + } + + @Override + public Response replicaof(String arg0, String arg1) { + return await(redisAPI.replicaof(arg0, arg1)); + } + + @Override + public Response restore(List args) { + return await(redisAPI.restore(args)); + } + + @Override + public Response restoreAsking(List args) { + return await(redisAPI.restoreAsking(args)); + } + + @Override + public Response role() { + return await(redisAPI.role()); + } + + @Override + public Response rpop(String arg0) { + return await(redisAPI.rpop(arg0)); + } + + @Override + public Response rpoplpush(String arg0, String arg1) { + return await(redisAPI.rpoplpush(arg0, arg1)); + } + + @Override + public Response rpush(List args) { + return await(redisAPI.rpush(args)); + } + + @Override + public Response rpushx(List args) { + return await(redisAPI.rpushx(args)); + } + + @Override + public Response sadd(List args) { + return await(redisAPI.sadd(args)); + } + + @Override + public Response save() { + return await(redisAPI.save()); + } + + @Override + public Response scan(List args) { + return await(redisAPI.scan(args)); + } + + @Override + public Response scard(String arg0) { + return await(redisAPI.scard(arg0)); + } + + @Override + public Response script(List args) { + return await(redisAPI.script(args)); + } + + @Override + public Response sdiff(List args) { + return await(redisAPI.sdiff(args)); + } + + @Override + public Response sdiffstore(List args) { + return await(redisAPI.sdiffstore(args)); + } + + @Override + public Response select(String arg0) { + return await(redisAPI.select(arg0)); + } + + @Override + public Response set(List args) { + return await(redisAPI.set(args)); + } + + @Override + public Response setbit(String arg0, String arg1, String arg2) { + return await(redisAPI.setbit(arg0, arg1, arg2)); + } + + @Override + public Response setex(String arg0, String arg1, String arg2) { + return await(redisAPI.setex(arg0, arg1, arg2)); + } + + @Override + public Response setnx(String arg0, String arg1) { + return await(redisAPI.setnx(arg0, arg1)); + } + + @Override + public Response setrange(String arg0, String arg1, String arg2) { + return await(redisAPI.setrange(arg0, arg1, arg2)); + } + + @Override + public Response shutdown(List args) { + return await(redisAPI.shutdown(args)); + } + + @Override + public Response sinter(List args) { + return await(redisAPI.sinter(args)); + } + + @Override + public Response sinterstore(List args) { + return await(redisAPI.sinterstore(args)); + } + + @Override + public Response sismember(String arg0, String arg1) { + return await(redisAPI.sismember(arg0, arg1)); + } + + @Override + public Response slaveof(String arg0, String arg1) { + return await(redisAPI.slaveof(arg0, arg1)); + } + + @Override + public Response slowlog(List args) { + return await(redisAPI.slowlog(args)); + } + + @Override + public Response smembers(String arg0) { + return await(redisAPI.smembers(arg0)); + } + + @Override + public Response smove(String arg0, String arg1, String arg2) { + return await(redisAPI.smove(arg0, arg1, arg2)); + } + + @Override + public Response sort(List args) { + return await(redisAPI.sort(args)); + } + + @Override + public Response spop(List args) { + return await(redisAPI.spop(args)); + } + + @Override + public Response srandmember(List args) { + return await(redisAPI.srandmember(args)); + } + + @Override + public Response srem(List args) { + return await(redisAPI.srem(args)); + } + + @Override + public Response sscan(List args) { + return await(redisAPI.sscan(args)); + } + + @Override + public Response strlen(String arg0) { + return await(redisAPI.strlen(arg0)); + } + + @Override + public Response subscribe(List args) { + return await(redisAPI.subscribe(args)); + } + + @Override + public Response substr(String arg0, String arg1, String arg2) { + return await(redisAPI.substr(arg0, arg1, arg2)); + } + + @Override + public Response sunion(List args) { + return await(redisAPI.sunion(args)); + } + + @Override + public Response sunionstore(List args) { + return await(redisAPI.sunionstore(args)); + } + + @Override + public Response swapdb(String arg0, String arg1) { + return await(redisAPI.swapdb(arg0, arg1)); + } + + @Override + public Response sync() { + return await(redisAPI.sync()); + } + + @Override + public Response time() { + return await(redisAPI.time()); + } + + @Override + public Response touch(List args) { + return await(redisAPI.touch(args)); + } + + @Override + public Response ttl(String arg0) { + return await(redisAPI.ttl(arg0)); + } + + @Override + public Response type(String arg0) { + return await(redisAPI.type(arg0)); + } + + @Override + public Response unlink(List args) { + return await(redisAPI.unlink(args)); + } + + @Override + public Response unsubscribe(List args) { + return await(redisAPI.unsubscribe(args)); + } + + @Override + public Response unwatch() { + return await(redisAPI.unwatch()); + } + + @Override + public Response wait(String arg0, String arg1) { + return await(redisAPI.wait(arg0, arg1)); + } + + @Override + public Response watch(List args) { + return await(redisAPI.watch(args)); + } + + @Override + public Response xack(List args) { + return await(redisAPI.xack(args)); + } + + @Override + public Response xadd(List args) { + return await(redisAPI.xadd(args)); + } + + @Override + public Response xclaim(List args) { + return await(redisAPI.xclaim(args)); + } + + @Override + public Response xdel(List args) { + return await(redisAPI.xdel(args)); + } + + @Override + public Response xgroup(List args) { + return await(redisAPI.xgroup(args)); + } + + @Override + public Response xinfo(List args) { + return await(redisAPI.xinfo(args)); + } + + @Override + public Response xlen(String arg0) { + return await(redisAPI.xlen(arg0)); + } + + @Override + public Response xpending(List args) { + return await(redisAPI.xpending(args)); + } + + @Override + public Response xrange(List args) { + return await(redisAPI.xrange(args)); + } + + @Override + public Response xread(List args) { + return await(redisAPI.xread(args)); + } + + @Override + public Response xreadgroup(List args) { + return await(redisAPI.xreadgroup(args)); + } + + @Override + public Response xrevrange(List args) { + return await(redisAPI.xrevrange(args)); + } + + @Override + public Response xsetid(String arg0, String arg1) { + return await(redisAPI.xsetid(arg0, arg1)); + } + + @Override + public Response xtrim(List args) { + return await(redisAPI.xtrim(args)); + } + + @Override + public Response zadd(List args) { + return await(redisAPI.zadd(args)); + } + + @Override + public Response zcard(String arg0) { + return await(redisAPI.zcard(arg0)); + } + + @Override + public Response zcount(String arg0, String arg1, String arg2) { + return await(redisAPI.zcount(arg0, arg1, arg2)); + } + + @Override + public Response zincrby(String arg0, String arg1, String arg2) { + return await(redisAPI.zincrby(arg0, arg1, arg2)); + } + + @Override + public Response zinterstore(List args) { + return await(redisAPI.zinterstore(args)); + } + + @Override + public Response zlexcount(String arg0, String arg1, String arg2) { + return await(redisAPI.zlexcount(arg0, arg1, arg2)); + } + + @Override + public Response zpopmax(List args) { + return await(redisAPI.zpopmax(args)); + } + + @Override + public Response zpopmin(List args) { + return await(redisAPI.zpopmin(args)); + } + + @Override + public Response zrange(List args) { + return await(redisAPI.zrange(args)); + } + + @Override + public Response zrangebylex(List args) { + return await(redisAPI.zrangebylex(args)); + } + + @Override + public Response zrangebyscore(List args) { + return await(redisAPI.zrangebyscore(args)); + } + + @Override + public Response zrank(String arg0, String arg1) { + return await(redisAPI.zrank(arg0, arg1)); + } + + @Override + public Response zrem(List args) { + return await(redisAPI.zrem(args)); + } + + @Override + public Response zremrangebylex(String arg0, String arg1, String arg2) { + return await(redisAPI.zremrangebylex(arg0, arg1, arg2)); + } + + @Override + public Response zremrangebyrank(String arg0, String arg1, String arg2) { + return await(redisAPI.zremrangebyrank(arg0, arg1, arg2)); + } + + @Override + public Response zremrangebyscore(String arg0, String arg1, String arg2) { + return await(redisAPI.zremrangebyscore(arg0, arg1, arg2)); + } + + @Override + public Response zrevrange(List args) { + return await(redisAPI.zrevrange(args)); + } + + @Override + public Response zrevrangebylex(List args) { + return await(redisAPI.zrevrangebylex(args)); + } + + @Override + public Response zrevrangebyscore(List args) { + return await(redisAPI.zrevrangebyscore(args)); + } + + @Override + public Response zrevrank(String arg0, String arg1) { + return await(redisAPI.zrevrank(arg0, arg1)); + } + + @Override + public Response zscan(List args) { + return await(redisAPI.zscan(args)); + } + + @Override + public Response zscore(String arg0, String arg1) { + return await(redisAPI.zscore(arg0, arg1)); + } + + @Override + public Response zunionstore(List args) { + return await(redisAPI.zunionstore(args)); + } + + private Response await(Uni mutinyResponse) { + try { + return mutinyResponse.subscribeAsCompletionStage() + .toCompletableFuture() + .get(timeout, SECONDS) + .getDelegate(); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } +} diff --git a/extensions/vertx-redis/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/vertx-redis/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..e2653d0d46fc48 --- /dev/null +++ b/extensions/vertx-redis/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,14 @@ +--- +name: "Vertx Redis" +metadata: + keywords: + - "redis" + - "vertx-redis" + - "vertx" + - "vert.x" + - "reactive" + guide: "https://quarkus.io/guides/redis" + categories: + - "persistence" + - "reactive" + status: "preview" diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 16e08046530047..33356ffccb184d 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -101,6 +101,7 @@ smallrye-graphql jpa-without-entity quartz + vertx-redis logging-gelf cache qute diff --git a/integration-tests/vertx-redis/README.md b/integration-tests/vertx-redis/README.md new file mode 100644 index 00000000000000..d943e88df2ca13 --- /dev/null +++ b/integration-tests/vertx-redis/README.md @@ -0,0 +1,21 @@ +# Redis example + +## Running the tests + +By default, the tests of this module are disabled. + +To run, you can run the following command: + +``` +mvn clean install -Dtest-redis +``` + +NB: Tests in this module will attempt a connection to a local Redis listening on the default port. +If you have specific requirements, you can define a specific connection URL with `-Dquarkus.redis.hosts=host:port`. + + +Additionally, you can generate a native image and run the tests for this native image by adding `-Dnative`: + +``` +mvn clean install -Dtest-redis -Dnative +``` diff --git a/integration-tests/vertx-redis/pom.xml b/integration-tests/vertx-redis/pom.xml new file mode 100644 index 00000000000000..f9ec73e94f8bda --- /dev/null +++ b/integration-tests/vertx-redis/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + ../pom.xml + + + redis-integration-test + Quarkus - Redis - Integration Test + + + localhost:6379 + + + + + io.quarkus + quarkus-resteasy-mutiny + + + io.quarkus + quarkus-vertx-redis + + + io.quarkus + quarkus-smallrye-health + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + src/main/resources + true + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + test-redis + + + test-redis + + + + + + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + false + + + + + + + + native-image + + + native + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + io.quarkus + quarkus-maven-plugin + + + native-image + + native-image + + + true + true + ${graalvmHome} + -H:+TraceClassInitialization + + + + + + + + + + diff --git a/integration-tests/vertx-redis/src/main/java/io/quarkus/vertx/redis/it/RedisResource.java b/integration-tests/vertx-redis/src/main/java/io/quarkus/vertx/redis/it/RedisResource.java new file mode 100644 index 00000000000000..2dea2260c64c01 --- /dev/null +++ b/integration-tests/vertx-redis/src/main/java/io/quarkus/vertx/redis/it/RedisResource.java @@ -0,0 +1,55 @@ +package io.quarkus.vertx.redis.it; + +import java.util.Arrays; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import io.quarkus.vertx.redis.SyncRedisAPI; +import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.redis.client.RedisAPI; + +@Path("/quarkus-redis") +@ApplicationScoped +public class RedisResource { + @Inject + SyncRedisAPI syncRedisAPI; + + @Inject + RedisAPI reactiveRedisAPI; + + // synchronous + @GET + @Path("/sync/{key}") + public String getSync(@PathParam("key") String key) { + return syncRedisAPI.get(key).toString(); + } + + @POST + @Path("/sync/{key}") + public void setSync(@PathParam("key") String key, String value) { + this.syncRedisAPI.set(Arrays.asList(key, value)); + } + + // reactive + @GET + @Path("/reactive/{key}") + public Uni getReactive(@PathParam("key") String key) { + return reactiveRedisAPI + .get(key) + .map(response -> response.toString()); + } + + @POST + @Path("/reactive/{key}") + public Uni setReactive(@PathParam("key") String key, String value) { + return this.reactiveRedisAPI + .set(Arrays.asList(key, value)) + .map(response -> null); + } + +} diff --git a/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/HealthCheckIT.java b/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/HealthCheckIT.java new file mode 100644 index 00000000000000..e7fde15660cabd --- /dev/null +++ b/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/HealthCheckIT.java @@ -0,0 +1,7 @@ +package io.quarkus.vertx.redis.it; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class HealthCheckIT extends HealthCheckTest { +} diff --git a/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/HealthCheckTest.java b/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/HealthCheckTest.java new file mode 100644 index 00000000000000..25a6c7767dbf9a --- /dev/null +++ b/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/HealthCheckTest.java @@ -0,0 +1,24 @@ +package io.quarkus.vertx.redis.it; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.containsInAnyOrder; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; + +@QuarkusTest +public class HealthCheckTest { + @Test + public void testHealthCheck() { + RestAssured.when().get("/health").then() + .contentType(ContentType.JSON) + .header("Content-Type", containsString("charset=UTF-8")) + .body("status", is("UP"), + "checks.status", containsInAnyOrder("UP"), + "checks.name", containsInAnyOrder("Redis connection health check")); + } +} diff --git a/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/QuarkusRedisIT.java b/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/QuarkusRedisIT.java new file mode 100644 index 00000000000000..7e3313672d603e --- /dev/null +++ b/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/QuarkusRedisIT.java @@ -0,0 +1,8 @@ +package io.quarkus.vertx.redis.it; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +class QuarkusRedisIT extends QuarkusRedisTest { + +} diff --git a/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/QuarkusRedisTest.java b/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/QuarkusRedisTest.java new file mode 100644 index 00000000000000..2eedc1c7aaa5d8 --- /dev/null +++ b/integration-tests/vertx-redis/src/test/java/io/quarkus/vertx/redis/it/QuarkusRedisTest.java @@ -0,0 +1,50 @@ +package io.quarkus.vertx.redis.it; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +class QuarkusRedisTest { + private static final String SYNC_KEY = "sync-key"; + private static final String SYNC_VALUE = "sync-value"; + + private static final String REACTIVE_KEY = "reactive-key"; + private static final String REACTIVE_VALUE = "reactive-value"; + + @Test + public void sync() { + RestAssured.given() + .body(SYNC_VALUE) + .when() + .post("/quarkus-redis/sync/" + SYNC_KEY) + .then() + .statusCode(204); + + RestAssured.given() + .when() + .get("/quarkus-redis/sync/" + SYNC_KEY) + .then() + .statusCode(200) + .body(CoreMatchers.is(SYNC_VALUE)); + } + + @Test + public void reactive() { + RestAssured.given() + .body(REACTIVE_VALUE) + .when() + .post("/quarkus-redis/reactive/" + REACTIVE_KEY) + .then() + .statusCode(204); + + RestAssured.given() + .when() + .get("/quarkus-redis/reactive/" + REACTIVE_KEY) + .then() + .statusCode(200) + .body(CoreMatchers.is(REACTIVE_VALUE)); + } +}