From 6a544068e744babff355fc2c4a135db8615c6af2 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Fri, 25 Oct 2024 13:56:11 -0400 Subject: [PATCH 1/6] New simple SSE test that shows how to use the API in Helidon SE (#98) * New simple SSE test that shows how to use the API in Helidon SE and also shows how to interact with an SSE endpoint using WebClient. * - Add a simple UI to the WebServer SSE example. * Gracefully handles client connection close before count is reached. Signed-off-by: Santiago Pericas-Geertsen Co-authored-by: Romain Grecourt --- examples/webserver/pom.xml | 1 + examples/webserver/sse/README.md | 16 +++ examples/webserver/sse/pom.xml | 96 ++++++++++++++++ .../helidon/examples/webserver/sse/Main.java | 60 ++++++++++ .../examples/webserver/sse/SseService.java | 88 ++++++++++++++ .../examples/webserver/sse/package-info.java | 20 ++++ .../sse/src/main/resources/WEB/index.html | 107 ++++++++++++++++++ .../sse/src/main/resources/logging.properties | 20 ++++ .../webserver/sse/SseServiceTest.java | 95 ++++++++++++++++ 9 files changed, 503 insertions(+) create mode 100644 examples/webserver/sse/README.md create mode 100644 examples/webserver/sse/pom.xml create mode 100644 examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/Main.java create mode 100644 examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/SseService.java create mode 100644 examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/package-info.java create mode 100644 examples/webserver/sse/src/main/resources/WEB/index.html create mode 100644 examples/webserver/sse/src/main/resources/logging.properties create mode 100644 examples/webserver/sse/src/test/java/io/helidon/examples/webserver/sse/SseServiceTest.java diff --git a/examples/webserver/pom.xml b/examples/webserver/pom.xml index 74e48b0d..84c6b219 100644 --- a/examples/webserver/pom.xml +++ b/examples/webserver/pom.xml @@ -53,5 +53,6 @@ tracing tutorial websocket + sse diff --git a/examples/webserver/sse/README.md b/examples/webserver/sse/README.md new file mode 100644 index 00000000..1742e306 --- /dev/null +++ b/examples/webserver/sse/README.md @@ -0,0 +1,16 @@ +# Helidon SE SSE Example + +This example demonstrates how to use Server Sent Events (SSE) with both the WebServer and WebClient APIs. + +This project implements a couple of SSE endpoints that send either text or JSON messages. +The unit test uses the WebClient API to test the endpoints. + +## Build, run and test + +Build and start the server: +```shell +mvn package +java -jar target/helidon-examples-webserver-sse.jar +``` + +Then open http://localhost:8080 in your browser. diff --git a/examples/webserver/sse/pom.xml b/examples/webserver/sse/pom.xml new file mode 100644 index 00000000..a471da0f --- /dev/null +++ b/examples/webserver/sse/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.2.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-sse + 1.0.0-SNAPSHOT + Helidon Examples WebServer SSE + + + io.helidon.examples.webserver.sse.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-sse + + + io.helidon.webserver + helidon-webserver-static-content + + + jakarta.json + jakarta.json-api + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.logging + helidon-logging-jul + + + io.helidon.webclient + helidon-webclient-sse + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + \ No newline at end of file diff --git a/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/Main.java b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/Main.java new file mode 100644 index 00000000..9f90c429 --- /dev/null +++ b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/Main.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.sse; + +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.staticcontent.StaticContentService; + +/** + * This application provides a simple service with a UI to exercise Server Sent Events (SSE). + */ +class Main { + + private Main() { + } + + /** + * Executes the example. + * + * @param args command line arguments, ignored + */ + public static void main(String[] args) { + // load logging configuration + LogConfig.configureRuntime(); + + WebServer server = WebServer.builder() + .routing(Main::routing) + .port(8080) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } + + /** + * Updates the routing rules. + * + * @param rules routing rules + */ + static void routing(HttpRules rules) { + rules.register("/", StaticContentService.builder("WEB") + .welcomeFileName("index.html") + .build()) + .register("/api", new SseService()); + } +} diff --git a/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/SseService.java b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/SseService.java new file mode 100644 index 00000000..eb21de21 --- /dev/null +++ b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/SseService.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.sse; + +import java.io.UncheckedIOException; +import java.lang.System.Logger.Level; +import java.time.Duration; + +import io.helidon.http.sse.SseEvent; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.helidon.webserver.sse.SseSink; + +import jakarta.json.spi.JsonProvider; + +/** + * SSE service. + */ +class SseService implements HttpService { + + private static final System.Logger LOGGER = System.getLogger(SseService.class.getName()); + private static final JsonProvider JSON_PROVIDER = JsonProvider.provider(); + + @Override + public void routing(HttpRules httpRules) { + httpRules.get("/sse_text", this::sseText) + .get("/sse_json", this::sseJson); + } + + void sseText(ServerRequest req, ServerResponse res) throws InterruptedException { + int count = req.query().first("count").asInt().orElse(1); + int delay = req.query().first("delay").asInt().orElse(0); + try (SseSink sseSink = res.sink(SseSink.TYPE)) { + for (int i = 0; i < count; i++) { + try { + sseSink.emit(SseEvent.builder() + .comment("comment#" + i) + .name("my-event") + .data("data#" + i) + .build()); + } catch (UncheckedIOException e) { + LOGGER.log(Level.DEBUG, e.getMessage()); // connection close? + return; + } + + if (delay > 0) { + Thread.sleep(Duration.ofSeconds(delay)); + } + } + } + } + + void sseJson(ServerRequest req, ServerResponse res) throws InterruptedException { + int count = req.query().first("count").asInt().orElse(1); + int delay = req.query().first("delay").asInt().orElse(0); + try (SseSink sseSink = res.sink(SseSink.TYPE)) { + for (int i = 0; i < count; i++) { + try { + sseSink.emit(SseEvent.create(JSON_PROVIDER.createObjectBuilder() + .add("data", "data#" + i) + .build())); + } catch (UncheckedIOException e) { + LOGGER.log(Level.DEBUG, e.getMessage()); // connection close? + return; + } + + if (delay > 0) { + Thread.sleep(Duration.ofSeconds(delay)); + } + } + } + } +} diff --git a/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/package-info.java b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/package-info.java new file mode 100644 index 00000000..687abe6d --- /dev/null +++ b/examples/webserver/sse/src/main/java/io/helidon/examples/webserver/sse/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Examples WebServer SSE. + */ +package io.helidon.examples.webserver.sse; diff --git a/examples/webserver/sse/src/main/resources/WEB/index.html b/examples/webserver/sse/src/main/resources/WEB/index.html new file mode 100644 index 00000000..633518a8 --- /dev/null +++ b/examples/webserver/sse/src/main/resources/WEB/index.html @@ -0,0 +1,107 @@ + + + + + + Helidon Examples WebServer SSE + + + + +

Text events

+
+
+ + +
+
+
+

JSON events

+
+
+ + +
+
+
+ + + diff --git a/examples/webserver/sse/src/main/resources/logging.properties b/examples/webserver/sse/src/main/resources/logging.properties new file mode 100644 index 00000000..452ea9d0 --- /dev/null +++ b/examples/webserver/sse/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO diff --git a/examples/webserver/sse/src/test/java/io/helidon/examples/webserver/sse/SseServiceTest.java b/examples/webserver/sse/src/test/java/io/helidon/examples/webserver/sse/SseServiceTest.java new file mode 100644 index 00000000..e249ccb0 --- /dev/null +++ b/examples/webserver/sse/src/test/java/io/helidon/examples/webserver/sse/SseServiceTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.sse; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.json.JsonObject; + +import io.helidon.http.sse.SseEvent; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.sse.SseSource; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.Test; + +import static io.helidon.http.HeaderValues.ACCEPT_EVENT_STREAM; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class SseServiceTest { + + private final Http1Client client; + + SseServiceTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRules rules) { + rules.register(new SseService()); + } + + @Test + void testSseText() throws InterruptedException { + try (var response = client.get("/sse_text") + .queryParam("count", "3") + .header(ACCEPT_EVENT_STREAM).request()) { + var latch = new CountDownLatch(3); + var events = new ArrayList(); + response.source(SseSource.TYPE, event -> { + events.add(event); + latch.countDown(); + }); + assertThat(latch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(events.size(), is(3)); + for (int i = 0; i < 3; i++) { + var event = events.get(i); + assertThat(event.comment().orElse(null), is("comment#" + i)); + assertThat(event.name().orElse(null), is("my-event")); + assertThat(event.data(String.class), is("data#" + i)); + } + } + } + + @Test + void testSseJson() throws InterruptedException { + try (var response = client.get("/sse_json") + .queryParam("count", "3") + .header(ACCEPT_EVENT_STREAM).request()) { + var latch = new CountDownLatch(3); + var events = new ArrayList(); + response.source(SseSource.TYPE, event -> { + events.add(event.data(JsonObject.class)); + latch.countDown(); + }); + assertThat(latch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(events.size(), is(3)); + for (int i = 0; i < 3; i++) { + var event = events.get(i); + assertThat(event, is(notNullValue())); + assertThat(event.getString("data", null), is("data#" + i)); + } + } + } +} From 8361346c5d1e847e205170ba69639bd5573501dc Mon Sep 17 00:00:00 2001 From: Tim Quinn Date: Fri, 25 Oct 2024 15:22:54 -0500 Subject: [PATCH 2/6] Add expanded Jandex indexing OpenAPI example; refactor OpenAPI example accordingly (#96) * Add expanded Jandex indexing OpenAPI example; refactor OpenAPI example accordingly Signed-off-by: Tim Quinn * Fix group ID * Change parent and group ID * Add Javadoc comment to new example method --------- Signed-off-by: Tim Quinn --- examples/microprofile/openapi/basic/README.md | 33 ++++ examples/microprofile/openapi/basic/pom.xml | 99 +++++++++++ .../openapi/basic}/GreetResource.java | 4 +- .../openapi/basic}/GreetingMessage.java | 2 +- .../openapi/basic}/GreetingProvider.java | 2 +- .../basic}/internal/SimpleAPIFilter.java | 2 +- .../basic}/internal/SimpleAPIModelReader.java | 2 +- .../openapi/basic}/internal/package-info.java | 2 +- .../examples/openapi/basic}/package-info.java | 2 +- .../src/main/resources/META-INF/beans.xml | 0 .../META-INF/microprofile-config.properties | 4 +- .../src/main/resources/logging.properties | 0 .../examples/openapi/basic}/MainTest.java | 4 +- .../META-INF/microprofile-config.properties | 0 .../openapi/expanded-jandex/README.md | 74 ++++++++ .../openapi/expanded-jandex/pom.xml | 139 +++++++++++++++ .../openapi/expandedJandex/GreetResource.java | 159 ++++++++++++++++++ .../expandedJandex/GreetingMessage.java | 57 +++++++ .../expandedJandex/GreetingProvider.java | 48 ++++++ .../openapi/expandedJandex/package-info.java | 20 +++ .../src/main/resources/META-INF/beans.xml | 25 +++ .../META-INF/microprofile-config.properties | 22 +++ .../src/main/resources/logging.properties | 36 ++++ .../openapi/expandedJandex/MainTest.java | 107 ++++++++++++ .../META-INF/microprofile-config.properties | 22 +++ examples/microprofile/openapi/pom.xml | 78 +-------- 26 files changed, 862 insertions(+), 81 deletions(-) create mode 100644 examples/microprofile/openapi/basic/README.md create mode 100644 examples/microprofile/openapi/basic/pom.xml rename examples/microprofile/openapi/{src/main/java/io/helidon/microprofile/examples/openapi => basic/src/main/java/io/helidon/microprofile/examples/openapi/basic}/GreetResource.java (97%) rename examples/microprofile/openapi/{src/main/java/io/helidon/microprofile/examples/openapi => basic/src/main/java/io/helidon/microprofile/examples/openapi/basic}/GreetingMessage.java (96%) rename examples/microprofile/openapi/{src/main/java/io/helidon/microprofile/examples/openapi => basic/src/main/java/io/helidon/microprofile/examples/openapi/basic}/GreetingProvider.java (96%) rename examples/microprofile/openapi/{src/main/java/io/helidon/microprofile/examples/openapi => basic/src/main/java/io/helidon/microprofile/examples/openapi/basic}/internal/SimpleAPIFilter.java (95%) rename examples/microprofile/openapi/{src/main/java/io/helidon/microprofile/examples/openapi => basic/src/main/java/io/helidon/microprofile/examples/openapi/basic}/internal/SimpleAPIModelReader.java (97%) rename examples/microprofile/openapi/{src/main/java/io/helidon/microprofile/examples/openapi => basic/src/main/java/io/helidon/microprofile/examples/openapi/basic}/internal/package-info.java (91%) rename examples/microprofile/openapi/{src/main/java/io/helidon/microprofile/examples/openapi => basic/src/main/java/io/helidon/microprofile/examples/openapi/basic}/package-info.java (92%) rename examples/microprofile/openapi/{ => basic}/src/main/resources/META-INF/beans.xml (100%) rename examples/microprofile/openapi/{ => basic}/src/main/resources/META-INF/microprofile-config.properties (86%) rename examples/microprofile/openapi/{ => basic}/src/main/resources/logging.properties (100%) rename examples/microprofile/openapi/{src/test/java/io/helidon/microprofile/examples/openapi => basic/src/test/java/io/helidon/microprofile/examples/openapi/basic}/MainTest.java (95%) rename examples/microprofile/openapi/{ => basic}/src/test/resources/META-INF/microprofile-config.properties (100%) create mode 100644 examples/microprofile/openapi/expanded-jandex/README.md create mode 100644 examples/microprofile/openapi/expanded-jandex/pom.xml create mode 100644 examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetResource.java create mode 100644 examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetingMessage.java create mode 100644 examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetingProvider.java create mode 100644 examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/package-info.java create mode 100644 examples/microprofile/openapi/expanded-jandex/src/main/resources/META-INF/beans.xml create mode 100644 examples/microprofile/openapi/expanded-jandex/src/main/resources/META-INF/microprofile-config.properties create mode 100644 examples/microprofile/openapi/expanded-jandex/src/main/resources/logging.properties create mode 100644 examples/microprofile/openapi/expanded-jandex/src/test/java/io/helidon/microprofile/examples/openapi/expandedJandex/MainTest.java create mode 100644 examples/microprofile/openapi/expanded-jandex/src/test/resources/META-INF/microprofile-config.properties diff --git a/examples/microprofile/openapi/basic/README.md b/examples/microprofile/openapi/basic/README.md new file mode 100644 index 00000000..bcdf6e21 --- /dev/null +++ b/examples/microprofile/openapi/basic/README.md @@ -0,0 +1,33 @@ +# Helidon MP OpenAPI Example + +This example shows a simple greeting application, similar to the one from the +Helidon MP QuickStart, enhanced with OpenAPI support. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-openapi-basic.jar +``` + +Try the endpoints: + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} + +curl -X GET http://localhost:8080/openapi +#Output: [lengthy OpenAPI document] +``` +The output describes not only then endpoints from `GreetResource` but +also one contributed by the `SimpleAPIModelReader`. + + diff --git a/examples/microprofile/openapi/basic/pom.xml b/examples/microprofile/openapi/basic/pom.xml new file mode 100644 index 00000000..1fe420dd --- /dev/null +++ b/examples/microprofile/openapi/basic/pom.xml @@ -0,0 +1,99 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.2.0-SNAPSHOT + + + io.helidon.examples.microprofile + helidon-examples-microprofile-openapi-basic + 1.0.0-SNAPSHOT + Helidon Examples Microprofile OpenAPI Basic + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.microprofile.openapi + helidon-microprofile-openapi + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.glassfish.jersey.media + jersey-media-json-binding + runtime + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetResource.java b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/GreetResource.java similarity index 97% rename from examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetResource.java rename to examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/GreetResource.java index f40c6fd6..1c27a0f5 100644 --- a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetResource.java +++ b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/GreetResource.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.microprofile.examples.openapi; +package io.helidon.microprofile.examples.openapi.basic; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; @@ -50,7 +50,7 @@ * * Note that the output will include not only the annotated endpoints from this * class but also an endpoint added by the - * {@link io.helidon.microprofile.examples.openapi.internal.SimpleAPIModelReader}. + * {@link io.helidon.microprofile.examples.openapi.basic.internal.SimpleAPIModelReader}. * * The message is returned as a JSON object. */ diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingMessage.java b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/GreetingMessage.java similarity index 96% rename from examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingMessage.java rename to examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/GreetingMessage.java index 752e9b13..a3b09969 100644 --- a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingMessage.java +++ b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/GreetingMessage.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.microprofile.examples.openapi; +package io.helidon.microprofile.examples.openapi.basic; /** * POJO defining the greeting message content. diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingProvider.java b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/GreetingProvider.java similarity index 96% rename from examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingProvider.java rename to examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/GreetingProvider.java index 515214ac..c0665822 100644 --- a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingProvider.java +++ b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/GreetingProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.microprofile.examples.openapi; +package io.helidon.microprofile.examples.openapi.basic; import java.util.concurrent.atomic.AtomicReference; diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIFilter.java b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/internal/SimpleAPIFilter.java similarity index 95% rename from examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIFilter.java rename to examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/internal/SimpleAPIFilter.java index 60e872b8..d4316ff3 100644 --- a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIFilter.java +++ b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/internal/SimpleAPIFilter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.microprofile.examples.openapi.internal; +package io.helidon.microprofile.examples.openapi.basic.internal; import java.util.Map; diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIModelReader.java b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/internal/SimpleAPIModelReader.java similarity index 97% rename from examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIModelReader.java rename to examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/internal/SimpleAPIModelReader.java index 6d60bfcd..6eecf9b1 100644 --- a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIModelReader.java +++ b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/internal/SimpleAPIModelReader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.microprofile.examples.openapi.internal; +package io.helidon.microprofile.examples.openapi.basic.internal; import org.eclipse.microprofile.openapi.OASFactory; import org.eclipse.microprofile.openapi.OASModelReader; diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/package-info.java b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/internal/package-info.java similarity index 91% rename from examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/package-info.java rename to examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/internal/package-info.java index e794f83f..3104cd77 100644 --- a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/package-info.java +++ b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/internal/package-info.java @@ -17,4 +17,4 @@ /** * Internal classes supporting Helidon MP OpenAPI. */ -package io.helidon.microprofile.examples.openapi.internal; +package io.helidon.microprofile.examples.openapi.basic.internal; diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/package-info.java b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/package-info.java similarity index 92% rename from examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/package-info.java rename to examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/package-info.java index 2d75af1a..f3c2fe88 100644 --- a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/package-info.java +++ b/examples/microprofile/openapi/basic/src/main/java/io/helidon/microprofile/examples/openapi/basic/package-info.java @@ -17,4 +17,4 @@ /** * Helidon MicroProfile OpenAPI example. */ -package io.helidon.microprofile.examples.openapi; +package io.helidon.microprofile.examples.openapi.basic; diff --git a/examples/microprofile/openapi/src/main/resources/META-INF/beans.xml b/examples/microprofile/openapi/basic/src/main/resources/META-INF/beans.xml similarity index 100% rename from examples/microprofile/openapi/src/main/resources/META-INF/beans.xml rename to examples/microprofile/openapi/basic/src/main/resources/META-INF/beans.xml diff --git a/examples/microprofile/openapi/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/openapi/basic/src/main/resources/META-INF/microprofile-config.properties similarity index 86% rename from examples/microprofile/openapi/src/main/resources/META-INF/microprofile-config.properties rename to examples/microprofile/openapi/basic/src/main/resources/META-INF/microprofile-config.properties index 393a125e..8517ada2 100644 --- a/examples/microprofile/openapi/src/main/resources/META-INF/microprofile-config.properties +++ b/examples/microprofile/openapi/basic/src/main/resources/META-INF/microprofile-config.properties @@ -21,5 +21,5 @@ app.greeting=Hello server.port=8080 server.host=0.0.0.0 -mp.openapi.filter=io.helidon.microprofile.examples.openapi.internal.SimpleAPIFilter -mp.openapi.model.reader=io.helidon.microprofile.examples.openapi.internal.SimpleAPIModelReader +mp.openapi.filter=io.helidon.microprofile.examples.openapi.basic.internal.SimpleAPIFilter +mp.openapi.model.reader=io.helidon.microprofile.examples.openapi.basic.internal.SimpleAPIModelReader diff --git a/examples/microprofile/openapi/src/main/resources/logging.properties b/examples/microprofile/openapi/basic/src/main/resources/logging.properties similarity index 100% rename from examples/microprofile/openapi/src/main/resources/logging.properties rename to examples/microprofile/openapi/basic/src/main/resources/logging.properties diff --git a/examples/microprofile/openapi/src/test/java/io/helidon/microprofile/examples/openapi/MainTest.java b/examples/microprofile/openapi/basic/src/test/java/io/helidon/microprofile/examples/openapi/basic/MainTest.java similarity index 95% rename from examples/microprofile/openapi/src/test/java/io/helidon/microprofile/examples/openapi/MainTest.java rename to examples/microprofile/openapi/basic/src/test/java/io/helidon/microprofile/examples/openapi/basic/MainTest.java index 402f4572..1a704b0d 100644 --- a/examples/microprofile/openapi/src/test/java/io/helidon/microprofile/examples/openapi/MainTest.java +++ b/examples/microprofile/openapi/basic/src/test/java/io/helidon/microprofile/examples/openapi/basic/MainTest.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.helidon.microprofile.examples.openapi; +package io.helidon.microprofile.examples.openapi.basic; -import io.helidon.microprofile.examples.openapi.internal.SimpleAPIModelReader; +import io.helidon.microprofile.examples.openapi.basic.internal.SimpleAPIModelReader; import io.helidon.microprofile.testing.junit5.HelidonTest; import jakarta.inject.Inject; diff --git a/examples/microprofile/openapi/src/test/resources/META-INF/microprofile-config.properties b/examples/microprofile/openapi/basic/src/test/resources/META-INF/microprofile-config.properties similarity index 100% rename from examples/microprofile/openapi/src/test/resources/META-INF/microprofile-config.properties rename to examples/microprofile/openapi/basic/src/test/resources/META-INF/microprofile-config.properties diff --git a/examples/microprofile/openapi/expanded-jandex/README.md b/examples/microprofile/openapi/expanded-jandex/README.md new file mode 100644 index 00000000..dc10ffc9 --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/README.md @@ -0,0 +1,74 @@ +# Helidon MP OpenAPI Expanded Jandex Indexing Example + +This example shows a simple greeting application, similar to the basic OpenAPI MP example, but with proper handling of types from _outside_ this project that are used in resource method signatures. + +### Why might I need expanded Jandex handling? +Many Helidon MP applications, including those created by the Helidon command line and the starter, run the Jandex plug-in to create an index file for the types defined in the project. +Helidon's OpenAPI support leverages much of SmallRye's OpenAPI implementation and SmallRye scans the resource methods in the application to generate an OpenAPI model. This scan uses the Jandex index file directly to get information about the types used in resource methods, and SmallRye uses this information in preparing the OpenAPI document. + +If a resource method's signature uses a type _outside_ the current project SmallRye's scan cannot usually find the type information because the Jandex index created by the typical project describes only the types defined in the current project, not types in any dependencies. + +### How does this affect my application? +In such cases, SmallRye logs warning messages about the types it cannot find. This can clutter the application output. + +More importantly, the resulting OpenAPI model is less robust. When SmallRye cannot find a type it has no choice but to model that type as an opaque `Object`. The resulting OpenAPI model and document are less useful than they could be. + +Note that if the dependency contains _its own_ Jandex index for its own types then SmallRye can find information about those types. + +### What does this example do? +This example shows how to expand the Jandex index built by your project to include selected types from _outside_ the project. +There are two key differences from the basic OpenAPI example: + +1. The new resource method `mediaType`added to `GreetingResource` returns a `MediaType` from Jarkarta RESTful Web Wervices, a type from outside this project. +2. The `pom.xml` configures its invocation of the Jandex plug-in to include that type in the generated Jandex index. + +The example `pom.xml` adds a single type from a single dependency to the index. If you need to for your application add more dependencies and more include entries. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-openapi-expanded-jandex.jar +``` + +Try the endpoints. These are the same actions supported by the basic OpenAPI example: + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} + +curl -X GET http://localhost:8080/openapi +#Output: [lengthy OpenAPI document] +``` +After running the last command notice that the OpenAPI document's `components/schemas` section contains a declaration for `MediaType` that includes the `MediaType` properties (`type`, `subtype`, etc.). Further, the document's entry for `paths/greet/mediatype:get` declares the response content by referring to that exact `#/components/schemas/MediaType` entry. + +## Building and running without expanded Jandex processing +You can simulate the build without the expanded Jandex processing to see the warning SmallRye logs during startup and to see the difference in the generated OpenAPI document. +When you build the project specify the `normal-jandex` Maven profile and skip the unit tests. + +```shell +mvn clean package -Pnormal-jandex -DskipTests +java -jar target/helidon-examples-microprofile-openapi-expanded-jandex.jar +``` +As the app starts notice a message like the following in the app output: +```list +WARN io.smallrye.openapi.runtime.scanner Thread[#1,main,5,main]: SROAP04005: Could not find schema class in index: jakarta.ws.rs.core.MediaType +``` + +Retrieve the OpenAPI document: +```shell +curl -X GET http://localhost:8080/openapi +``` +Notice two things about the output: +1. The `components/schemas` section contains no entry for `MediaType`. That's expected given that SmallRye could not find the type information for it. +2. The response content for `paths/greet/mediatype:get` is simply `object`. + + While it is true that the response is an `Object` this version of the OpenAPI document for your app is less useful than the earlier one because of the missing type information for `MediaType`. \ No newline at end of file diff --git a/examples/microprofile/openapi/expanded-jandex/pom.xml b/examples/microprofile/openapi/expanded-jandex/pom.xml new file mode 100644 index 00000000..92a44581 --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/pom.xml @@ -0,0 +1,139 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.2.0-SNAPSHOT + + + io.helidon.examples.microprofile + helidon-examples-microprofile-openapi-expanded-jandex + 1.0.0-SNAPSHOT + Helidon Examples Microprofile OpenAPI with expanded Jandex handling + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.microprofile.openapi + helidon-microprofile-openapi + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.glassfish.jersey.media + jersey-media-json-binding + runtime + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + jakarta.ws.rs + jakarta.ws.rs-api + + + **/MediaType.class + + + + + + + + + + + + + + normal-jandex + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + + + + diff --git a/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetResource.java b/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetResource.java new file mode 100644 index 00000000..163aa583 --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetResource.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.examples.openapi.expandedJandex; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +/** + * A simple JAX-RS resource with OpenAPI annotations to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET http://localhost:8080/greet + *

+ * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + *

+ * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + *

+ * Get OpenAPI document for the endpoints + * curl -X GET http://localhost:8080/openapi + *

+ * Note that the output will include not only the annotated endpoints from this + * class but also an endpoint added by the + * {@link io.helidon.microprofile.examples.openapi.expanded.jandex.internal.SimpleAPIModelReader}. + *

+ * The message is returned as a JSON object. + */ +@Path("/greet") +@RequestScoped +public class GreetResource { + + /** + * The greeting message provider. + */ + private final GreetingProvider greetingProvider; + + /** + * Using constructor injection to get a configuration property. + * By default this gets the value from META-INF/microprofile-config + * + * @param greetingConfig the configured greeting message + */ + @Inject + public GreetResource(GreetingProvider greetingConfig) { + this.greetingProvider = greetingConfig; + } + + /** + * Return a worldly greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Operation(summary = "Returns a generic greeting", + description = "Greets the user generically") + @APIResponse(description = "Simple JSON containing the greeting", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = GreetingMessage.class))) + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getDefaultMessage() { + return createResponse("World"); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link GreetingMessage} + */ + @Path("/{name}") + @GET + @Operation(summary = "Returns a personalized greeting") + @APIResponse(description = "Simple JSON containing the greeting", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = GreetingMessage.class))) + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message JSON containing the new greeting + * @return {@link Response} + */ + @Path("/greeting") + @PUT + @Operation(summary = "Set the greeting prefix", + description = "Permits the client to set the prefix part of the greeting (\"Hello\")") + @RequestBody( + name = "greeting", + description = "Conveys the new greeting prefix to use in building greetings", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = GreetingMessage.class), + examples = @ExampleObject( + name = "greeting", + summary = "Example greeting message to update", + value = "{\"greeting\": \"New greeting message\"}"))) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response updateGreeting(GreetingMessage message) { + if (message.getMessage() == null) { + GreetingMessage entity = new GreetingMessage("No greeting provided"); + return Response.status(Response.Status.BAD_REQUEST).entity(entity).build(); + } + + greetingProvider.setMessage(message.getMessage()); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + /** + * Simplistic method to return a type from outside this project. + * + * @return {@link jakarta.ws.rs.core.MediaType} + */ + @Path("/mediatype") + @GET + @Produces(MediaType.APPLICATION_JSON) + public MediaType mediatype() { + return MediaType.APPLICATION_JSON_TYPE; + } + + private GreetingMessage createResponse(String who) { + String msg = String.format("%s %s!", greetingProvider.getMessage(), who); + + return new GreetingMessage(msg); + } +} diff --git a/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetingMessage.java b/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetingMessage.java new file mode 100644 index 00000000..823bd3fb --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetingMessage.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.examples.openapi.expandedJandex; + +/** + * POJO defining the greeting message content. + */ +@SuppressWarnings("unused") +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetingProvider.java b/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetingProvider.java new file mode 100644 index 00000000..86e2acba --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/GreetingProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.examples.openapi.expandedJandex; + +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Provider for greeting message. + */ +@ApplicationScoped +public class GreetingProvider { + private final AtomicReference message = new AtomicReference<>(); + + /** + * Create a new greeting provider, reading the message from configuration. + * + * @param message greeting to use + */ + @Inject + public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) { + this.message.set(message); + } + + String getMessage() { + return message.get(); + } + + void setMessage(String message) { + this.message.set(message); + } +} diff --git a/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/package-info.java b/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/package-info.java new file mode 100644 index 00000000..22d4277e --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/src/main/java/io/helidon/microprofile/examples/openapi/expandedJandex/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon MicroProfile OpenAPI example with expanded Jandex processing. + */ +package io.helidon.microprofile.examples.openapi.expandedJandex; diff --git a/examples/microprofile/openapi/expanded-jandex/src/main/resources/META-INF/beans.xml b/examples/microprofile/openapi/expanded-jandex/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000..52f89a20 --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/openapi/expanded-jandex/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/openapi/expanded-jandex/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000..6546504f --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Application properties. This is the default greeting +app.greeting=Hello + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 diff --git a/examples/microprofile/openapi/expanded-jandex/src/main/resources/logging.properties b/examples/microprofile/openapi/expanded-jandex/src/main/resources/logging.properties new file mode 100644 index 00000000..f7313283 --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/src/main/resources/logging.properties @@ -0,0 +1,36 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#org.glassfish.jersey.level=INFO +#org.jboss.weld=INFO diff --git a/examples/microprofile/openapi/expanded-jandex/src/test/java/io/helidon/microprofile/examples/openapi/expandedJandex/MainTest.java b/examples/microprofile/openapi/expanded-jandex/src/test/java/io/helidon/microprofile/examples/openapi/expandedJandex/MainTest.java new file mode 100644 index 00000000..15f8369b --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/src/test/java/io/helidon/microprofile/examples/openapi/expandedJandex/MainTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.examples.openapi.expandedJandex; + +import java.io.IOException; +import java.io.InputStream; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonPointer; +import jakarta.json.JsonString; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexReader; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +@HelidonTest +class MainTest { + + private final WebTarget target; + + @Inject + MainTest(WebTarget target) { + this.target = target; + } + + private static String escape(String path) { + return path.replace("/", "~1"); + } + + @Test + void testHelloWorld() { + GreetingMessage message = target.path("/greet") + .request() + .get(GreetingMessage.class); + assertThat("default message", message.getMessage(), + is("Hello World!")); + + message = target.path("/greet/Joe") + .request() + .get(GreetingMessage.class); + assertThat("hello Joe message", message.getMessage(), + is("Hello Joe!")); + + try (Response r = target.path("/greet/greeting") + .request() + .put(Entity.entity("{\"message\" : \"Hola\"}", MediaType.APPLICATION_JSON))) { + assertThat("PUT status code", r.getStatus(), is(204)); + } + + message = target.path("/greet/Jose") + .request() + .get(GreetingMessage.class); + assertThat("hola Jose message", message.getMessage(), + is("Hola Jose!")); + } + + @Test + public void testOpenAPI() { + JsonObject jsonObject = target.path("/openapi") + .request(MediaType.APPLICATION_JSON) + .get(JsonObject.class); + JsonObject paths = jsonObject.get("paths").asJsonObject(); + + JsonPointer jp = Json.createPointer("/" + escape("/greet") + "/get/summary"); + JsonString js = (JsonString) jp.getValue(paths); + assertThat("/greet GET summary did not match", js.getString(), is("Returns a generic greeting")); + } + + @Test + void checkJandexContentsForExternalType() { + Index indexFromMavenBuild; + try (InputStream is = getClass().getResource("/META-INF/jandex.idx").openStream()) { + IndexReader indexReader = new IndexReader(is); + indexFromMavenBuild = indexReader.read(); + } catch (IOException e) { + throw new RuntimeException(e); + } + ClassInfo mediaTypeClassInfo = indexFromMavenBuild.getClassByName(MediaType.class); + assertThat("MediaType index information from generated Jandex file", mediaTypeClassInfo, notNullValue()); + } +} diff --git a/examples/microprofile/openapi/expanded-jandex/src/test/resources/META-INF/microprofile-config.properties b/examples/microprofile/openapi/expanded-jandex/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000..0502dc34 --- /dev/null +++ b/examples/microprofile/openapi/expanded-jandex/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2019, 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Override configuration in main source branch, so we do not use 8080 port for tests +config_ordinal=1000 +# Microprofile server properties +server.port=-1 +server.host=0.0.0.0 diff --git a/examples/microprofile/openapi/pom.xml b/examples/microprofile/openapi/pom.xml index 60aceefd..e929d26e 100644 --- a/examples/microprofile/openapi/pom.xml +++ b/examples/microprofile/openapi/pom.xml @@ -22,78 +22,18 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.helidon.applications - helidon-mp - 4.2.0-SNAPSHOT - + io.helidon.examples.microprofile + helidon-examples-microprofile-project + 1.0.0-SNAPSHOT - io.helidon.examples.microprofile + io.helidon.examples.microprofile.openapi helidon-examples-microprofile-openapi 1.0.0-SNAPSHOT Helidon Examples Microprofile OpenAPI + pom - - - io.helidon.microprofile.bundles - helidon-microprofile-core - - - io.helidon.microprofile.openapi - helidon-microprofile-openapi - - - io.helidon.logging - helidon-logging-jul - runtime - - - org.glassfish.jersey.media - jersey-media-json-binding - runtime - - - io.smallrye - jandex - runtime - true - - - org.junit.jupiter - junit-jupiter-api - test - - - org.hamcrest - hamcrest-all - test - - - io.helidon.microprofile.testing - helidon-microprofile-testing-junit5 - test - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-libs - - - - - io.smallrye - jandex-maven-plugin - - - make-index - - - - - + + basic + expanded-jandex + From 10c9e81ffcf802407cd14bde2fc258839cfb4bae Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 30 Oct 2024 09:03:39 -0400 Subject: [PATCH 3/6] New gRPC SE example that implements a random number generator service. Example created for blog article. (#99) --- examples/webserver/grpc-random/README.md | 21 +++ examples/webserver/grpc-random/pom.xml | 135 ++++++++++++++++++ .../examples/webserver/grpc/random/Main.java | 49 +++++++ .../webserver/grpc/random/RandomService.java | 91 ++++++++++++ .../webserver/grpc/random/package-info.java | 20 +++ .../grpc-random/src/main/proto/random.proto | 32 +++++ .../src/main/resources/application.yaml | 30 ++++ .../grpc-random/src/main/resources/client.p12 | Bin 0 -> 4181 bytes .../src/main/resources/logging.properties | 24 ++++ .../grpc-random/src/main/resources/server.p12 | Bin 0 -> 4133 bytes .../grpc/random/RandomServiceTest.java | 128 +++++++++++++++++ .../src/test/resources/application.yaml | 30 ++++ examples/webserver/pom.xml | 1 + 13 files changed, 561 insertions(+) create mode 100644 examples/webserver/grpc-random/README.md create mode 100644 examples/webserver/grpc-random/pom.xml create mode 100644 examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/Main.java create mode 100644 examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/RandomService.java create mode 100644 examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/package-info.java create mode 100644 examples/webserver/grpc-random/src/main/proto/random.proto create mode 100644 examples/webserver/grpc-random/src/main/resources/application.yaml create mode 100644 examples/webserver/grpc-random/src/main/resources/client.p12 create mode 100644 examples/webserver/grpc-random/src/main/resources/logging.properties create mode 100644 examples/webserver/grpc-random/src/main/resources/server.p12 create mode 100644 examples/webserver/grpc-random/src/test/java/io/helidon/examples/webserver/grpc/random/RandomServiceTest.java create mode 100644 examples/webserver/grpc-random/src/test/resources/application.yaml diff --git a/examples/webserver/grpc-random/README.md b/examples/webserver/grpc-random/README.md new file mode 100644 index 00000000..f7bee41f --- /dev/null +++ b/examples/webserver/grpc-random/README.md @@ -0,0 +1,21 @@ +# Helidon gRPC SE Randome Example + + +This example shows a simple _Random_ service written with the Helidon gRPC SE +API. See `RandomService` for the service implementation and `RandomServiceTest` +for how to use the Helidon's `WebClient` to access the service. . + +The gRPC service definition is found in the `random.proto` file which is compiled +using `protoc` at build time. + +## Build and run tests + +```shell +mvn package +``` + +## Run the app + +```shell +java -jar target/helidon-examples-webserver-grpc-random.jar +``` diff --git a/examples/webserver/grpc-random/pom.xml b/examples/webserver/grpc-random/pom.xml new file mode 100644 index 00000000..b470ed81 --- /dev/null +++ b/examples/webserver/grpc-random/pom.xml @@ -0,0 +1,135 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.2.0-SNAPSHOT + + + + io.helidon.examples.webserver + helidon-examples-webserver-grpc-random + 1.0.0-SNAPSHOT + Helidon Examples WebServer gRPC Random + + + Application demonstrates the use of gRPC with a service that returns random numbers. + + + + io.helidon.examples.webserver.grpc.random.Main + + + + + io.grpc + grpc-api + + + io.helidon.config + helidon-config-yaml + + + io.helidon.grpc + helidon-grpc-core + + + io.helidon.webserver + helidon-webserver-grpc + + + io.helidon.logging + helidon-logging-jul + + + com.google.protobuf + protobuf-java + + + io.helidon.webclient + helidon-webclient-grpc + + + + javax.annotation + javax.annotation-api + provided + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + compile + compile-custom + + + + + com.google.protobuf:protoc:${version.lib.google-protobuf}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${version.lib.grpc}:exe:${os.detected.classifier} + + + + + + + \ No newline at end of file diff --git a/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/Main.java b/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/Main.java new file mode 100644 index 00000000..81376ada --- /dev/null +++ b/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/Main.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.grpc.random; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.grpc.GrpcRouting; + +class Main { + + private Main() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + // initialize global config from default configuration + Config config = Config.create(); + Config.global(config); + Config serverConfig = config.get("server"); + + // start server and register gRPC routing and health check + WebServer.builder() + .config(serverConfig) + .addRouting(GrpcRouting.builder().service(new RandomService())) + .build() + .start(); + } +} diff --git a/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/RandomService.java b/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/RandomService.java new file mode 100644 index 00000000..973f6286 --- /dev/null +++ b/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/RandomService.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.grpc.random; + +import java.util.Random; + +import io.helidon.examples.webserver.grpc.random.Random.ParamMessage; +import io.helidon.examples.webserver.grpc.random.Random.RandomMessage; +import io.helidon.webserver.grpc.GrpcService; + +import com.google.protobuf.Descriptors; +import io.grpc.stub.StreamObserver; + +class RandomService implements GrpcService { + + private final Random random = new Random(); + + @Override + public Descriptors.FileDescriptor proto() { + return io.helidon.examples.webserver.grpc.random.Random.getDescriptor(); + } + + @Override + public void update(Routing router) { + router.unary("RandomSingle", this::randomSingle) + .bidi("RandomMany", this::randomMany); + } + + private void randomSingle(ParamMessage request, StreamObserver response) { + int bound = request.getNumber(); + response.onNext(newRandomMessage(random.nextInt(bound))); + response.onCompleted(); + } + + private StreamObserver randomMany(StreamObserver response) { + return new StreamObserver<>() { + + private int bound = Integer.MIN_VALUE; + private int count = Integer.MIN_VALUE; + + @Override + public void onNext(ParamMessage paramMessage) { + // collect bound and count, in that order + if (bound == Integer.MIN_VALUE) { + bound = paramMessage.getNumber(); + } else if (count == Integer.MIN_VALUE) { + count = paramMessage.getNumber(); + } else { + onError(new IllegalStateException("Received extra input params")); + } + } + + @Override + public void onError(Throwable throwable) { + response.onError(throwable); + } + + @Override + public void onCompleted() { + // verify input params received + if (bound == Integer.MIN_VALUE || count == Integer.MIN_VALUE) { + onError(new IllegalStateException("Did not receive all input params")); + } + + // send stream of random numbers + for (int i = 0; i < count; i++) { + response.onNext(newRandomMessage(random.nextInt(bound))); + } + response.onCompleted(); + } + }; + } + + private static RandomMessage newRandomMessage(int random) { + return RandomMessage.newBuilder().setNumber(random).build(); + } +} diff --git a/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/package-info.java b/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/package-info.java new file mode 100644 index 00000000..69084065 --- /dev/null +++ b/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of gRPC in webserver. + */ +package io.helidon.examples.webserver.grpc.random; diff --git a/examples/webserver/grpc-random/src/main/proto/random.proto b/examples/webserver/grpc-random/src/main/proto/random.proto new file mode 100644 index 00000000..559d6952 --- /dev/null +++ b/examples/webserver/grpc-random/src/main/proto/random.proto @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +syntax = "proto3"; +option java_package = "io.helidon.examples.webserver.grpc.random"; + +service RandomService { + rpc RandomSingle (ParamMessage) returns (RandomMessage) {} + rpc RandomMany (stream ParamMessage) returns (stream RandomMessage) {} +} + +message ParamMessage { + int32 number = 1; +} + +message RandomMessage { + int32 number = 1; +} diff --git a/examples/webserver/grpc-random/src/main/resources/application.yaml b/examples/webserver/grpc-random/src/main/resources/application.yaml new file mode 100644 index 00000000..327fc590 --- /dev/null +++ b/examples/webserver/grpc-random/src/main/resources/application.yaml @@ -0,0 +1,30 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "server.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "server.p12" diff --git a/examples/webserver/grpc-random/src/main/resources/client.p12 b/examples/webserver/grpc-random/src/main/resources/client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..4eb3b8325cd0190163032aca86e1d4fc94856822 GIT binary patch literal 4181 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 U@+;F6i_J(6+nRj}N+2@-U-0Veh5!Hn literal 0 HcmV?d00001 diff --git a/examples/webserver/grpc-random/src/test/java/io/helidon/examples/webserver/grpc/random/RandomServiceTest.java b/examples/webserver/grpc-random/src/test/java/io/helidon/examples/webserver/grpc/random/RandomServiceTest.java new file mode 100644 index 00000000..312d9ab3 --- /dev/null +++ b/examples/webserver/grpc-random/src/test/java/io/helidon/examples/webserver/grpc/random/RandomServiceTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.grpc.random; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.examples.webserver.grpc.random.Random.ParamMessage; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webserver.Router; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; +import io.helidon.examples.webserver.grpc.random.Random.ParamMessage; +import io.helidon.examples.webserver.grpc.random.Random.RandomMessage; +import io.helidon.examples.webserver.grpc.random.RandomServiceGrpc; +import io.helidon.examples.webserver.grpc.random.RandomServiceGrpc.RandomServiceBlockingStub; + +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.lessThan; + +/** + * Tests gRPC Strings service using {@link io.helidon.webclient.api.WebClient}. + */ +@ServerTest +class RandomServiceTest { + private static final long TIMEOUT_SECONDS = 10; + private static final int BOUND = 100; + private static final int COUNT = 10; + + private final WebClient webClient; + + private RandomServiceTest(WebServer server) { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + this.webClient = WebClient.builder() + .tls(clientTls) + .baseUri("https://localhost:" + server.port()) + .build(); + } + + @SetUpRoute + static void routing(Router.RouterBuilder router) { + router.addRouting(GrpcRouting.builder().service(new RandomService())); + } + + @Test + void testRandomSingle() { + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + RandomServiceBlockingStub service = RandomServiceGrpc.newBlockingStub(grpcClient.channel()); + RandomMessage res = service.randomSingle(newParamMessage(BOUND)); + assertThat(res.getNumber(), is(lessThan(BOUND))); + } + + @Test + void testRandomMany() throws InterruptedException, TimeoutException, ExecutionException { + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + RandomServiceGrpc.RandomServiceStub service = RandomServiceGrpc.newStub(grpcClient.channel()); + CompletableFuture> future = new CompletableFuture<>(); + StreamObserver req = service.randomMany(multiStreamObserver(future)); + req.onNext(newParamMessage(BOUND)); + req.onNext(newParamMessage(COUNT)); + req.onCompleted(); + Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + int n = 0; + for (; res.hasNext(); n++) { + assertThat(res.next().getNumber(), is(lessThan(BOUND))); + } + assertThat(n, is(COUNT)); + } + + static ParamMessage newParamMessage(int n) { + return ParamMessage.newBuilder().setNumber(n).build(); + } + + static StreamObserver multiStreamObserver(CompletableFuture> future) { + return new StreamObserver<>() { + private final List value = new ArrayList<>(); + + @Override + public void onNext(ResT value) { + this.value.add(value); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value.iterator()); + } + }; + } +} diff --git a/examples/webserver/grpc-random/src/test/resources/application.yaml b/examples/webserver/grpc-random/src/test/resources/application.yaml new file mode 100644 index 00000000..00b3aef2 --- /dev/null +++ b/examples/webserver/grpc-random/src/test/resources/application.yaml @@ -0,0 +1,30 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 0 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "server.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "server.p12" diff --git a/examples/webserver/pom.xml b/examples/webserver/pom.xml index 84c6b219..1e75eb90 100644 --- a/examples/webserver/pom.xml +++ b/examples/webserver/pom.xml @@ -39,6 +39,7 @@ echo fault-tolerance grpc + grpc-random imperative multiport mutual-tls From 0027ca5d97fce6dd03fb51f183878174facc930b Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 30 Oct 2024 17:40:59 -0400 Subject: [PATCH 4/6] Updates application.yaml file and shows how to configure the gRPC protocol in WebClient. (#101) Signed-off-by: Santiago Pericas-Geertsen --- examples/webserver/grpc-random/README.md | 2 +- .../io/helidon/examples/webserver/grpc/random/Main.java | 2 +- .../examples/webserver/grpc/random/RandomServiceTest.java | 7 +++++++ .../grpc-random/src/test/resources/application.yaml | 6 ++++++ .../webserver/grpc/src/test/resources/application.yaml | 6 ------ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/examples/webserver/grpc-random/README.md b/examples/webserver/grpc-random/README.md index f7bee41f..795fa079 100644 --- a/examples/webserver/grpc-random/README.md +++ b/examples/webserver/grpc-random/README.md @@ -1,4 +1,4 @@ -# Helidon gRPC SE Randome Example +# Helidon gRPC SE Random Example This example shows a simple _Random_ service written with the Helidon gRPC SE diff --git a/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/Main.java b/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/Main.java index 81376ada..023a656e 100644 --- a/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/Main.java +++ b/examples/webserver/grpc-random/src/main/java/io/helidon/examples/webserver/grpc/random/Main.java @@ -39,7 +39,7 @@ public static void main(String[] args) { Config.global(config); Config serverConfig = config.get("server"); - // start server and register gRPC routing and health check + // start server and register gRPC routing WebServer.builder() .config(serverConfig) .addRouting(GrpcRouting.builder().service(new RandomService())) diff --git a/examples/webserver/grpc-random/src/test/java/io/helidon/examples/webserver/grpc/random/RandomServiceTest.java b/examples/webserver/grpc-random/src/test/java/io/helidon/examples/webserver/grpc/random/RandomServiceTest.java index 312d9ab3..3aa63562 100644 --- a/examples/webserver/grpc-random/src/test/java/io/helidon/examples/webserver/grpc/random/RandomServiceTest.java +++ b/examples/webserver/grpc-random/src/test/java/io/helidon/examples/webserver/grpc/random/RandomServiceTest.java @@ -26,9 +26,11 @@ import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; +import io.helidon.config.Config; import io.helidon.examples.webserver.grpc.random.Random.ParamMessage; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webclient.grpc.GrpcClientProtocolConfig; import io.helidon.webserver.Router; import io.helidon.webserver.WebServer; import io.helidon.webserver.grpc.GrpcRouting; @@ -65,9 +67,14 @@ private RandomServiceTest(WebServer server) { .trustStore(true) .keystore(Resource.create("client.p12")))) .build(); + Config config = Config.create(); + GrpcClientProtocolConfig protocolConfig = GrpcClientProtocolConfig.builder() + .config(config.get("grpc-protocol-config")) + .build(); this.webClient = WebClient.builder() .tls(clientTls) .baseUri("https://localhost:" + server.port()) + .addProtocolConfig(protocolConfig) .build(); } diff --git a/examples/webserver/grpc-random/src/test/resources/application.yaml b/examples/webserver/grpc-random/src/test/resources/application.yaml index 00b3aef2..a46f02dc 100644 --- a/examples/webserver/grpc-random/src/test/resources/application.yaml +++ b/examples/webserver/grpc-random/src/test/resources/application.yaml @@ -28,3 +28,9 @@ server: passphrase: "password" resource: resource-path: "server.p12" + +grpc-protocol-config: + poll-wait-time: PT30S + abort-poll-time-expired: true + init-buffer-size: 10000 + heartbeat-period: PT10S \ No newline at end of file diff --git a/examples/webserver/grpc/src/test/resources/application.yaml b/examples/webserver/grpc/src/test/resources/application.yaml index fa5b0ee7..c77abef3 100644 --- a/examples/webserver/grpc/src/test/resources/application.yaml +++ b/examples/webserver/grpc/src/test/resources/application.yaml @@ -33,9 +33,3 @@ server: observers: health: details: true - -grpc-client: - poll-wait-time: PT30S - abort-poll-time-expired: true - init-buffer-size: 10000 - heartbeat-period: PT10S \ No newline at end of file From aeb9cde63d48a11cce85bf6c485da57351b94457 Mon Sep 17 00:00:00 2001 From: Joe DiPol Date: Thu, 7 Nov 2024 15:36:13 -0800 Subject: [PATCH 5/6] 4.x concurrency limits (#102) * Add concurrency-limits example --- .../webserver/concurrency-limits/README.md | 139 ++++++++++++++++++ examples/webserver/concurrency-limits/pom.xml | 82 +++++++++++ .../webserver/concurrencylimits/Main.java | 75 ++++++++++ .../concurrencylimits/SleepService.java | 63 ++++++++ .../concurrencylimits/package-info.java | 17 +++ .../src/main/resources/application.yaml | 37 +++++ .../src/main/resources/logging.properties | 22 +++ .../webserver/concurrencylimits/MainTest.java | 49 ++++++ examples/webserver/pom.xml | 1 + 9 files changed, 485 insertions(+) create mode 100644 examples/webserver/concurrency-limits/README.md create mode 100644 examples/webserver/concurrency-limits/pom.xml create mode 100644 examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/Main.java create mode 100644 examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/SleepService.java create mode 100644 examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/package-info.java create mode 100644 examples/webserver/concurrency-limits/src/main/resources/application.yaml create mode 100644 examples/webserver/concurrency-limits/src/main/resources/logging.properties create mode 100644 examples/webserver/concurrency-limits/src/test/java/io/helidon/examples/webserver/concurrencylimits/MainTest.java diff --git a/examples/webserver/concurrency-limits/README.md b/examples/webserver/concurrency-limits/README.md new file mode 100644 index 00000000..14aeab27 --- /dev/null +++ b/examples/webserver/concurrency-limits/README.md @@ -0,0 +1,139 @@ +# Helidon SE Concurrency Limits Example + +This example demonstrates the Concurrency Limits feature of Helidon WebServer. +For other approaches for rate limiting see the [Rate Limit Example](../ratelimit). + +The Concurrency Limits feature provides mechanisms to limit concurrent execution of incoming requests. +Currently two algorithms are supported: + +1. Fixed: A semaphore based limit that supports queuing for a permit and timeout on the queue. +2. AIMD: Additive-increase/multiplicative-decrease. Uses a feedback control algorithm that combines linear growth of the request limit when there is no congestion with an exponential reduction when congestion is detected. + +The example does the following: + +1. Defines two ports for accepting incoming requests: 8080 and 8088 +2. 8080 is configured to use the Fixed concurrency limit algorithm +3. 8088 is configured to use the AIMD concurrency limit algorithm + +The server has two endpoints: `fixed/sleep` and `aimd/sleep`. Both sleep for the specified number of seconds to simulate a slow workload. + +These values are configured in [`application.yaml`](./src/main/resources/application.yaml) + +## Build and run + +Build and start the server: +```shell +mvn package +java -jar target/helidon-examples-webserver-concurrencylimits.jar +``` + +## Exercise the application + +### Fixed Concurrency Limit + +The server is configured to process 6 requests concurrently with a backlog queue of depth 4. Therefore it can handle bursts of up to 10 requests at a time. + +Send a burst of 15 concurrent requests to the `fixed/sleep` endpoint (each requesting a sleep of 3 seconds): +```shell +curl -s \ + --noproxy '*' \ + -o /dev/null \ + --parallel \ + --parallel-immediate \ + -w "%{http_code}\n" \ + "http://localhost:8080/fixed/sleep/3?c=[1-15]" +``` + +When the 15 concurrent requests hit the server: + +* 5 of the requests are rejected with a 503 because they exceed the size of the number of concurrent requests limit (6) plus the size of the queue (4). +* 10 of the requests are accepted by the server +* The first 6 of those return a 200 after sleeping for 3 seconds +* The next 4 requests are then processed from the queue and return a 200 after sleeping for 3 more seconds. + +You will see this in the output: +``` +503 +503 +503 +503 +503 +# Three second pause +200 +200 +200 +200 +200 +200 +# Three second pause +200 +200 +200 +200 +``` + +### AIMD Concurrency Limit + +The AIMD limiter is [adaptive](https://en.wikipedia.org/wiki/Additive_increase/multiplicative_decrease) +and will adjust according to the workload and responsiveness of the server. +If the server successfully processes requests then the limiter will increase limits (up to a max limit). +If the server starts to fail or timeout requests then the limiter will reduce limits (down to a min limit). + +In this example we set the initial and minimum limit to 6 and a max limit of 10 concurrent requests. +We configure the timeout as 3 seconds. So requests that can't be serviced within 3 seconds fail -- which can lead to a reduction of the current limit. + +First execute 15 concurrent requests, each sleeping for 3 seconds. + +```shell +curl -s \ + --noproxy '*' \ + -o /dev/null \ + --parallel \ + --parallel-immediate \ + -w "%{http_code}\n" \ + "http://localhost:8088/aimd/sleep/3?c=[1-15]" +``` + +This will process 6 request (the currently configured limit), and fail 9: + +``` +503 +503 +503 +503 +503 +503 +503 +503 +503 +# Pause for 3 seconds +200 +200 +200 +200 +200 +200 +``` + +Repeat the curl command and you will see the same results. Because we have the AIMD timeout set to 3 seconds, and our sleep operation is taking 3 seconds, the current limit is never increased. + +Now repeat the curl command, but specify a sleep time of 2 seconds: + +```shell +curl -s \ + --noproxy '*' \ + -o /dev/null \ + --parallel \ + --parallel-immediate \ + -w "%{http_code}\n" \ + "http://localhost:8088/aimd/sleep/2?c=[1-15]" +``` + +As before the first invocation will process 6 requests. But as you +repeat the curl command you will see more requests are +successful until you hit the max concurrency limit of 10. Since +each request is now taking 2 seconds (and not 3), the limiter +is able to adapt and increase the current limit up to the maximum. + +Now go back and run the curl command a few times using a sleep time +of 3, and you will see the limiter adapt again and lower the current limit. diff --git a/examples/webserver/concurrency-limits/pom.xml b/examples/webserver/concurrency-limits/pom.xml new file mode 100644 index 00000000..25eec9d1 --- /dev/null +++ b/examples/webserver/concurrency-limits/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.2.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-concurrencylimits + 1.0.0-SNAPSHOT + Helidon Examples WebServer Concurrency Limits + + + io.helidon.examples.webserver.concurrencylimits.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/Main.java b/examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/Main.java new file mode 100644 index 00000000..5c06bc01 --- /dev/null +++ b/examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/Main.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.concurrencylimits; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * The application main class. + */ +public class Main { + + private static final System.Logger LOGGER = System.getLogger(Main.class.getName()); + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * @param args command line arguments. + */ + public static void main(String[] args) { + + // load logging configuration + LogConfig.configureRuntime(); + + // initialize global config from default configuration + Config config = Config.create(); + Config.global(config); + + // Default port uses the fixed limiter + // "aimd" port uses the AIMD limiter + WebServerConfig webserverConfig = WebServer.builder() + .config(config.get("server")) + .routing(Main::fixedRouting) + .routing("aimd", Main::aimdRouting) + .buildPrototype(); + + WebServer webserver = webserverConfig.build().start(); + + LOGGER.log(System.Logger.Level.INFO, "WEB server is up! http://localhost:" + webserver.port() + "/fixed/sleep" + + " " + "http://localhost:" + webserver.port("aimd") + "/aimd/sleep"); + } + + /** + * Updates HTTP Routing. + */ + static void fixedRouting(HttpRouting.Builder routing) { + routing.register("/fixed", new SleepService()); + } + + static void aimdRouting(HttpRouting.Builder routing) { + routing.register("/aimd", new SleepService()); + } +} diff --git a/examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/SleepService.java b/examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/SleepService.java new file mode 100644 index 00000000..f8718457 --- /dev/null +++ b/examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/SleepService.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.concurrencylimits; + +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +class SleepService implements HttpService { + + private static final System.Logger LOGGER = System.getLogger(SleepService.class.getName()); + + SleepService() { + } + + @Override + public void routing(HttpRules rules) { + rules.get("/sleep/{seconds}", this::sleepHandler); + } + + /** + * Sleep for a specified number of seconds. + * The optional path parameter controls the number of seconds to sleep. Defaults to 1 + * + * @param request server request + * @param response server response + */ + private void sleepHandler(ServerRequest request, ServerResponse response) { + int seconds = request.path().pathParameters().first("seconds").asInt().orElse(1); + response.send(String.valueOf(sleep(seconds))); + } + + /** + * Sleep current thread. + * + * @param seconds number of seconds to sleep + * @return number of seconds requested to sleep + */ + private static int sleep(int seconds) { + LOGGER.log(System.Logger.Level.DEBUG, Thread.currentThread() + ": Sleeping for " + seconds + " seconds"); + try { + Thread.sleep(seconds * 1_000L); + } catch (InterruptedException e) { + LOGGER.log(System.Logger.Level.WARNING, e); + } + return seconds; + } +} diff --git a/examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/package-info.java b/examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/package-info.java new file mode 100644 index 00000000..2cd5203c --- /dev/null +++ b/examples/webserver/concurrency-limits/src/main/java/io/helidon/examples/webserver/concurrencylimits/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.concurrencylimits; diff --git a/examples/webserver/concurrency-limits/src/main/resources/application.yaml b/examples/webserver/concurrency-limits/src/main/resources/application.yaml new file mode 100644 index 00000000..0e9d72bb --- /dev/null +++ b/examples/webserver/concurrency-limits/src/main/resources/application.yaml @@ -0,0 +1,37 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + concurrency-limit: + # Default port uses fixed limit algorithm + fixed: + permits: 6 + queue-length: 4 + queue-timeout: PT10S + sockets: + - name: "aimd" + port: 8088 + host: 0.0.0.0 + concurrency-limit: + # Second port uses aimd limit algorithm + aimd: + min-limit: 6 + initial-limit: 6 + max-limit: 10 + backoff-ratio: 0.5 + timeout: PT3S \ No newline at end of file diff --git a/examples/webserver/concurrency-limits/src/main/resources/logging.properties b/examples/webserver/concurrency-limits/src/main/resources/logging.properties new file mode 100644 index 00000000..1fe67722 --- /dev/null +++ b/examples/webserver/concurrency-limits/src/main/resources/logging.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO + +#io.helidon.examples.webserver.ratelimit.SleepService.level=FINE diff --git a/examples/webserver/concurrency-limits/src/test/java/io/helidon/examples/webserver/concurrencylimits/MainTest.java b/examples/webserver/concurrency-limits/src/test/java/io/helidon/examples/webserver/concurrencylimits/MainTest.java new file mode 100644 index 00000000..c80fcea4 --- /dev/null +++ b/examples/webserver/concurrency-limits/src/test/java/io/helidon/examples/webserver/concurrencylimits/MainTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.concurrencylimits; + +import io.helidon.http.Status; +import io.helidon.webclient.api.HttpClientResponse; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class MainTest { + private final WebClient client; + + protected MainTest(WebClient client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.fixedRouting(builder); + } + + @Test + void testSleep() { + try (HttpClientResponse response = client.get("fixed/sleep/1").request()) { + assertThat(response.status(), is(Status.OK_200)); + } + } +} \ No newline at end of file diff --git a/examples/webserver/pom.xml b/examples/webserver/pom.xml index 1e75eb90..75feadef 100644 --- a/examples/webserver/pom.xml +++ b/examples/webserver/pom.xml @@ -36,6 +36,7 @@ basic basics comment-aas + concurrency-limits echo fault-tolerance grpc From d433b18ab88d92af70f831478d23fe45f0a912a3 Mon Sep 17 00:00:00 2001 From: Joe DiPol Date: Mon, 2 Dec 2024 17:26:35 -0700 Subject: [PATCH 6/6] 4.x: Change bind-address to host in socket configuration in multiport example (#104) * Use host instead of bind-address in multiport example --- .../webserver/multiport/src/main/resources/application.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/webserver/multiport/src/main/resources/application.yaml b/examples/webserver/multiport/src/main/resources/application.yaml index 8335d53e..6c1afb60 100644 --- a/examples/webserver/multiport/src/main/resources/application.yaml +++ b/examples/webserver/multiport/src/main/resources/application.yaml @@ -19,10 +19,10 @@ server: sockets: - name: "private" port: 8081 - bind-address: "localhost" + host: "localhost" - name: "admin" port: 8082 - bind-address: "localhost" + host: "localhost" features: observe: sockets: "admin"